Zerg Lurker Crash

Level 10
Joined
May 31, 2019
Messages
136
Hello. Lately I've been working on revamping the Zerg Campaign's system code so that it's all in a modified Blizzard.j file. So far it's been going good, but today I've hit a snag with my Lurkers.

In the current release of the campaign, you can see they work just fine. However, re-doing them totally in JASS presents an opportunity to make their code more efficient.

Unfortunately, they're now crashing!

After some debugging, I've determined the exact line that is causing the crash.

JASS:
call CreateNUnitsAtLoc(1, sc_TIMER_LURKER_SPINE, lurkerOwner, spinePt, bj_UNIT_FACING)

Some context:
The Lurker fires 6 spines, each one appears in front of the last one. The loop ran when the Lurker attacks only creates dummy timer units that upon death will spawn the spines and deal damage.

What's note-worthy is that the creation of the first spine's dummy timer works just fine, no crash. It is only once the loop reaches its second iteration that the crash happens at this line.

I thought maybe my math was off in the JASS version and it was creating an invalid location or something, but from what I can tell, this part is very much the same as in the GUI version

  • -------- Prior to entering this loop, tmpPoint1 has been set to the position of the Attacking Unit (the Lurker) --------
  • Set tmpPtLurkerSpine = (tmpPoint1 offset by ((Real((Integer A))) x Lurker_SpineDistance) towards (Facing of (Attacking unit)) degrees)
  • Unit - Create 1 Lurker_SpineTimerUnitType for (Owner of (Attacking unit)) at tmpPtLurkerSpine facing Default building facing degrees
JASS:
set spinePt = PolarProjectionBJ(lurkerPt, ( I2R(bj_forLoopAIndex) * sc_REAL_LURKER_SPINE_DISTANCE ), GetUnitFacing(whichLurker))
call CreateNUnitsAtLoc(1, sc_TIMER_LURKER_SPINE, lurkerOwner, spinePt, bj_UNIT_FACING)

I also tried setting the point to a global instead of a local variable, but this did not help anything.

So I'm pretty stumped on what the issue might be. I'd be grateful if anyone were able to crack it.

Below, the full GUI (working) version and JASS (crashing) version.

  • Lurker Attack
    • Events
      • Unit - A unit Is attacked
    • Conditions
      • Or - Any (Conditions) are true
        • Conditions
          • (Unit-type of (Attacking unit)) Equal to Zerg Lurker (Burrowed)
          • (Unit-type of (Attacking unit)) Equal to Hunter Lurker (Burrowed)
    • Actions
      • -------- Set the attacking Lurker's index if it doesn't have one already. --------
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Or - Any (Conditions) are true
            • Conditions
              • LurkerUnit[(Custom value of (Attacking unit))] Not equal to (Attacking unit)
              • Lurker_CurrentIndex Equal to 0
        • Then - Actions
          • Set Lurker_CurrentIndex = (Lurker_CurrentIndex + 1)
          • Unit - Set the custom value of (Attacking unit) to Lurker_CurrentIndex
          • Set LurkerUnit[Lurker_CurrentIndex] = (Attacking unit)
        • Else - Actions
      • -------- Get the relevant position data --------
      • Set tmpPoint1 = (Position of (Attacking unit))
      • Set tmpPoint2 = (Position of (Triggering unit))
      • -------- Setup spines --------
      • Unit - Make (Attacking unit) face (Triggering unit) over 0.00 seconds
      • For each (Integer A) from 1 to Lurker_MissileSpines, do (Actions)
        • Loop - Actions
          • Game - Display to (All players) the text: (Attempting to create spine timer + (((String((Integer A))) + of Lurker ) + (String((Custom value of (Attacking unit))))))
          • -------- Set the position of the spine --------
          • Set tmpPtLurkerSpine = (tmpPoint1 offset by ((Real((Integer A))) x Lurker_SpineDistance) towards (Facing of (Attacking unit)) degrees)
          • Game - Display to (All players) the text: (Distance of spine from Lurker: + (String(((Real((Integer A))) x Lurker_SpineDistance))))
          • -------- Set the expiration timer for creating the spine --------
          • Set tmpRealLurkerSpine = (((Real((Integer A))) - 0.99) x Lurker_SpineDelay)
          • -------- CREATE TIMER FOR ART DUMMY --------
          • Unit - Create 1 Lurker_SpineTimerUnitType for (Owner of (Attacking unit)) at tmpPtLurkerSpine facing Default building facing degrees
          • Animation - Change (Last created unit)'s vertex coloring to (100.00%, 100.00%, 100.00%) with 100.00% transparency
          • Unit - Set the custom value of (Last created unit) to (Custom value of (Attacking unit))
          • Unit - Set life of (Last created unit) to (Real((Integer A)))
          • Unit - Add a tmpRealLurkerSpine second Generic expiration timer to (Last created unit)
          • -------- PLAY THE FIRST TIMER UNIT'S BIRTH ANIMATION SO THE LURKER MISSILE SOUND PLAYS --------
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • (Integer A) Equal to 1
            • Then - Actions
              • Unit - Create 1 Lurker Fire (Sound Dummy) for (Owner of (Attacking unit)) at tmpPtLurkerSpine facing Default building facing degrees
              • Animation - Play (Last created unit)'s birth animation
              • Animation - Change (Last created unit)'s vertex coloring to (100.00%, 100.00%, 100.00%) with 100.00% transparency
              • Unit - Add a 1.00 second Generic expiration timer to (Last created unit)
            • Else - Actions
          • -------- CREATE TIMER FOR DAMAGE DUMMY --------
          • Unit - Create 1 Lurker_SpineDmgUnitType for (Owner of (Attacking unit)) at tmpPtLurkerSpine facing Default building facing degrees
          • Animation - Change (Last created unit)'s vertex coloring to (100.00%, 100.00%, 100.00%) with 100.00% transparency
          • Unit - Set the custom value of (Last created unit) to (Custom value of (Attacking unit))
          • Unit - Set life of (Last created unit) to (Real((Integer A)))
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • (Unit-type of (Attacking unit)) Equal to Hunter Lurker (Burrowed)
            • Then - Actions
              • Unit - Add Hunter Lurker Spine (Passive Marker) to (Last created unit)
            • Else - Actions
          • Unit - Set level of Spine Number ID (for Lurkers) for (Last created unit) to (Integer A)
          • Unit - Add a (tmpRealLurkerSpine + Lurker_SpineDmgPoint) second Generic expiration timer to (Last created unit)
          • Custom script: call RemoveLocation(udg_tmpPtLurkerSpine)
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • LurkerUnit[(Custom value of (Attacking unit))] Equal to (Attacking unit)
        • Then - Actions
          • Game - Display to (All players) the text: Lurker successfully...
        • Else - Actions
      • -------- Clear leaks --------
      • Custom script: call RemoveLocation(udg_tmpPoint1)
      • Custom script: call RemoveLocation(udg_tmpPoint2)

JASS:
function SC_Lurker_Attack takes player lurkerOwner, unit whichLurker, unit attackedUnit, location lurkerPt returns nothing
    local integer lurkerId      = GetUnitUserData(whichLurker)
    local real lurkerAcqRange   = 0.00
    local real lurkerSpinesReal = 0.00
    local integer lurkerRange   = 0
    local integer spineCount    = sc_array_lurker_spineCount[lurkerId]
    local location spinePt      = null
    local real spineDelay       = 0.00
    local real spineDelayDamage = 0.00
    local real baseDamageAmount = sc_array_lurker_baseDamage[lurkerId]
    local integer damageUpgLvl  = 0
    local attacktype attackType = sc_array_lurker_attacktype[lurkerId]
    local boolean debugThis     = true
    
    call SetUnitFacingToFaceUnitTimed(whichLurker, attackedUnit, 0)
    
    // If the Lurker's spine amount has not been set, set it.
    if spineCount == null then
        set lurkerAcqRange = GetUnitAcquireRange(whichLurker)
        //set lurkerRange = R2I(lurkerAcqRange)
        set lurkerSpinesReal = lurkerAcqRange/100.00
        //set lurkerSpinesReal = 6.00
        //set spineCount = 6
        set spineCount = R2I(Math_Floor(lurkerSpinesReal))
        //set spineCount = ModuloInteger(lurkerRange, 100)
        set sc_array_lurker_spineCount[lurkerId] = spineCount
    endif
    
    // Set the Lurker's attack type if it's not already set
    if attackType == null then
        set attackType = ConvertAttackType(GetUnitAbilityLevel(whichLurker, sc_ABIL_ATTACK_TYPE) - 1)
        set sc_array_lurker_attacktype[lurkerId] = attackType
    endif
    
    // If the Lurker's base damage has not been set, set it.
    if baseDamageAmount == null then
        // Base damage not-yet set
        set baseDamageAmount = I2R(GetUnitPointValue(whichLurker))
        set damageUpgLvl = sc_array_lurker_damageUpgLvlPerPlayer[GetPlayerId(lurkerOwner)]
        if damageUpgLvl != GetPlayerTechCount(lurkerOwner, sc_TECH_ZERG_MISSILE_ATTACKS, true) then
            // Damage upgrade was pre-set to something higher than 0, not researched
            set damageUpgLvl = GetPlayerTechCount(lurkerOwner, sc_TECH_ZERG_MISSILE_ATTACKS, true)
            set sc_array_lurker_damageUpgLvlPerPlayer[GetPlayerId(lurkerOwner)] = damageUpgLvl
        endif
        
        set baseDamageAmount = baseDamageAmount + I2R(sc_INT_LURKER_DAMAGE_UPG_AMOUNT * damageUpgLvl)
        set sc_array_lurker_baseDamage[lurkerId] = baseDamageAmount
    endif
    
    // Compute the actual damage the Lurker should do by checking for damage buffs
    set sc_array_lurker_currentDamage[lurkerId] = SC_ApplyDamageBuff(whichLurker, baseDamageAmount)
    
    if debugThis then
        call DisplayTextToPlayer(lurkerOwner, 0, 0, "Lurker #" + I2S(lurkerId) + ":")
        
        call DisplayTextToPlayer(lurkerOwner, 0, 0, "- lurkerPt: (" + R2S(GetLocationX(lurkerPt)) + "," + R2S(GetLocationY(lurkerPt)) + ")")
        //call DisplayTextToPlayer(lurkerOwner, 0, 0, "- Acq Range: " + R2S(lurkerAcqRange))
        //call DisplayTextToPlayer(lurkerOwner, 0, 0, "- Lurker Spines (Real): " + R2S(lurkerSpinesReal))
        //call DisplayTextToPlayer(lurkerOwner, 0, 0, "- Spine Count: " + I2S(spineCount))
        call DisplayTextToPlayer(lurkerOwner, 0, 0, "- Attack Type: " + I2S(GetUnitAbilityLevel(whichLurker, sc_ABIL_ATTACK_TYPE) - 1))
        call DisplayTextToPlayer(lurkerOwner, 0, 0, "- Pre-Buff Damage: " + R2S(baseDamageAmount))
    endif
    
    // Setup the spines
    set sc_array_lurker_currentSpine[lurkerId] = 0
    call DestroyGroup(sc_array_lurker_spineDamageGroup[lurkerId])
    
    // End this code early for testing
    //return
    
    set bj_forLoopAIndex=1
    set bj_forLoopAIndexEnd=spineCount
    loop
        exitwhen bj_forLoopAIndex > bj_forLoopAIndexEnd
        if debugThis then
            //call DisplayTextToPlayer(lurkerOwner, 0, 0, "- ForLoop Index: " + I2S(bj_forLoopAIndex))
            //exitwhen bj_forLoopAIndex == 3
        endif
        // Set the position of the spine
        set spinePt = PolarProjectionBJ(lurkerPt, ( I2R(bj_forLoopAIndex) * sc_REAL_LURKER_SPINE_DISTANCE ), GetUnitFacing(whichLurker))
        // Set the delay for spawning the spine
        set spineDelay =( ( I2R(bj_forLoopAIndex) - 0.99 ) * sc_REAL_LURKER_SPINE_DELAY )
        // Set the delay for the spine dealing damage
        set spineDelayDamage = spineDelay + sc_REAL_LURKER_SPINE_DELAY + (sc_REAL_LURKER_SPINE_DUR/2.00)
            
        // End this code early for testing
        if debugThis and bj_forLoopAIndex == 2 then
            call DisplayTextToPlayer(lurkerOwner, 0, 0, "- spinePt: (" + R2S(GetLocationX(spinePt)) + "," + R2S(GetLocationY(spinePt)) + ")")
            call DisplayTextToPlayer(lurkerOwner, 0, 0, "- spineDelay: " + R2S(spineDelay))
            call DisplayTextToPlayer(lurkerOwner, 0, 0, "- spineDelayDamage: " + R2S(spineDelayDamage))
        endif
        
        // Comment out the below function call to observe that it is responsible for a crash.
         call CreateNUnitsAtLoc(1, sc_TIMER_LURKER_SPINE, lurkerOwner, spinePt, bj_UNIT_FACING)
        // CREATE TIMER FOR SPINE ART DUMMY
        if debugThis then
            exitwhen bj_forLoopAIndex == 2
        endif
        call SetUnitExploded(bj_lastCreatedUnit, true)
        call SetUnitUserData(bj_lastCreatedUnit, lurkerId)
        call UnitApplyTimedLife(bj_lastCreatedUnit, 'BTLF', spineDelay)
        
        // Create a sound dummy for the 'Lurker Fire' sound
        if bj_forLoopAIndex == 1 then
            call CreateNUnitsAtLoc(1, sc_SOUNDDUMMY_LURKER_FIRE, lurkerOwner, spinePt, bj_UNIT_FACING)
            call SetUnitExploded(bj_lastCreatedUnit, true)
            call SetUnitAnimation(bj_lastCreatedUnit, "birth")
            //call SetUnitVertexColorBJ(GetLastCreatedUnit(), 100, 100, 100, 100.00)
            call UnitApplyTimedLife(bj_lastCreatedUnit, 'BTLF', 1.00)
        endif
        
        
        // CREATE TIMER FOR DAMAGE DUMMY
        call CreateNUnitsAtLoc(1, sc_TIMER_LURKER_SPINE_DAMAGE, lurkerOwner, spinePt, bj_UNIT_FACING)
        call SetUnitExploded(bj_lastCreatedUnit, true)
        //call SetUnitVertexColorBJ(GetLastCreatedUnit(), 100, 100, 100, 100.00)
        call SetUnitUserData(bj_lastCreatedUnit, lurkerId)
        //call SetUnitAbilityLevelSwapped('Szsn', GetLastCreatedUnit(), GetForLoopIndexA())
        call UnitApplyTimedLife(bj_lastCreatedUnit, 'BTLF', spineDelayDamage )

        
        set bj_forLoopAIndex=bj_forLoopAIndex + 1
        
        // Clear leak
        call RemoveLocation(spinePt)
    endloop
    
    // Clear leak
    set attackType = null
        
endfunction

I've also attached my current Blizzard.j, as well as a zip containing a stripped-down version of my mod and a test map, so you can try it in-game yourself. Like the Zerg Campaign, it requires version 1.28 of War3.
 

Attachments

  • PiesZergLurkerJassTest.7z
    20.3 MB · Views: 12
  • Blizzard.j
    515.8 KB · Views: 19

Dr Super Good

Spell Reviewer
Level 62
Joined
Jan 18, 2005
Messages
27,083
Make sure spineCount is a reasonable value.

Does this ability crash when imported as a trigger into a test map? If so then can you provide that test map? Maybe with a copy of the GUI trigger in it as well?

If the ability does not crash when imported as a trigger but does when imported into Blizzard.j then it must be doing something unsupported by the Blizzard.j file.
 
Level 10
Joined
May 31, 2019
Messages
136
Here's a new test attachment: This time it contains two different versions of the map, one with the relevant Blizzard.j code imported directly into the map, and one using the old GUI triggers.
It still requires war3 v1.28 and the included War3Mod.MPQ

This in-map JASS version crashes the same as the OP's version that uses Blizzard.j. You can comment out the line below to have it not crash (but of course not create the necessary dummy units either) and see it display the spineCount as 6.
JASS:
// Comment out the below function call to observe that it is responsible for a crash.
         call CreateNUnitsAtLoc(1, sc_TIMER_LURKER_SPINE, lurkerOwner, spinePt, bj_UNIT_FACING)

The GUI trigger version works just fine.
 

Attachments

  • PiesZergLurkerTestGUIvsJASS.7z
    20.2 MB · Views: 13

Dr Super Good

Spell Reviewer
Level 62
Joined
Jan 18, 2005
Messages
27,083
Your version of Bribe unit indexer seems corrupt. Not only does it interfere with your own loop index A, the following function of it is an infinite loop.

JASS:
function IndexUnitCheck takes unit whichUnit returns boolean
    local integer unitType = GetUnitTypeId(whichUnit)
    local integer unitTypeToFilter = 0
   
    //Unit-Types
    set bj_forLoopAIndex=1
    set bj_forLoopAIndexEnd=sc_ARRAYMAX_INDEX_FILTER_UNIT_TYPE
    loop
        exitwhen bj_forLoopAIndex > bj_forLoopAIndexEnd
        set unitTypeToFilter = sc_ARRAY_INDEX_FILTER_UNIT_TYPE[bj_forLoopAIndex]
        if unitType == unitTypeToFilter then
            return false
        endif
    endloop
   
    return true
endfunction

bj_forLoopAIndex is never incremented inside the loop and as such the loop exit condition is impossible to meet. The freeze (or in your case, "crash") is the result of this infinite loop taking a ludicrous amount of time to execute.

Another problem is that you use loop index A in your own function at the same time units are created. As such the unit indexer may corrupt the loop index values. To fix this, rather use local integers to manage the loop.

If Bribe unit indexer is not provided in JASS form then I recommend looking for another unit indexer that is, or keeping it as GUI compiled to JASS.
 
Level 10
Joined
May 31, 2019
Messages
136
Ah yes, that turned out to be the issue. For some reason I was treating forLoopA as a local...even though it is not. lol.

That particular part of Bribe's indexer is where custom filtering is intended to happen. and so my bad loop isn't part of Bribe's code. The actual indexing appears to work correctly.

Thank you!
 
Top