[JASS] [SOLVED] Updating hashtable unit's abilities

Level 5
Joined
Mar 9, 2023
Messages
43
Hello!
I really thought I was decent at hashtables, but I have an issue where a repeated function continuously identifies the ability 'A0CO' (dummy) from a unit saved in a hashtable, even if the ability is removed.

Here are the relevant functions:
JASS:
function Sniper_Exposure takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local integer t_id = GetHandleId(t)
    local unit u = LoadUnitHandle(HASH, t_id, 'Snip')
    local real x = GetUnitX(u)
    local real y = GetUnitY(u)
    local integer lvl = GetUnitAbilityLevel(u, 'A0BQ')
    local real x2 = LoadReal(HASH, t_id, 'SnpX')
    local real y2 = LoadReal(HASH, t_id, 'SnpY')
    //call DisplayTextToPlayer(GetOwningPlayer(u), 0, 0, "A0CO level: " + I2S(GetUnitAbilityLevel(u, 'A0CO')))
    if GetUnitAbilityLevel(u, 'A0CO') >= 1 then
    call SetUnitAbilityLevel(u,'A0BQ',lvl+1)
    elseif (x2 == x and y2 == y) then
        call SetUnitAbilityLevel(u, 'A0BQ', lvl + 1)
    else
        call SetUnitAbilityLevel(u, 'A0BQ', 1)
    endif
    call SaveReal(HASH, t_id, 'SnpX', x)
    call SaveReal(HASH, t_id, 'SnpY', y)
    set u = null
    set t = null
endfunction

function SniperExposure takes unit u returns nothing
    local timer t
    local integer u_id = GetHandleId(u)
    local real interval = 1.55 - (GetUnitAbilityLevel(u,'A04R') * 0.15)
    local player p = GetOwningPlayer(u)
    if LoadTimerHandle(HASH, u_id, 'SnpT') != null then
        call DestroyTimer(LoadTimerHandle(HASH, u_id, 'SnpT'))
    endif
    if GetUnitAbilityLevel(u,'A04R') >= 1 then
        set t = CreateTimer()
        call SaveTimerHandle(HASH, u_id, 'SnpT', t)
        call SaveUnitHandle(HASH, GetHandleId(t), 'Snip', u)
        call TimerStart(t, interval, true, function Sniper_Exposure)
    endif
  
    set t = null
    set p = null
endfunction

function Trig_SniperLearnExposure_Actions takes nothing returns nothing
    local unit u = GetLearningUnit()
    call SniperExposure(u)
    set u = null
endfunction

What it does is creating a hashtable for a repeating timer for a specific unit, which is then increasing or resetting one of the hero's abilities depending on the hero's position.

It's linked to a jump which changes the position of the hero, thus resetting the level to 1. However, I am trying to make an optional condition which if true, prevents the lvl from resetting during the jump.

I've cleaned out my failed attempts at removing the ability. During the repeating timer I can add the 'A0CO' ability via an outside trigger, but not remove it. Is it possible to do without refreshing the SniperExposure(u) function?

Edit:


Solved! Here are the two working functions:
JASS:
function Sniper_Exposure takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local integer t_id = GetHandleId(t)
    local integer count = LoadInteger(HASH, GetHandleId(t), 'SnpC')
    local unit u = LoadUnitHandle(HASH, t_id, 'Snip')
    local real x = GetUnitX(u)
    local real y = GetUnitY(u)
    local integer lvl = GetUnitAbilityLevel(u, 'A0BQ')
    local real x2 = LoadReal(HASH, t_id, 'SnpX')
    local real y2 = LoadReal(HASH, t_id, 'SnpY')
    if sniperMoving then
    call SetUnitAbilityLevel(u,'A0BQ',lvl+1)
        set count = count + 1
    call SaveInteger(HASH, t_id, 'SnpC', count)
     if count == 2 then
    set sniperMoving = false
    set count = 0
    call SaveInteger(HASH, t_id, 'SnpC', count)
    endif
    elseif (x2 == x and y2 == y) then
        call SetUnitAbilityLevel(u, 'A0BQ', lvl + 1)
    else
        call SetUnitAbilityLevel(u, 'A0BQ', 1)
    endif
    call SaveReal(HASH, t_id, 'SnpX', x)
    call SaveReal(HASH, t_id, 'SnpY', y)
    set u = null
    set t = null
endfunction

function SniperExposure takes unit u returns nothing //remember that this function is called first from a different one
    local timer t
    local integer u_id = GetHandleId(u)
    local integer count
    local real interval = 1.55 - (GetUnitAbilityLevel(u,'A04R') * 0.15)
    local player p = GetOwningPlayer(u)
    if LoadTimerHandle(HASH, u_id, 'SnpT') != null then
        call FlushChildHashtable(HASH, GetHandleId(LoadTimerHandle(HASH, u_id, 'SnpT')))
        call DestroyTimer(LoadTimerHandle(HASH, u_id, 'SnpT'))
    endif
    if GetUnitAbilityLevel(u,'A04R') >= 1 then
        set t = CreateTimer()
        call SaveTimerHandle(HASH, u_id, 'SnpT', t)
    set count = LoadInteger(HASH, GetHandleId(t), 'SnpC')
    if count == 0 then
        call SaveInteger(HASH, GetHandleId(t), 'SnpC', count)
    endif
        call SaveUnitHandle(HASH, GetHandleId(t), 'Snip', u)
        call TimerStart(t, interval, true, function Sniper_Exposure)
    endif
    set t = null
    set p = null
endfunction
 
Last edited:
Level 41
Joined
Feb 27, 2007
Messages
5,233
Saving a handle of a unit saves... the handle, not the unit. A gamecache saves the actual unit itself, but that's not what hashtables do. So whenever you load from a hashtable it isn't loading some prior copy of the unit that used to exist but is instead returning the handle id of the unit as it currently exists and then just converts that to a unit variable for you instead of an integer. What you are describing (if I understand you correctly) is that removing the ability from the unit, then reloading a variable that points to it using the hashtable, then checking the ability level of the unit returns > 0 unexpectedly? That should never happen.

Some thoughts:
  • I don't see anywhere in your code that you actually remove 'A0CO' from the unit.
  • Have you made an error typing the rawcode and it's actually supposed to be 'A0C*Q*' like the 'A0BQ' I see below it?
  • You're not clearing the data saved to the timer anywhere. That should happen whenever you destroy the timer; you'll use FlushChildHashtable(HASH, handleidofthetimerwhichyoullgetdifferentlyindifferentfunctions). This is a sort of permanent memory leak if you don't clear the hashtable eventually. It's possible (if the timer's handle ID is immediately reused) that the new data is stored in the same place the previous data was (and thus doesn't leak 'more' data), but that is not a likely scenario to count on in the vast majority of situation.

  • If your code knows when the jump starts and when it ends (presumably it's a triggered jump from A to B), why not just set some boolean flag manually during the jump and then check against that flag to see if movement should reset the ability level or not. A single boolean variable would work for a single unit, or you could use the hashtable, or you could do something like putting the unit in a particular unit group while it's jumping (and then checking for membership of that group).
Wanted to note that this use of rawcodes as hashtable keys is super smart. An improvement over using a global int variable? No. But I've never seen anyone else do that and it looks slick as heck:
JASS:
call SaveTimerHandle(HASH, u_id, 'SnpT', t)
Checking if two reals are exactly equal to each other is a very dicey thing to do, as with floating point precision and other rounding/truncating effects its possible that the two reals aren't actually equal when they 'should' be. You generally always want to compare reals using an inequality not an equality. You can fix that here by looking at the delta between x and y coordinates to ensure the unit hasn't moved far 'enough'.
JASS:
local real x = GetUnitX(u)
local real y = GetUnitY(u)
local real x2 = LoadReal(HASH, t_id, 'SnpX')
local real y2 = LoadReal(HASH, t_id, 'SnpY')

//instead of this:
elseif (x2 == x and y2 == y) then
//do this:
elseif (RAbsBJ(x2-x) <= MOVE_THRESHOLD) and (RAbsBJ(y2-y) <= MOVE_THRESHOLD) then
//where MOVE_THRESHOLD is a global with value like 0.001 or something
 
Level 5
Joined
Mar 9, 2023
Messages
43
Thank you for the response, troubleshooting and explanations! I went with more boolean testing after reading your response. While I do understand hashtables, I can't explain exactly how it works behind the scenes, so your explanation is helpful! These two functions (among others) were created by the dev UselessLord. The usual explanation was "here you can port this", leaving it to me to figure things out. Adding to complicated functions can be tricky.

E.g. A lot of extra functionalities is conditional based on if a hero has level 1 in an ability.
In the "jump" ability trigger, I had this line when it ended.
JASS:
if GetUnitAbilityLevel(cast, 'A0CB') >= 1 then 
call PolledWait(1.75) //Yes I could use a timer, but for test purposes it's quicker.
set sniperMoving = false
endif

And the following condition in level increase function:
JASS:
    if sniperMoving then
    call SetUnitAbilityLevel(u,'A0BQ',lvl+1)
    elseif (x2 == x and y2 == y) then
        call SetUnitAbilityLevel(u, 'A0BQ', lvl + 1)
    else
        call SetUnitAbilityLevel(u, 'A0BQ', 1)
    endif

Once the bool had been flagged, it remained constantly flagged even if I set it to false outside of the lvl increase function. It was the exact same situation as when it didn't recognize that the unit no longer had a dummy ability for checks (could be how I fetch the unit u originally). To that doesn't make sense, so I have a lot to learn. I do understand refreshing saving to replace previous instances, the crux was just that refreshing the saved handles can reset the ability.

Here's the working version. I had to manipulate the bool inside, and for jump ability timing and lvl interval not to miss each other: I added a bufferzone.
JASS:
    local integer count = LoadInteger(HASH, GetHandleId(t), 'SnpC')
    local unit u = LoadUnitHandle(HASH, t_id, 'Snip')
    local real x = GetUnitX(u)
    local real y = GetUnitY(u)
    local integer lvl = GetUnitAbilityLevel(u, 'A0BQ')
    local real x2 = LoadReal(HASH, t_id, 'SnpX')
    local real y2 = LoadReal(HASH, t_id, 'SnpY')
    if sniperMoving then //Global bool instead
    call SetUnitAbilityLevel(u,'A0BQ',lvl+1)
    set count = count + 1
    call SaveInteger(HASH, t_id, 'SnpC', count)
    if count == 2 then
    set sniperMoving = false
    set count = 0
    call SaveInteger(HASH, t_id, 'SnpC', count)
    endif

And the timer:
JASS:
    local integer count
    local real interval = 1.55 - (GetUnitAbilityLevel(u,'A04R') * 0.15)
    local player p = GetOwningPlayer(u)
    if LoadTimerHandle(HASH, u_id, 'SnpT') != null then
        call DestroyTimer(LoadTimerHandle(HASH, u_id, 'SnpT'))
    endif
    if GetUnitAbilityLevel(u,'A04R') >= 1 then
        set t = CreateTimer()
        call SaveTimerHandle(HASH, u_id, 'SnpT', t)
    set count = LoadInteger(HASH, GetHandleId(t), 'SnpC')
    if count == 0 then
        call SaveInteger(HASH, GetHandleId(t), 'SnpC', count)
    endif

My own hashtables are often quite boring and generic...
JASS:
function LostFootman_Actions takes nothing returns nothing
local unit u = GetSpellAbilityUnit()
local timer t = CreateTimer()
local integer t_id = GetHandleId(t)
    call UnitAddAbility(u, 'A02P')
    call SaveUnitHandle(HASH,t_id,'unit',u)
    call SaveInteger(HASH,t_id,'time',0)
    call SetUnitVertexColor( u, 155, 155, 155, 80)
    call TimerStart(t,4.0,false,function LostFootman_timer)
set u = null
set t = null
endfunction

You're really right about the coordinate comparison! For this ability there's no need for an offset, but I really gotta memorise that native. And you're right about flushing. It's not a great concern, but I added a flush to my timer removal to flush if the timer already exists.
JASS:
    if LoadTimerHandle(HASH, u_id, 'SnpT') != null then
        call FlushChildHashtable(HASH, GetHandleId(LoadTimerHandle(HASH, u_id, 'SnpT')))
        call DestroyTimer(LoadTimerHandle(HASH, u_id, 'SnpT'))
    endif
 
Top