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

[vJASS] No code arrays, so....

Status
Not open for further replies.
Level 39
Joined
Feb 27, 2007
Messages
5,023
I want to be able to have a user pass functions to one of my functions so that for a particular struct I can .evaluate() those functions when a certain thing happens. Some places I want to do this the next line is the .destroy() so I want to use evaluate instead of running in another thread.
JASS:
struct s
    code onUnitDeath
endfunction

//in Unit death trigger
call someS.onUnitDeath.evaluate()
call someS.destroy()

//user code:
call s.create(... , function CustomOnDeath)

But you can't have code arrays! So what do I do, make it a boolexpr? Or do I have to make it a string input for ExecuteFunc (thus forcing into another thread):
JASS:
struct s
    string onUnitDeath = ""
endfunction

//in Unit death trigger
call ExecuteFunc(someS.onUnitDeath)
call someS.destroy()

//user code:
call s.create(... , "CustomOnDeath")
 

Deleted member 219079

D

Deleted member 219079

Use filters, upon execution add as condition to empty trigger, evaluate it and clear trigger conditions.
 
Level 39
Joined
Feb 27, 2007
Messages
5,023
I don't see how that would help here. I want users of my system to be able to write their own functions to call and be able to set them in the user's functions just like trigger register calls. If necessary I can use a function interface to just inherit everything from my main struct and overwrite a .onDeath() method myself but that totally changes how users access the system.
 

Deleted member 219079

D

Deleted member 219079

extends is implemented badly. I highly discourage using them.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
You are making an Event thingy it seems.
Well... what .evaluate() does is simply running a trigger with that function as condition.
So instead of doing that, you should just run a trigger instead as trigger arrays are allowed.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
JASS:
struct Event
   
    readonly static constant hashtable TABLE = InitHashtable()
   
    private trigger trig
   
    public static method create takes nothing returns thistype
        local thistype this = .allocate()
        set .trig = CreateTrigger()
        return this
    endmethod
   
    public method destroy takes nothing returns nothing
        call FlushChildHashtable(.TABLE, this)
        call DestroyTrigger(.trig)
        set .trig = null
        call .deallocate()
    endmethod
   
    public method register takes code c returns nothing
        local boolexpr be = Condition(c)
        call SaveTriggerConditionHandle(.TABLE, this, GetHandleId(be), TriggerAddCondition(.trig, be))
        call DestroyBoolExpr(be)
        set be = null
    endmethod
   
    public method unregister takes code c returns nothing
        local boolexpr be = Condition(c)
        local integer id = GetHandleId(be)
        call TriggerRemoveCondition(.trig, LoadTriggerConditionHandle(Event.TABLE, this, id))
        call RemoveSavedHandle(.TABLE, this, id)
        call DestroyBoolExpr(be)
        set be = null
    endmethod
   
    public method run takes nothing returns boolean
        return TriggerEvaluate(.trig)
    endmethod
   
endstruct

JASS:
struct Unit
   
    readonly static Event onDeath
    readonly static thistype onDeath_dyingUnit
    readonly static thistype onDeath_killingUnit
   
    readonly boolean isDead
    private unit ref
   
    private method kill takes Unit source returns boolean
        if not .isDead then
            set .onDeath_dyingUnit = this
            set .onDeath_killinUnit = source
            call .onDeath.run()
            set .onDeath_dyingUnit = 0
            set .onDeath_killinUnit = 0
           
            set .isDead = true
            call UnitDamageTarget(source.ref, .ref, 99999, false, false, null, null, null)
            call SetWidgetLife(.ref, 0)
            return true
        endif
        return false
    endmethod
   
    private static method onInit takes nothing returns nothing
        set .onDeath = Event.create()
    endmethod
   
endstruct

You could also do it like this:
JASS:
    private method kill takes Unit source returns boolean
        local boolean b
        if not .isDead then
            set .onDeath_dyingUnit = this
            set .onDeath_killinUnit = source
            set b = .onDeath.run()
            set .onDeath_dyingUnit = 0
            set .onDeath_killinUnit = 0
           
            if b then
                set .isDead = true
                call UnitDamageTarget(source.ref, .ref, 99999, false, false, null, null, null)
                call SetWidgetLife(.ref, 0)
            endif
            return b
        endif
        return false
    endmethod
 

Deleted member 219079

D

Deleted member 219079

Very nice unregister, did you come up with it yourself?
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
@jondrean
I actually didn't... I think.

@Aniki
JASS:
struct EventTest
   
    private static Event event
   
    private static method onEventRun takes nothing returns boolean
        call BJDebugMsg("Ran event!")
        return false
    endmethod
   
    private static method onInit takes nothing returns nothing
        set .event = Event.create()
        call .event.register(function thistype.onEventRun)
        call .event.run()
    endmethod
   
endstruct

Seems to work fine.
I must say that it is not entirely the same script that I use, because I also use a second version of it using an integer parameter, causing different triggers to be created/destroyed/executed/registered to/unregistered to, depending on that parameter.
However, it does seem to work fine even with destroying the boolean expression.
 
Level 13
Joined
Nov 7, 2014
Messages
571
However, it does seem to work fine even with destroying the boolean expression.

@Wietlol
Try this:
JASS:
library Foo initializer init

function callback takes nothing returns boolean
    call BJDebugMsg("callback called")
    return false
endfunction

globals
    trigger trg = CreateTrigger()
endglobals

function trigger_evaluate takes nothing returns nothing
    call TriggerEvaluate(trg)
endfunction

private function init takes nothing returns nothing
    local boolexpr be = Condition(function callback)
    call TriggerAddCondition(trg, be)
    call DestroyBoolExpr(be)
    set be = null

    //call TriggerEvaluate(trg) // callback called
    //call TimerStart(CreateTimer(), 0.0, false, function trigger_evaluate) // callback called
    call TimerStart(CreateTimer(), 0.03125, false, function trigger_evaluate) // callback NOT called
endfunction

endlibrary
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Ye, that is true.
It seems that you dont need to destroy it as Condition(c) == Condition(c) which means that destroying either will be fine.
So, you can just remove the DestroyBoolExpr() from the register.

This time, I took some time to think it through and I think I should also use conditionfunc instead of boolexpr.
JASS:
struct Event
  
    readonly static constant hashtable TABLE = InitHashtable()
  
    private trigger trig
  
    public static method create takes nothing returns thistype
        local thistype this = .allocate()
        set .trig = CreateTrigger()
        return this
    endmethod
  
    public method destroy takes nothing returns nothing
        call FlushChildHashtable(.TABLE, this)
        call DestroyTrigger(.trig)
        set .trig = null
        call .deallocate()
    endmethod
  
    public method register takes code c returns nothing
        local conditionfunc cf = Condition(c)
        call SaveTriggerConditionHandle(.TABLE, this, GetHandleId(cf), TriggerAddCondition(.trig, cf))
        set cf = null
    endmethod
  
    public method unregister takes code c returns nothing
        local conditionfunc cf = Condition(c)
        local integer id = GetHandleId(cf)
        call TriggerRemoveCondition(.trig, LoadTriggerConditionHandle(.TABLE, this, id))
        call RemoveSavedHandle(.TABLE, this, id)
        call DestroyCondition(cf)
        set cf = null
    endmethod
  
    public method run takes nothing returns boolean
        return TriggerEvaluate(.trig)
    endmethod
  
endstruct
 
Level 19
Joined
Dec 12, 2010
Messages
2,069
Direct calls are fastest thing eva - less than 0 mcs, since all of them are cahced at the game loading
JASS:
call XXX()
Then goes wrapper like ForForce of one player, or TriggerAddCondition() and EvaluateTrigger() combination
here game have to lookup for passed code functions
This takes about 100x more time than call

and finally ExecuteFunc, which takes 200x more time than call.

normally you won't bother with this at all, but just in case if you have more than 1000 calls simultaneosly, you may need that
 

Deleted member 219079

D

Deleted member 219079

Have you investigated the impact of parameters?
 

Deleted member 219079

D

Deleted member 219079

I'm sure people would benefit from it. Just saying, if you happen to have the tools :)
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Ofc a direct call is better... but you try to make an event library supporting listeners with direct calls without the user having to edit the event library.

O wait... I know, you cant.
With inverse textmacros, you would get really close, but those dont really exist.

In the end, this is simply a TriggerEvaluate().
Only registering and unregistering (which should be done on map init or during a lifecycle of something special) are the ones that really do something else.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,202
One can use trigger arrays, binding the code as an action or condition to the trigger and running that.
One can use string arrays and execute the function via function name (warning, optimizers may break this).
One can also bind to boolexpr or whatever.

Trigger approach is fastest. String approach is more light weight, readable and debuggable but slower. Boolexpr has additional flexibilities such as recombining for faster execution but also has additional requirements to the function declarations.
 
Level 39
Joined
Feb 27, 2007
Messages
5,023
One can use trigger arrays, binding the code as an action or condition to the trigger and running that.
One can use string arrays and execute the function via function name (warning, optimizers may break this).
One can also bind to boolexpr or whatever.
  1. If I TriggerExecute() will that spawn a new thread? The problem I see is doing something like this which may prematurely remove data necessary for the onEvent:
    JASS:
    struct s
        trigger onEventTrigger
        unit u
    
        method EventWhatever takes nothing returns nothing
            call TriggerExecute(this.onEventTrigger)
            call this.destroy()
            //Does the unit get killed?
            //Always? Only if the code is fast enough?
        endmethod
    endstruct
    
    function CustomOnEvent takes nothing returns nothing
        local s inst = HoweverIGetIt()
        call KillUnit(inst.u)
    enfunction
  2. This is definitely the easiest option but fails to replicate code inputs to wc3-style trigger functions. (TriggerRegisterAnyUnitEventBJ(), etc.)
  3. This would work perfectly except every user function has to return false, aye?
If the first works, great, but I'd settle for 3 if necessary. Speed of execution is not an issue-- these custom events will be happening only once every few seconds for a short time.
 
I haven't read through the whole thread, but yes #1 should work.

It would look like:
temp.png

Even though TriggerExecute runs things on a new "thread", it is scheduled right away. After the trigger-action fully completes, it goes back to run call this.destroy().

The only issue to worry about is recursion. Let's say you have a situation like this:
JASS:
struct Example
    private static trigger eventTrigger = CreateTrigger()

    static integer data = 0

    static method fireEvent takes nothing returns nothing
        set data = data + 1
        call TriggerExecute(eventTrigger)
    endmethod

    static method registerAction takes code c returns nothing
        call TriggerAddAction(eventTrigger, c)
    endfunction
endstruct

// ... 

function Callback takes nothing returns nothing
    call BJDebugMsg("data: " + I2S(Example.data))
    if Example.data == 0 then
        call Example.fireEvent()
    endif
    call BJDebugMsg("data: " + I2S(Example.data))
endfunction

// start here
function Init takes nothing returns nothing
    call Example.registerAction(function Callback)
    call Example.fireEvent()
endfunction

This code looks pretty ugly and is contrived, but Example.data shows the problem. If you fire the event within a callback (recursively), then your instance "data" could've changed after it was called. So in this case, it would print:
"data: 0" (from the fireEvent in Init)
"data: 1" (from the fireEvent in Callback)
"data: 1" (from the fireEvent in Callback)
"data: 1" (from the fireEvent in Init)

Ideally, we would want it to print:
"data: 0"
"data: 1"
"data: 1"
"data: 0"

Basically, "data" should go back to its old value after the .fireEvent() is called in Callback. There are a couple of ways to approach this problem. All of them are pretty easy:
  1. Just use locals to store "Example.data" at the top of your function callbacks. That way you won't really have to worry about "Example.data" changing, since you'll always keep your own local value.
  2. Maintain some sort of stack.
  3. There is a nice little trick that Nestharus used to get around this. Just use a temporary variable when you fire the trigger:
    JASS:
    static method fireEvent takes nothing returns nothing
        local integer oldValue = data
        set data = data + 1
        call TriggerExecute(eventTrigger)
        set data = oldValue
    endmethod
    It is a bit of a pain to go through why this works, but if you follow the code through, it'll store "0" as the oldValue, set the data to 1, and when that thread finishes it'll restore the data value to "0".

You don't really have to worry about this that much. But it is a tiny change that will prevent some despair later if you accidentally run into that problem. If you want to store multiple variables for the trigger to access, you would do the same thing. Just use one more temp variable for each variable.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Why not use function interfaces?
JASS:
function interface Code takes nothing returns nothing

struct S

    static Code array codes

    static method test takes nothing returns nothing
        local integer i = 0
        set codes[1] = Code.A
        set codes[2] = Code.B
        set codes[3] = Code.C
        loop
            set i = i + 1
            call codes[i].evaluate()
            exitwhen i == 3
        endloop
    endmethod

endstruct

Basically its just like using TriggerEvaluate() but requires less amount of work from the user cause you don't have to manually manage the triggers yourself.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
1. What does Code.A, Code.B and Code.C mean?

In the end, because you do codes.evaluate(), you still happen to run multiple triggers for one event.
Then my version, where the system deals with all the trigger stuff, is still more efficient.
And, It is probably easier to use.
 

Deleted member 219079

D

Deleted member 219079

Ye, Wietlol posted a nice solution and I think it should be submitted to JASS section so people won't post these threads again.
 
Level 13
Joined
Nov 7, 2014
Messages
571
Status
Not open for further replies.
Top