• 🏆 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!

Events Revisited

Status
Not open for further replies.
Level 31
Joined
Jul 10, 2007
Messages
6,306
The big thing many of us were concerned about back in the day was correctly support recursive events. Two correct approaches were devised for this.

JASS:
/*
*    stack approach
*/
globals
    integer array stack
    integer index = 0
endglobals

function FireEvent takes integer dat returns nothing
    set index = index + 1
    set stack[index] = dat
    //Fire Event
    set index = index - 1
endfunction

/*
*    locals approach
*/
globals
    integer eventDat = 0
endglobals
function FireEvent2 takes integer dat returns nothing
    local integer prev = eventDat
    set eventDat = dat

    //Fire Event
    set eventDat = prev
endfunction

Events have always been global and they have always had one stage. For a DDS, a damage event would run all code registered to it whenever any unit was damaged. For a unit indexer, a deindex event would run for any indexed unit that was deindexed. It was up to the user to determine whether or not to run their own code using if-statements.

JASS:
//when a unit is indexed

globals
    boolean array flag
endglobals

function Index takes nothing returns nothing
    if (condition) then
        set flag[GetUnitUserData(indexedUnit)] = true

        // code
    endif
endfunction
function Deindex takes nothing returns nothing
    if (flag[GetUnitUserData(deindexedUnit)]) then
       set flag[GetUnitUserData(deindexedUnit)] = false

       // code
    endif
endfunction

This would sort of simulate local events (the deindex event is tied only to units that pass the if-statement in the index event).

The other thing was handling data that may be destroyed during the course of an event. For example, consider a unit that is removed in the middle of a damage event. If another unit is created, then the data is overwritten and the damage event becomes corrupt. AIDS devised locks. When an index was locked, it would not be recycled until it was unlocked. Later, someone decided to use a queue for recycling instead of a stack. This meant that locks were no longer needed as the recycled index wouldn't be reused for a while (in almost all cases).

The only current weakness seems to be handling local events. Furthermore, consider events that have multiple stages.

Let's consider a unit index/deindex event. Stage 1 would be index. Stage 2 would be deindex. We can consider these to be Start and Stop.

Suppose a resource B requires data from a resource A. If registering normally, the events would look like this.

Index A
Index B
Deindex A
Deindex B

The problem with the above is that the data B relies on is destroyed before B ever gets run.

What happens when we have local events, using if-statements? Well, everything is global, so they just go in with the global events and will go after whatever data the require.

Index Simulated Local C
Index A
Index B
Deindex A
Deindex B
Deindex Simulated Local C (will run whether indexed or not)


So there are two challenges. The first challenge is to make it so that events run in the correct order.

Index A
Index B
Deindex B
Deindex A

The second challenge is to get local events going (meaning that deindex events are unit-specific).

The final result would be this

Global Index A
Global Index B
Global Index C
Local Deindex C
Global Deindex B
Global Deindex A

And would go in 3 stages

Global Index
Local Deindex
Global Deindex

Why does local go before global? A local event is, for all intents and purposes, registered *after* a global event. Everything is in reverse order, so the local event ends up at the top. Global events also can't rely on local event data because that data may or may not exist, meaning that local events should always be registered after global events.

Global Start
Local Start
Local End (registration is in reverse order here)
Global End

The only way to achieve local events is to create 1 trigger for every single unit for every single event.

Each unit would then have 1 trigger on it for a unit indexer

Local Trigger End

The system itself would have 2 global triggers

Global Trigger Start
Global Trigger End

The events would fire as follows

On Index:
GlobalTrigger Start

On Deindex:
Local Trigger End
Global Trigger End


For a DDS, 4 evaluations per event

GlobalTriggStart
LocalTriggerStart
LocalTriggerEnd
GlobalTriggerEnd

However, the end triggers will still run code on them in the incorrect order! A trigger does not run in reverse! This means that the end events would have to be lists of code that would each have to be evaluated one by one...

GlobalTriggStart
LocalTriggerStart
LocalListEnd (loop through this in reverse)
GlobalListEnd (loop through this in reverse)

Now we're talking a ton of overhead.

So what if instead of using trigger conditions (TriggerAddCondition) or managing a list ourselves, we used a boolean expression to store the data? We can store it in reverse.

expression = Or(last, first)

The problem with this is that a boolean expression is rather slow when chained together

expression = Or(7, Or(6, Or(5, Or(4, Or(3, Or(2, 1))))))

However, when balanced out, it's actually faster than evaluating a trigger that relied on TriggerAddCondition

expression = Or(Or(Or(7, 6), Or(5, 4)), Or(Or(3, 2), 1))

In conclusion, there are several things to worry about when writing events

1. Data
2. Recursion Support
3. Data Destruction (locks from AIDS, consider a queue recycler)
4. Global vs Local Events (local shouldn't run for everything!)
5. Start vs End Events (Start runs forward, End runs backwards)
6. Multi-Stage Events (Start/End would be 2 stages) (how many stages might a combat system have in it?)


An example of correct order (but not local events) using Trigger

JASS:
/*
*	Here, the entry point of a system for some event is A
*
*	B depends on the data of A
*/

library A uses Trigger
	struct A extends array
		static Trigger event
		static Trigger start
		static Trigger end
		static integer data = 0
	
		private static method s takes nothing returns boolean
			set data = 100
			
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "A Start -> " + I2S(data))
			
			return false
		endmethod
		
		private static method e takes nothing returns boolean
			set data = 0
			
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "A End -> " + I2S(data))
			
			return false
		endmethod
	
		private static method onInit takes nothing returns nothing
			set event = Trigger.create(false)
			set start = Trigger.create(false)
			set end = Trigger.create(true)
			
			call start.register(Condition(function thistype.s))
			call end.register(Condition(function thistype.e))
			
			call event.reference(start)
			call event.reference(end)
		endmethod
	endstruct
endlibrary

library B uses A
	/*
	*	can do sub events if desired with triggers in B
	*/
	struct B extends array
		static integer data = 0
	
		private static method s takes nothing returns boolean
			set data = A.data*2
			
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "B Start -> " + I2S(A.data) + ", " + I2S(data))
			
			return false
		endmethod
		
		private static method e takes nothing returns boolean
			set data = 0
			
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "B End -> " + I2S(A.data) + ", " + I2S(data))
			
			return false
		endmethod
	
		private static method onInit takes nothing returns nothing
			call A.start.register(Condition(function thistype.s))
			call A.end.register(Condition(function thistype.e))
		endmethod
	endstruct
endlibrary

library User uses A, B
	struct User extends array
		static integer data = 0
		
		private static method s takes nothing returns boolean
			set data = B.data + A.data
			
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "User Start -> " + I2S(A.data) + ", " + I2S(B.data) + ", " + I2S(data))
			
			return false
		endmethod
		
		private static method e takes nothing returns boolean
			set data = 0
		
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "User End -> " + I2S(A.data) + ", " + I2S(B.data) + ", " + I2S(data))
			
			return false
		endmethod
	
		private static method onInit takes nothing returns nothing
			call A.start.register(Condition(function thistype.s))
			call A.end.register(Condition(function thistype.e))
		endmethod
	endstruct
endlibrary

struct Run extends array
	private static method onInit takes nothing returns nothing
		call A.event.fire()
	endmethod
endstruct

attachment.php
 

Attachments

  • output.jpg
    output.jpg
    12.5 KB · Views: 265
Last edited:

Cokemonkey11

Spell Reviewer
Level 30
Joined
May 9, 2006
Messages
3,545
I don't really find this interesting from a software design perspective. You should take a moment to motivate the problem before getting technical.

What exactly do you consider the issue? That controlling event response order is difficult?

StructuredDD users solve this by simply requires ImportantHandler - done.

Index events are essentially the same - you can organize their importance with the stack method, without using priority queue, simply by changing the order the observers are added.

Of course there are advantages (brevity) and disadvantages (speed) to both approaches, and I'm not arguing that the stack is better, but this is sort of a roundabout way of saying that the observer pattern can be tricky without priority. So what?
 
Status
Not open for further replies.
Top