• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Attack Indexer

Status
Not open for further replies.
JASS:
library AttackIndexer /* v1.1
**************************************************************************************
*
*   !!! THIS SYSTEM CANNOT DETECT IF AN ATTACK IS LAUNCHED !!!
*
*   A system for indexing attacks and attaching data on Wc3 attack projectiles
*
*   The system indexes an attack whenever it is attempted.
*   An attack is deindexed whenever the unit deals physical damage.
*   Take note that the damage is modified to zero damage whenever the
*   the PDD detects a physical damage from an attack that haven't been indexed.
*
*   If you are going to use the AttackIndexer, just register your code to the
*   index/deindex events (see API)   
*   Take note that the events won't fire if an attempted attack came from a unit 
*   that is not indexed by the unit indexer and if the amount of damage dealt
*   is equal to 0.
*
*************************************************************************************
*
*   */requires /*
*
*       */DamageEvent /*
*       - hiveworkshop.com/forums/jass-resources-412/system-physical-damage-detection-228456/
*
*       Please use either of the two :
*
*       */optional UnitDex /*
*       - hiveworkshop.com/forums/submissions-414/system-unitdex-unit-indexer-248209/
*       */optional UnitIndexerGUI /* 
*       - hiveworkshop.com/forums/jass-resources-412/snippet-gui-unit-indexer-vjass-plugin-268592/
*
*************************************************************************************
*
*   API
*
*   struct Attack extends array
*
*       readonly static integer indexed
*       - index of the recently indexed attack. Use this inside your onIndex event.
*       readonly static integer deindexed
*       - inex of the deindexed attack.
*
*       static method register takes code c, boolean onIndex returns nothing
*       - register code to the index events. 
*       - if onIndex = true, the code is fired whenever an attack is indexed
*
*       static method isValid takes unit u, integer attackId returns boolean
*       - check if the attack came from the unit
*
*       static method isEmpty takes unit u returns boolean
*       - check if the unit has indexed attacks
*
*       static method pop takes unit u returns integer
*       - remove the oldest indexed attack of the unit.
*
*       
*************************************************************************************/
    globals
        /*
        *   How long does an attack last?
        *   This is made for the purpose of removing attacks that are either:
        *   - stopped in the middle of launching
        *   - if the unit missed the attack
        *   - if the attack has been evaded
        */
        private constant real ATTACK_DURATION = 90
        /*
        *   Allow to null the damage if the attack is invalid?
        */
        private constant boolean NULL_INVALID_ATTACK = true
    endglobals
    
    private module Init
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule
    
    private struct AttackQueue extends array
        /*
        *   Used for checking whenever the unit is indexed
        */
        private boolean indexed
        /*
        *   the time the attack has been attempted
        */
        private real time
        /*
        *   the owner of the attack
        */
        private thistype owner
        /*
        *   The global queue
        *   all attacks, regardless of the unit, is enqueued here
        */
        private thistype g_front
        private thistype g_back
        private thistype g_next
        private thistype g_prev
        /*
        *   The unit's attack queue
        */
        private thistype front
        private thistype back
        private thistype next
        
        private static thistype array recycler
        
        private static method allocate takes nothing returns thistype
            local thistype this = recycler[0]
            if recycler[this] == 0 then
                set recycler[0] = this + 1
            else
                set recycler[0] = recycler[this]
            endif
            return this
        endmethod
        
        private method deallocate takes nothing returns nothing
            set recycler[this] = recycler[0]
            set recycler[0] = this
        endmethod
        
        private static method init takes nothing returns nothing
            set recycler[0] = 1
        endmethod
        
        implement Init
        
        static method operator [] takes unit u returns thistype
            local thistype this = GetUnitId(u)
            if not indexed and this != 0 then
                set indexed = true
            endif
            return this
        endmethod
        
        method push takes real gameTime returns thistype
            local thistype newNode = 0
            if indexed then
                /*
                *   Check if the oldest node has expired
                */
                if front != 0 and gameTime - front.time > ATTACK_DURATION then
                    /*
                    *   If true, use the front node as the new node
                    */
                    set newNode = front
                    set front = front.next
                /*
                *   Check if the oldest attack indexed has expired
                */
                elseif g_front != 0 and gameTime - g_front.time > ATTACK_DURATION then
                    set newNode = g_front
                    set g_front = g_front.g_next
                    set g_front.g_prev = 0
                else
                    /* 
                    *   if not, allocate an attack
                    */
                    set newNode = allocate()
                endif
                /*
                *   If the global queue is empty, set the newNode as the front
                */
                if g_front == 0 then
                    set g_front = newNode
                endif
                /*
                *   Push the newNode to the back
                */
                set newNode.g_prev = g_back
                set g_back.g_next = newNode
                set newNode.g_next = 0
                set g_back = newNode
                /*
                *   save the inex of the owner
                */
                set newNode.owner = this
                /*
                *   if the queue has empty, we make sure that the new node
                *   also becomes the front node
                */
                if front == 0 then
                    set front = newNode
                endif
                /*
                *   push the node to the back
                */
                set back.next = newNode
                set newNode.next = 0
                set back = newNode
                /*
                *   set the current game time
                */
                set newNode.time = gameTime
            endif
            return newNode
        endmethod
        
        method pop takes nothing returns thistype
            local thistype node = 0
            if indexed then
                /*
                *   recycle the node
                */
                call front.deallocate()
                set node = front
                /*
                *   Remove the node from the global queue
                */
                set node.g_prev.g_next = node.g_next
                set node.g_next.g_prev = node.g_prev
                /*
                *   Update the front and back of the global queue if it happens to be
                *   the front or back being popped
                */
                if node == g_front then
                    set g_front = g_front.g_next
                endif
                if node == g_back then
                    set g_back = g_back.g_prev
                endif
                /*
                *   set the new front node of the unit's queue
                */
                set front = front.next
                /*
                *   clear data
                */
                set node.time = 0
                set node.owner = 0
            endif
            return node
        endmethod
        
        method has takes thistype node returns boolean
            return indexed and node.owner == this
        endmethod
        
        method empty takes nothing returns boolean
            return indexed and front == 0
        endmethod
        
        method destroy takes nothing returns nothing
            if indexed then
                loop
                    exitwhen 0 == front
                    call pop()
                endloop
                set indexed = false
            endif
        endmethod
    endstruct
    
    struct Attack extends array
        readonly static integer indexed
        readonly static integer deindexed
        
        private static trigger indexHandler = CreateTrigger()
        private static trigger deindexHandler = CreateTrigger()
        
        private static constant timer gameTime = CreateTimer()
        
        static method register takes code c, boolean onIndex returns nothing
            if onIndex then
                call TriggerAddCondition(indexHandler, Condition(c))
            else
                call TriggerAddCondition(deindexHandler, Condition(c))
            endif
        endmethod
        
        private static method fire takes boolean onIndex returns nothing
            if onIndex then
                call TriggerEvaluate(indexHandler)
            else
                call TriggerEvaluate(deindexHandler)
            endif
        endmethod
        
        
        static method isValid takes unit u, integer attackId returns boolean
            return AttackQueue[u].has(attackId)
        endmethod
        
        static method isEmpty takes unit u returns boolean
            return AttackQueue[u].empty()
        endmethod
        
        static method pop takes unit u returns integer
            return AttackQueue[u].pop()
        endmethod
        
        private static method attack takes nothing returns boolean
            local unit u = GetAttacker()
            if GetUnitId(u) != 0 then
                /*
                *   index the attack then fire the index event
                */
                set indexed = AttackQueue[u].push(TimerGetElapsed(gameTime))
                call fire(true)
            endif
            set u = null
            return false
        endmethod
        
        private static method damaged takes nothing returns boolean
            /*
            *   We only want physical damage :)
            */
            if PDDS.damageType == PHYSICAL then
                /*
                *   check if the unit has indexed attacks
                */
                if AttackQueue[PDDS.source].empty() then
                    /*
                    *   if no, the damage is invalid
                    */
                    if NULL_INVALID_ATTACK then
                        set PDDS.amount = 0
                    endif
                else
                    /*
                    *   Deindex the attack then fire the deindex event
                    */
                    set deindexed = AttackQueue[PDDS.source].pop()
                    call fire(false)
                endif
            endif
            return false
        endmethod
        
        private static method onDeindexUnits takes nothing returns boolean
            /*
            *   Attacks of units that are deindexed should be removed
            *   to give space for new attacks
            */
            call AttackQueue[GetIndexedUnit()].destroy()
            return false
        endmethod
        
        private static method init takes nothing returns nothing
            /*
            *   Register attack event
            */
            local trigger t = CreateTrigger()
            call TriggerAddCondition(t, Condition(function thistype.attack))
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ATTACKED)
            set t = null
            /*
            *   Register to the damage event
            */
            call AddDamageHandler(function thistype.damaged)
            /*
            *   Register to the deindex event
            */
            call OnUnitDeindex(function thistype.onDeindexUnits)
            /*
            *   Run the timer for one year :D
            */
            call TimerStart(gameTime, 31557600, false, null)
        endmethod
        
        implement Init
    endstruct
endlibrary

Demo:
JASS:
library LifeSteal requires AttackIndexer
/*
*   Instead of using PDD, we are going to use Attack Indexer instead
*   so we can handle the damage modification much better
*/
    private struct T extends array
        private static real array factor
        static method attacked takes nothing returns boolean
            /*
            *   Check if unit has Mask of Death
            */
            if UnitHasItemOfTypeBJ(GetAttacker(), 'I000') then
                /*
                *   Get a random factor then attach it to the indexed attack
                */
                set factor[Attack.indexed] = GetRandomReal(0, 1)
                call BJDebugMsg("Attack indexed! :" + I2S(Attack.indexed))
                call BJDebugMsg("Attached lifesteal factor: " + R2S(factor[Attack.indexed]))
            endif
            return false
        endmethod
        
        static method damaged takes nothing returns boolean
            /*
            *   Heal the unit by getting the attached data
            */
            call SetWidgetLife(PDDS.source, GetWidgetLife(PDDS.source) + PDDS.amount*factor[Attack.deindexed])
            call BJDebugMsg("Attack deindexed! :" + I2S(Attack.deindexed))
            call BJDebugMsg("Attached lifesteal factor: " + R2S(factor[Attack.deindexed]))
            return false
        endmethod
        
        private static method onInit takes nothing returns nothing
            /*
            *   Register to the events
            */
            call Attack.register(function thistype.attacked, true) // onIndex
            call Attack.register(function thistype.damaged, false) // onDeindex
        endmethod
    endstruct
endlibrary

Changelogs:
1.1
- Fixed a bug where the push method starts allocating 0 node and allocating the same old node to all attacks.
 

Attachments

  • Attack Indexer.w3x
    54.4 KB · Views: 45
Last edited:
Updated.

I tested this out for multiple units then the system bugged out so I have fixed it.

Made 3 Stress tests to ensure that this doesn't happen again.

Units : 35
Attack Speed (all) = 0.0

Test 1 :
Total generated attacks : 8652 allocated attacks (within 4-6 minutes Game Time)

Test 2:
Total generated attacks : 10932 allocated attacks (again, Game Time)

Test 3:
Total generated attacks : 6503 allocated attacks (2 minutes real time)

During all those tests, It never happened again :D

I can sleep now
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Hmm... another failed attempt at attack indexing : (.


The only way to index attacks is to actually index the attacks. Trying to synchronize them with timers and queues will never work due to blink, evasion, curse, etc.


The only numeric value a warcraft 3 attack has is damage. Thus, setting a unit's damage and then reading that damage out will properly index the attack. If the damage is 1, then the instance of the attack is 1, etc, etc.

Also, attacks should have no correlation with units after they have fired. This causes a whole slew of new problems. They should be completely autonomous from the unit : ). This means that a unit can't have its own attack queue. There are reasons for this. Attacks can expire long after a unit is removed. Attacks can't depend on a unit for data. It must come populated with all of the data it needs. The only thing that an attack needs to know is the player that it came from. If it references the unit, it needs to do so by handle only for the case of null units. Don't want to reference any unit indices that later belong to a different unit.



Sadly, this has been the solution for the past couple of years that nobody has wanted to code =).



There is already a library on wc3c that has been there for years that does the synchronization technique. Of course, it isn't well known because it never worked right.



Nice attempt Almia : )
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
If you want to remove the attack, set .prev.next = .next and set .next.prev = .prev. I don't see what's so difficult? : o

The attack's instance is the damage it is dealt.

If you don't see why you need to use BonusMod, you need to really think hard about how Evasion, Curse, and Blink will wreak havoc on your current system. BonusMod will allow you to set the damage of an attack, which you can in turn treat as an instance. From this, you can then get the specific instance of an attack when a damage event occurs. Damage is 5? There's your instance. You can then remove the attack, even if it is in the middle of the list somewhere.

You can't deindex an attack upon damage or your system will break when an attack is splash, strikethrough, bounce, or multishot. Splash will be difficult for coders to program correctly. I guess you can retrieve the target point of the attack and use that as the epicenter. Thus attacks should always have a target location : D.


Keep in mind that unit is attacked will not get all attacks. Consider artillery. A tower is told to attack the ground and that tower has splash damage. Units within the point of impact get hit. How do you detect that attack given that there is no unit being attacked?
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
On what exact time the damage bonus is applied and removed?

I am not sure if you have to set the damage before the attack or if you can set it on the attack. You will have to test.

Are you 100% sure that this might not affect the gameplay?

Yup

Why do you think my script will be broken?

If attack 1 misses, your attack 1 won't pop. Attack 2 hits, it registers as attack one.

Let's say that every attack has an additional bonus so that attack 1 does 1 damage, attack 2 does 2 damage, etc.

1, 2, 3, 4, 5

In this case, these attacks hit

2, 4, 5

These are what your system will fire

1, 2, 3

The actual damage should be 11, but your system will only do 6.

The same can happen with blink, a projectile hitting sooner than expected. Curse can also throw things out of whack. You need to actually get the actual attack, not expect them to hit in order. They won't hit in order.



Let's say you deindex on damage.

Multishot occurs. The first shot hits, damage fires, deindexed attack. The second unit gets hit. Uh oh, the attack is gone. Gg.
Same happens for splash, bounce, strikethrough, and cleave.
 
Status
Not open for further replies.
Top