• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[vJASS] Need to Tweak & Optimize Attack Height Damage System

Status
Not open for further replies.
Level 6
Joined
Sep 19, 2005
Messages
169
Hi. A long time ago someone I know made a system for my map. The system is supposed to give units who are above the units they are attacking an increase in the amount of damage they do. The system works fine but in my tests the map crashes when there are too many units fighting at once. Is there maybe another damage detection system that is more efficient? The script was written a while ago. I'm going to use the system in my map for the 1v1 mode because theres a lot less units and the engine can handle the system.

Also I want don't want ships to be able to use this system because ships always fight on even ground. Can this be filtered by unit movement type (float) or something?

Code:
JASS:
library BoolexprUtils initializer init
    globals
        boolexpr BOOLEXPR_TRUE=null
        boolexpr BOOLEXPR_FALSE=null
    endglobals

    private function rettrue takes nothing returns boolean
        return true
    endfunction

    private function retfalse takes nothing returns boolean
        return false
    endfunction

    private function init takes nothing returns nothing
        set BOOLEXPR_TRUE=Condition(function rettrue)
        set BOOLEXPR_FALSE=Condition(function retfalse)
    endfunction
endlibrary

library LightLeaklessDamageDetect initializer Init
    
    // Creating threads off of this that last longer than the timeout below will likely cause issues, like everything blowing up (handle stack corruption)
    // It seems that threads created by timers, rather than executefunc / .evaluate / .execute are not affected. Any threads created from the timer thread are fine.
    // This being safe with even the usage laid out above isn't guarenteed. Use at own risk.
    // If you start getting random bugs, see if commenting out the timer line below (see comments) helps
    // If it does, report it in the thread for this script at [url]www.wc3campaigns.net[/url]
    
    globals
        private constant real SWAP_TIMEOUT = 600. // keep high; 600 should be about the right balance.
    endglobals
    
    globals
        private conditionfunc array func
        private integer funcNext = 0
        private trigger current = null
        private trigger toDestroy = null
        private group swapGroup
        private rect mapRect
    endglobals
    
    // One of the only accessible functions. Use it to add a condition. Must return boolean type, and then have return false at the end.
    // Note that it's technically a condition, so if you put a wait in there, it'll die. But waits are lame anyway.
    function AddOnDamageFunc takes conditionfunc cf returns nothing
        call TriggerAddCondition(current, cf)
        set func[funcNext] = cf
        set funcNext = funcNext + 1
    endfunction
    
    // These inline. For avoiding feedback loops. Feel free to make your own wrapper function for damage functions using this.
    function DisableDamageDetect takes nothing returns nothing
        call DisableTrigger(current)
    endfunction
    function EnableDamageDetect takes nothing returns nothing
        call EnableTrigger(current)
    endfunction
    
    // no more accessible functions, folks.
    
    //! textmacro CGLeaklessDamageDetectAddFilter takes UNIT
        
        // add here any conditions to add the unit to the trigger, example below, commented out:
        // if GetUnitTypeId($UNIT$) != 'h000' then // where 'h000' is a dummy unit
        call TriggerRegisterUnitEvent(current, $UNIT$, EVENT_UNIT_DAMAGED)
        // endif
        
    //! endtextmacro
    
    private function AddEx takes nothing returns boolean
        //! runtextmacro CGLeaklessDamageDetectAddFilter("GetFilterUnit()")
        return false
    endfunction

    private function Enters takes nothing returns boolean
        //! runtextmacro CGLeaklessDamageDetectAddFilter("GetTriggerUnit()")
        return false
    endfunction
    
    private function Swap takes nothing returns nothing
        local integer i = 0
        local boolean b = IsTriggerEnabled(current)
        
        call DisableTrigger(current)
        if toDestroy != null then
            call DestroyTrigger(toDestroy)
        endif
        set toDestroy = current
        set current = CreateTrigger()
        
        if not(b) then
            call DisableTrigger(current)
        endif
        
        call GroupEnumUnitsInRect(swapGroup, mapRect, Filter(function AddEx))
        
        loop
            exitwhen i >= funcNext
            call TriggerAddCondition(current, func[i])
            set i = i + 1
        endloop
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local region r = CreateRegion()
        local integer i = 0
        set mapRect = GetWorldBounds()
        call RegionAddRect(r, mapRect)
        call TriggerRegisterEnterRegion(t, r, null)
        call TriggerAddCondition(t, Condition(function Enters))
        
        set swapGroup = CreateGroup()
        
        set current = CreateTrigger()
        loop
            exitwhen i >= funcNext
            call TriggerAddCondition(current, func[i])
            set i = i + 1
        endloop
        
        call GroupEnumUnitsInRect(swapGroup, GetWorldBounds(), Filter(function AddEx))
        
        // Commenting out the next line will make the system leak indexes and events, but should make it safer.
        call TimerStart(CreateTimer(), SWAP_TIMEOUT, true, function Swap)
    endfunction
    
endlibrary
JASS:
library AttackHeightAdvantage initializer Init requires LightLeaklessDamageDetect, BoolexprUtils
    //Written by Pyrogasm for Psycomarauder
    //
    //It's pretty simple: just modify the below calculation however you like. Use teh maths!
    //Only return the bonus damage you'd like to apply (or subtract, if you give it negative damage)

    globals
        private constant integer ORB_ABILITYID = 'Admg'
        private constant integer ORB_BUFFID = 'Bdmg'
        private constant attacktype ATTACK_TYPE = ATTACK_TYPE_CHAOS
        private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_UNIVERSAL

        private boolean SystemActive = false
        
        private location ZLoc = null
    endglobals

    private function DamageCalculation takes unit Source, unit Target, real Damage returns real
        local real D

        call MoveLocation(ZLoc, GetUnitX(Source), GetUnitY(Source))
        set D = GetLocationZ(ZLoc)
        call MoveLocation(ZLoc, GetUnitX(Target), GetUnitY(Target))
        set D = D-GetLocationZ(ZLoc)

        //For every 80 height, the attacker gains 7% damage bonus
        return D/80.00*0.07*Damage
    endfunction
    
    function AttackHeightAdvantage_Activate takes boolean activate returns nothing
        set SystemActive = activate
    endfunction

    private function OnDamage takes nothing returns boolean
        local unit T = GetTriggerUnit()
        local unit S = GetEventDamageSource()

        if GetUnitAbilityLevel(T, ORB_BUFFID) > 0 and SystemActive and not IsUnitType(S, UNIT_TYPE_STRUCTURE) then
            call UnitRemoveAbility(T, ORB_BUFFID)

            call DisableDamageDetect()
            call UnitDamageTarget(S, T, DamageCalculation(S, T, GetEventDamage()), false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
            call EnableDamageDetect()
        endif

        set T = null
        set S = null
        return false
    endfunction

    private function EnterAdd takes nothing returns boolean
        local unit U = GetTriggerUnit()
        local unit S = GetEventDamageSource()
        if not IsUnitType(S, UNIT_TYPE_STRUCTURE) then
            call UnitAddAbility(U, ORB_ABILITYID)
            call UnitMakeAbilityPermanent(U, true, ORB_ABILITYID)
        endif

        set U = null
        return false
    endfunction

    private function InitAdd takes nothing returns nothing
        local unit U = GetEnumUnit()
    if not IsUnitType(U, UNIT_TYPE_STRUCTURE) then
        call UnitAddAbility(U, ORB_ABILITYID)
        call UnitMakeAbilityPermanent(U, true, ORB_ABILITYID)
    endif
        set U = null
    
    endfunction


    private function Init takes nothing returns nothing
        local group G = CreateGroup()
        local trigger T = CreateTrigger()
        local region R = CreateRegion()

        call GroupEnumUnitsInRect(G, bj_mapInitialPlayableArea, BOOLEXPR_TRUE)
        call ForGroup(G, function InitAdd)
        call DestroyGroup(G)

        call RegionAddRect(R, bj_mapInitialPlayableArea)
        call TriggerRegisterEnterRegion(T, R, BOOLEXPR_TRUE)
        call TriggerAddCondition(T, Condition(function EnterAdd))

        set ZLoc = Location(0.00, 0.00)
        call AddOnDamageFunc(Condition(function OnDamage))

        set G = null
        set R = null
    endfunction
endlibrary
 
Chances are if you have enough units to crash the system, you don't really need the system.

It's generally worth noting that the wc3 engine isn't built to handle damage detection with more than 200 units or so.

The most efficient system for DD I've seen to date works by handling small groups of about 20 units at a time in a dynamic struct stack.

A system like that is the kind of thing you base an entire map around, not just add some small change like additional damage for units on low ground.
 
Level 6
Joined
Sep 19, 2005
Messages
169
Of course ha, thats what I plan to do. But I was wondering if you guys could help me modify the code a bit. After doing some more testing, I think this system starts to crap out when mortars attack whilst other fighting is going. The splash damage is I'm assuming firing the damage system but more quickly than it can handle. Any chance you could help me modify this so that nothing happens if ships or units of a specific type are the attackers? Hopefully if the code is firing off less than wc3 will be able to handle it.

Read in another thread that apparently the Damage detection system (Light Leakless) is leaking a rect. Is this version of the system still doing that? I'm pretty damn noob when it comes to JASS...
 
Level 6
Joined
Sep 19, 2005
Messages
169
BoolExpr is only used for this system.

Detect System:
JASS:
library LightLeaklessDamageDetect initializer Init
    
    // Creating threads off of this that last longer than the timeout below will likely cause issues, like everything blowing up (handle stack corruption)
    // It seems that threads created by timers, rather than executefunc / .evaluate / .execute are not affected. Any threads created from the timer thread are fine.
    // This being safe with even the usage laid out above isn't guarenteed. Use at own risk.
    // If you start getting random bugs, see if commenting out the timer line below (see comments) helps
    // If it does, report it in the thread for this script at [url]www.wc3campaigns.net[/url]
    
    globals
        private constant real SWAP_TIMEOUT = 600. // keep high; 600 should be about the right balance.
    endglobals
    
    globals
        private conditionfunc array func
        private integer funcNext = 0
        private trigger current = null
        private trigger toDestroy = null
        private group swapGroup
        private rect mapRect
    endglobals
    
    // One of the only accessible functions. Use it to add a condition. Must return boolean type, and then have return false at the end.
    // Note that it's technically a condition, so if you put a wait in there, it'll die. But waits are lame anyway.
    function AddOnDamageFunc takes conditionfunc cf returns nothing
        call TriggerAddCondition(current, cf)
        set func[funcNext] = cf
        set funcNext = funcNext + 1
    endfunction
    
    // These inline. For avoiding feedback loops. Feel free to make your own wrapper function for damage functions using this.
    function DisableDamageDetect takes nothing returns nothing
        call DisableTrigger(current)
    endfunction
    function EnableDamageDetect takes nothing returns nothing
        call EnableTrigger(current)
    endfunction
    
    // no more accessible functions, folks.
    
    //! textmacro CGLeaklessDamageDetectAddFilter takes UNIT
        
        // add here any conditions to add the unit to the trigger, example below, commented out:
        // if GetUnitTypeId($UNIT$) != 'h000' then // where 'h000' is a dummy unit
        call TriggerRegisterUnitEvent(current, $UNIT$, EVENT_UNIT_DAMAGED)
        // endif
        
    //! endtextmacro
    
    private function AddEx takes nothing returns boolean
        //! runtextmacro CGLeaklessDamageDetectAddFilter("GetFilterUnit()")
        return false
    endfunction

    private function Enters takes nothing returns boolean
        //! runtextmacro CGLeaklessDamageDetectAddFilter("GetTriggerUnit()")
        return false
    endfunction
    
    private function Swap takes nothing returns nothing
        local integer i = 0
        local boolean b = IsTriggerEnabled(current)
        
        call DisableTrigger(current)
        if toDestroy != null then
            call DestroyTrigger(toDestroy)
        endif
        set toDestroy = current
        set current = CreateTrigger()
        
        if not(b) then
            call DisableTrigger(current)
        endif
        
        call GroupEnumUnitsInRect(swapGroup, mapRect, Filter(function AddEx))
        
        loop
            exitwhen i >= funcNext
            call TriggerAddCondition(current, func[i])
            set i = i + 1
        endloop
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local region r = CreateRegion()
        local integer i = 0
        set mapRect = GetWorldBounds()
        call RegionAddRect(r, mapRect)
        call TriggerRegisterEnterRegion(t, r, null)
        call TriggerAddCondition(t, Condition(function Enters))
        
        set swapGroup = CreateGroup()
        
        set current = CreateTrigger()
        loop
            exitwhen i >= funcNext
            call TriggerAddCondition(current, func[i])
            set i = i + 1
        endloop
        
        call GroupEnumUnitsInRect(swapGroup, GetWorldBounds(), Filter(function AddEx))
        
        // Commenting out the next line will make the system leak indexes and events, but should make it safer.
        call TimerStart(CreateTimer(), SWAP_TIMEOUT, true, function Swap)
    endfunction
    
endlibrary
 
After looking through everything, I can safely say the damage detection system is definitely what's causing the problem.

No simple changes to it are going to save your map - if you're experiencing lag, only a new damage detection could make it better.

And by the way making naval units not part of the system won't work because you need to make it so ground units can attack naval units and vice versa.

and if you check target AND attacker for naval units, it's going to lag even more than just letting them use the system together.
 
Level 6
Joined
Sep 19, 2005
Messages
169
Whats so bad about the current detection system? It was made in 08 but it was ok'd at wc3c at the time, although later in the thread I guess a leak what detected and never fixed. Is it hard to change it to use a different detection system? My map doesn't depend on this to 'save' the map at all, its just that it would add another element of strategy/
 
In english, this system doesn't "recycle" correctly. There is a trigger that will approach infinite events (causing a crash).

There are 3 known methods for dynamic DD triggers (that I know of)

1: A global trigger that gets a new event for every unit. (fastest, but crashes eventually. Your system uses this.)

2: A trigger for every unit in the map (slowest, but won't crash at least - considered bad practice to use this because of the sheer number of dynamically allocated triggers)

3. A stack of triggers which each handle something like 20 units. When all the units in a trigger are no longer in the DD system, that index is removed. (There is no public system out there that implements this perfectly afaik. - The last I heard of this was from PurplePoot)
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,258
As he does destroy the trigger it does not leak.

Yes triggers do not leak at the slightest if done properly from my experiements. Need to test conditions and actions though if those leak.

The problem can be something stupid...
As timers are likly interupt fired (a timeout of 1/10000 of a second is valid), it is possible that the trigger gets destroyed as a unit damage sequence is being run. Especially if it is at the stage to evaluate the events hooked to a unit, destroying the trigger could cause serious problems and inconsistencies (disabling a trigger probably just invalidates its observer but does not remove it). The reason timers do not cause problems with modifying globals is probably because it gets queued onto the interpreter thread.

This would also explain a lot of other random crashes involving timed code and why they seem unsolvable (if consistency gets interrupted at key stages).

This however is a very wild theory and I have virtually no proof. Getting proof is hard as no one has bothered documenting the inner workings of the JASS engine.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,258
He has a queue of size 1.

Every 600 seconds.
1. if queue has a trigger, dequeue and destroy.
2. Pause current trigger.
3. Enqueue current trigger.
4. Create new current trigger with same state as previous trigger.
5. Add damage observer for all units on map to new trigger.
6. Add all observers to new trigger.
 
Level 6
Joined
Sep 19, 2005
Messages
169
Well damn. I mean I took a computer science 1 class and they taught me how to be ultra pro with qbasic, but vjass systems are still mostly beyond me :/

Anyone wanna take a shot at rebuilding the system with a different detection system and/or add those filters I talked about so it doesnt fire for ships & a specific unit type? Maybe when it adds the damage observer only add for unit types not matching the ship or specifc unit id? Or would we run into the same sort of issues? The map is consistently one of the most played maps on bnet and I can give you massive credit in whatever form you want ingame if that is an incentive at all.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,258
A possible design has 3 basic systems.

1. For units that will never be removed (eg heroes).
2. For units that will be removed (eg lane spawns)
3. For mixed (eg towers)

By breaking this appart, we have 3 separate groups.

Units that are of Group 1 will always exist so the trigger handling group 1 will never have to be destroyed. In the rare case that a unit is removed (eg a dropper), the event could be left to leak. As no triggers get destroyed, a Crash should be impossible.

Units that are of Group 2 will alwasy be removed very quickly so triggers here will need a short life cycle. Breaking it down into 20 units a trigger and only destroying the trigger once all units it observes have been removed should work. As all the affect units are removed, a crash should be impossible from this section.

This means that all the risk would be comming from Group 3 units. Ofcourse if you are willing to leak more you could potentially class Group 3 as Group 1.

But I would not be suprized if timers are involved in some way, especially since the crashes are very random and unexplained.
 
The Damage Detection Engine concept that I developed was this:

Store all unit events on one trigger
Every time 15 units are removed, destroy the trigger and rebuild it

Using trigger conditions instead of actions, this prevents any "handle stack corruption". It's the most well-rounded approach.

I told my idea to Nestharus and gave him permission to upload it with some improvements, hence this was born: http://www.hiveworkshop.com/forums/jass-functions-413/snippet-damageevent-186829/
 
The problem with that method is that you could run into a situation where you just remove ten from group A, ten from group B, etc... leaving all triggers with plenty of leaks instead of one trigger constantly refreshed of leaks.

I like Dr Super Good's suggestion. If a damage detection engine was taylored for each specific map you could see some pretty amazing results. The thing is a generic damage detection engine is the only thing that would work too well as a public resource, whereas special systems like that would require work for each map with its own unique setup.
 
The problem with that method is that you could run into a situation where you just remove ten from group A, ten from group B, etc... leaving all triggers with plenty of leaks instead of one trigger constantly refreshed of leaks.

Let's take a very liberal example and assume the map is 6v6 and each player has 3 heroes.

Now let's assume that for each hero created, 19 other units are created in between them.

In this worst case scenario, the map (in the late game) would have 36 triggers of damage detection for each hero.

Now let's assume it's very late game and the map employs something like starcraft proportions. Everyone has a maxed out, 200 unit army (all units cost 1 food).

200 units * 12 players / 20 units per trigger = 120 triggers

and let's just say it has 1 extra trigger for a swap.

36 + 120 + 1 = 157 triggers for damage detection, all leakless and working without a crash.

But what happens when we take a more realistic example? How about an AoS like super good mentioned?

Let's be very liberal again, and assume the AoS is EotA on the biggest terrain region.

It's 5v5 and each computer controls at most 2 AI heroes each.

We know that all the player's heroes are spawned at the same time, and assuming where in trigger they're added to damage detection, we know there is at max 2 triggers. (first spawned hero allocates spot #19 in one trigger, next 9 heroes take slots #0-8 in the next trigger)

Now assume that the AI heroes are all spawned with time between them, so at most 4 more triggers for those.

The biggest EotA region has 10 bases for each team, which when fully upgraded support 7 spawns each. Each base additionally spawns 5 units by default.

20 bases * (5 units + 7 spawns) / 20 units per trigger = 12 triggers for units

And let's just say there's 2 more triggers for swap.

2+4+12+2=20 triggers for arguably one of the most massive maps in terms of units and size for bnet.

I'm not saying that tailoring a DD system to a map is a bad idea, I'm just trying to say it's possible (and reasonable) to make a system that works for all maps.
 
Status
Not open for further replies.
Top