Dismiss Notice
60,000 passwords have been reset on July 8, 2019. If you cannot login, read this.

[vJASS] - Sync Local Booleans

Discussion in 'The Lab' started by Blarto, Apr 12, 2020.

  1. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    Posting this here in case its useful for someone.

    A simple library to synchronize boolean values (heavily inspired-by/based-on TH's [vJASS] - SyncInteger & all the research done around this topic). I decided to code this because BlzTriggerRegisterPlayerSyncEvent/BlzSendSyncData/BlzGetTriggerSyncData were too cumbersome and syncinteger+sync too bloated for synchronizing tiny values. Also this works in all versions of of Wc3 so no dependency 1.31+.

    This library should only be used for tiny value synchronizations (flags, smallints, etc), for local values, for stuff such as config done at map initialization.

    Unlike TH's library, this one only works for booleans (although you can pack integers into boolean chains, its not recommended with this library), it does not create dummy units unless synchronization is requested, cleans after itself after usage is over, and is overall more compact (library is barebones because there's no need for string/int sync).

    In addition, the API provides an immediate callback for convenience:
    Code (vJASS):

    // for example
    function test takes nothing returns nothing
          if GetSyncedBoolean() == true then
               call BJDebugMsg(GetPlayerName(GetTriggerPlayer()) + ": yes")
          else
               call BJDebugMsg(GetPlayerName(GetTriggerPlayer()) + ": no")
          endif
    endmethod

    //...
    function bleh takes nothing returns nothing
        local boolean b = false
        if GetLocalPlayer() == Player(0) then
             set b = true
        endif
        call SyncBoolean(Player(0), b, function test)
    endfunction
     


    Warning: SyncBoolean IS NOT safe in GetLocalPlayer() blocks
    Code (vJASS):

    // bad
    if GetLocalPlayer() == Player(0) then
       // desync
       call SyncBoolean(Player(0), b, function ...)
    endif

    // Good
    call SyncBoolean(Player(0), b, function ...)
     


    Code (vJASS):

    library BooleanSync requires optional TimerUtils
        private struct BooleanSync extends array
            // must be a unit with vision (~100 sight range) so players/triggers can select it
            private static constant integer DUMMY_UID = 'nrat' // in this instance, its a wc3 rat (350 sight range)
            // area in the map that's out of the way for any play
            private static real DUMMY_X
            private static real DUMMY_Y
         
            // dummy unit handles
            private static unit dummy1
            private static unit dummy2
            private static unit dummy_helper
            // callback stack
            private static trigger array callbacks
            private static integer stack
            // utility handles
            private static integer deleteTick
            private static timer deleteTimer
            private static trigger selectTrigger
            private static group selectionGroup
            // event response
            readonly static boolean synced
            static hashtable callbacksHT
            static method Code2Trigger takes code c returns trigger
                local integer cfid
                if c == null then
                    return null
                endif
                set cfid = GetHandleId(Condition(c))
                if not HaveSavedHandle(callbacksHT, cfid, cfid) then
                    call SaveTriggerHandle(callbacksHT, cfid, cfid, CreateTrigger())
                    call TriggerAddCondition(LoadTriggerHandle(callbacksHT, cfid, cfid), Condition(c))
                endif
                return LoadTriggerHandle(callbacksHT, cfid, cfid)
            endmethod
         
            private static method CreateDummy takes nothing returns unit
                local unit u
                local integer i = 0
                // create dummy unit
                set u = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMY_UID, DUMMY_X, DUMMY_Y, 0)
                // Make dummy only selectable through triggers
                call UnitAddAbility(u, 'Amrf')
                call SetUnitFlyHeight(u, 5000, 0)
                call UnitAddAbility(u, 'Aeth')
                call SetUnitScale(u, 0, 0, 0)
                call PauseUnit(u, true)
                // Hide health bar
                call UnitAddAbility(u , 'Aloc')
                call ShowUnit(u, false)
                call UnitRemoveAbility(u , 'Aloc')
                call ShowUnit(u, true)
             
                // ensure it doesn't die
                call SetUnitInvulnerable(u, true)
                // share vision
                loop
                    call UnitShareVision(u, Player(i), true)
                    set i = i + 1
                    exitwhen i >= bj_MAX_PLAYER_SLOTS
                endloop
                set dummy_helper = u
                set u = null
                return dummy_helper
            endmethod
            private static method onSelect takes nothing returns boolean
                if GetTriggerUnit() == dummy1 or GetTriggerUnit() == dummy2 then
                    if GetTriggerUnit() == dummy1 then
                        set synced = true
                    else
                        set synced = false
                    endif
                    if callbacks[stack] != null then
                        call TriggerEvaluate(callbacks[stack])
                        set callbacks[stack] = null
                    endif
                    set stack = stack - 1
                    if stack < 0 then
                        // shield from array negative index crash
                        set stack = 0
                    endif
                endif
                return false
            endmethod
            private static method onInit takes nothing returns nothing
                local integer i = 0
                static if LIBRARY_TimerUtils then
                    set deleteTimer = null
                else
                    set deleteTimer = CreateTimer()
                endif
                set selectTrigger = CreateTrigger()
                loop
                    call TriggerRegisterPlayerUnitEvent(selectTrigger, Player(i), EVENT_PLAYER_UNIT_SELECTED, null)
                    set i = i + 1
                    exitwhen i >= bj_MAX_PLAYER_SLOTS
                endloop
                call TriggerAddCondition(selectTrigger, Filter(function thistype.onSelect))
                call DisableTrigger(selectTrigger)
                set DUMMY_X = GetRectMinX(GetWorldBounds()) + 1000
                set DUMMY_Y = GetRectMaxY(GetWorldBounds()) - 1000
                set dummy1 = null
                set dummy2 = null
                set synced = false
                set selectionGroup = CreateGroup()
                set stack = 0
                set deleteTick = 0
                set callbacksHT = InitHashtable()
            endmethod
         
            private static method deleteThis takes nothing returns nothing
                set deleteTick = deleteTick - 1  
                if deleteTick <= 0 then
                    static if LIBRARY_TimerUtils then
                        call ReleaseTimer(GetExpiredTimer())
                        set deleteTimer = null
                    endif
                    call DisableTrigger(selectTrigger)
                    if dummy1 != null then
                        call RemoveUnit(dummy1)
                        set dummy1 = null
                    endif
                    if dummy2 != null then
                        call RemoveUnit(dummy2)
                        set dummy2 = null
                    endif
                endif
            endmethod
            static method syncB takes player p, boolean b, code callback returns nothing
                local unit u
                local unit last
                local integer count
     
                if p == null then
                    set p = GetLocalPlayer()
                endif
                call EnableTrigger(selectTrigger)
                if dummy1 == null then
                    set dummy1 = CreateDummy()
                endif
                if dummy2 == null then
                    set dummy2 = CreateDummy()
                endif
                if GetLocalPlayer() == p then
                    call GroupEnumUnitsSelected(selectionGroup, p, null)
                    set count = 0
                    set u = FirstOfGroup(selectionGroup)
                    loop
                        exitwhen u == null
                        set last = u
                        call GroupRemoveUnit(selectionGroup, u)
                        set count = count + 1
                        set u = FirstOfGroup(selectionGroup)
                    endloop
                    if count >= 12 then
                        call SelectUnit(last, false)
                    endif
                    if b then
                        call SelectUnit(dummy1, true)
                        call SelectUnit(dummy1, false)
                    else
                        call SelectUnit(dummy2, true)
                        call SelectUnit(dummy2, false)
                    endif
                    if count >= 12 then
                        call SelectUnit(last, true)
                    endif
                endif
             
                set stack = stack + 1
                set callbacks[stack] = thistype.Code2Trigger(callback)
                if deleteTick <= 0 then
                    static if LIBRARY_TimerUtils then
                        if deleteTimer == null then
                            set deleteTimer = NewTimer()
                        endif
                        call TimerStart(deleteTimer, 1, true, function thistype.deleteThis)
                    else
                        call TimerStart(deleteTimer, 1, true, function thistype.deleteThis)
                    endif
                endif
                set deleteTick = 5
                set u = null
                set last = null
            endmethod
        endstruct
        function GetSyncedBoolean takes nothing returns boolean
            return BooleanSync.synced
        endfunction
        function SyncBoolean takes player p, boolean b, code callback returns nothing
            call BooleanSync.syncB(p, b, callback)
        endfunction
    endlibrary
     
     
    Last edited: Apr 13, 2020
  2. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    7,014
    Resources:
    12
    Models:
    5
    Maps:
    1
    Spells:
    1
    Tutorials:
    1
    JASS:
    4
    Resources:
    12
    I cant imagine that this is actually faster than the sync natives. Did you do some speed tests on this one?
     
  3. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    This is based on unit selection. Its slower.

    Main purpose is just back-compatibility API for old wc3 where new natives are not available. Also with new API you fiddle with triggers, A wrapper for that just like this one will be around 50-100 lines If i were to guess. This is around 200 lines.

    Secondary purpose is ease of use

    i.e a 1 liner instead of fiddling with triggers:
    Code (vJASS):

    call SyncBoolean(p,b,function asdf)
     




    Tertiary purpose is just to make some easy library to demonstrate [vJASS] - Detect Reforged
     
  4. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,793
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    You should be able to just register
    EVENT_PLAYER_END_CINEMATIC
    and call
    ForceUICancel
    , especially if you are only syncing during initialization.
     
  5. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    you mean GetLocalPlayer block into ForceUICancel?

    hmm neat idea. but it might interfere with other EVENT_PLAYER_END_CINEMATIC event triggers...

    Would remove dependency for [vJASS] - Detect Reforged though. I'll rewrite that one eventually.
     
    Last edited: Apr 13, 2020
  6. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,793
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    I think the overhead from disabling cinematic triggers is much lower than creating a unit to synchronize selections.
     
  7. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    I'll try to rewrite [vJASS] - Detect Reforged to use ESC cinematic trigger to remove dependency.

    I made sure units are created for 5 seconds before being destroyed (and repeated calls reset the 5 sec timer). so overhead is ammortized to be relatively small. I just didnt think of ESC cinematic trigger at that time.

    Edit: on second thought having to go and disable a dozen or so ESC triggers might also be pretty potato in terms of actually maintaining that. At-least with units you know its 2 units every 5 seconds at worst case scenario.
     
  8. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    7,014
    Resources:
    12
    Models:
    5
    Maps:
    1
    Spells:
    1
    Tutorials:
    1
    JASS:
    4
    Resources:
    12
    I do get the backwards compatibility stuff...
    But shouldnt we as the community finally move on and let go of the past?
    We cant stick to 1.26 forever.
     
    Last edited: Apr 13, 2020
  9. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    That's a topic deserving its own discussion thread.

    But here are the facts

    Netease (China) is 1.26-1.28 (rarely 1.31 + sometimes its own special derivatives of new wc3 verison)
    Gameranger (Europe) is 1.26
    M16 (Korea) is 1.28?
    Bnet2 (North America, Europe, Asia is mostly dead) is 1.32+

    And Bnet community is really tiny in comparison to the other 3 combined.

    Asking people to switch to 1.32 is asking them to download 30+GB and untested software + all the jank that comes with it (worse performance, new bugs, etc). And sometimes it might not be an option. In any case it appears the community hasn't move on as a whole (maybe we have but not other people). Oh and blizzard has killed all attempts to make private servers off new versions for a while now...

    What wc3 ver you use depends on who your players are and who is your target audeince. But even if you just make map for yourself then I'd argue you don't need any new feature past 1.28 (aside from graphics).

    Hell if you want to play with new features, Netease DZ API supports lua (and is back/forward compatible with bnet2) so you can make your maps for netease instead of bnet2 and probably have more success doing so.
     
    Last edited: Apr 13, 2020
  10. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    7,014
    Resources:
    12
    Models:
    5
    Maps:
    1
    Spells:
    1
    Tutorials:
    1
    JASS:
    4
    Resources:
    12
    The problem is: if not even mappers switch to Reforged, nobody will. And the game will die as a consequence since its not longer possible to get legally.
     
  11. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    Mappers wont switch to reforged because reforged has less players than the other alternatives, thus no real advantage to develop exclusively for it (Lua, UI, natives are nice but are not game changers from the perspective of the whole ecosystem).
    Players wont switch to reforged because Activision pulled a Bethesda and removed a slew of features for one stupid reason or another, and expected Players to make their own content.
    Activision won't fix reforged because there's no money in it, and possibly because of their Business policy preventing them reverting some destructive changes (wc3 is so tiny in their portfolio there are no reason to make exceptions either).

    This is why essentially responsibility is on Mappers to revive it. This standoff will be broken by either all the alternative platforms/players embracing 1.31+ or Blizzard fixing their shit.

    We know none of that will happen though, certainly not in an orderly and timely manner.

    And yeah....
    This is why all of my jass stuff +maps are backwards compatible. I simply don't know which way the wind will blow so I have to stick to what has the highest chance to be universally workable across the entire ecosystem. Sucks I can't play with new features but that's life.

    Chinese law or European law or American law or Russian law or Korean law? I don't think the law is relevant to these discussions until someone actually gets fined somewhere for playing/running/distributing a bootleg wc3 server.
     
  12. LeP

    LeP

    Joined:
    Feb 13, 2008
    Messages:
    478
    Resources:
    0
    Resources:
    0
    Personally i'm also interested in a syncing library which can work on many different patches for jhcr.
    I long had the idea to allow hot code reload in multiplayer games where i would need a good very-close-to-pure-jass library. I recently enabled to target different patches in jhcr since i personally dont want to install a 30+gb patch which forces me to use bnet app.

    I would like a library which has potentially different ways to sync strings (or integers) on different patches with the same API. Personally i would even encourage to use those new natives for newer patches. For my specific case of jhcr i would use the c preprocessor to switch the different implemetations but for vjass one could imagine using textmacros or static ifs.
     
  13. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    [vJASS] - Sync (Game Cache) uses gamecache to Sync strings but I could never get it to work

    So I ended up rewriting [vJASS] - SyncInteger with different sync modes SyncBoolean, SyncInteger, SyncLocation, and SyncZ, and SyncSignal, all of which are optimal and reuse the same dummies (i'll post once its done & tested in live games). This Will deprecate/improve SyncBoolean once I'm done.

    IMO strings are a lost cause, but not entirely. To Sync tiny common strings using SelectUnit (not that I would recommend it), you need 64 dummies and encode everything to base 64. String length will be number of selections (limit of selections is about 300 before wc3 will begin throttling traffic). This will be sufficient for save/load stuff. 64 dummies is a lot but those are made to be destroyed once syncing session is over then probably good.

    SelectUnit is universal and probably superior to gamecache in terms of consistency across all versions of wc3, but in this ([vJASS] - SyncInteger) gamecache was shown to be x3-x4 times faster and having x4 capacity. I suspect it is because the warcraft 3 netcode guarantees SelectUnit event to be consistent across all players, and resolve in the exact same order it was issued via jass (a fact which i'm exploiting in my sync library)
     
  14. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,793
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    Sync (Game Cache) works fine on old and the latest patches. I designed it 4 years ago and some maps still use it on Battle.net to this day. There really is no reason to sync with pure selections anymore. The game cache sync library only uses a selection or two at the end when data has finished syncing.
     
  15. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    The problem is that the gamecache approach requires selection sync to function. This causes the code to bloat to 1000+ lines and new complexity is introduced (select units + game cache) + more complex API. Of-course the benefit is increased speed etc. So I believe your solution is good for the heavy duty things, but since it comes with selection API anyway, most people's use cases will function with that SelectUnit just fine without ever needing to use gamecache.

    Personally I don't trust gamecache or any sync implementation using it. Possibly because I couldn't get it to work, which is likely because I didn't understand how to use it properly.

    This lead me to the position that Ease-of-use API > speed or whatever else gamecache library offers in my case. The cost is network performance, which I'm willing to pay.

    Which is why i designed this to be like a classic async callback structure
    Code (Text):

    call SyncInteger(player, int, function callback)
    call SyncBoolean(player, bool, function callback)
    call SyncLocation(player, x,y, function callback)
     
    I tried to adapt your library and implementing this API style but I failed. And I have no time currently to figure out why. So selection based sync it is for now.
     
    Last edited: Apr 14, 2020
  16. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,793
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    If you really wanted a lightweight version with speed you could implement game cache syncing (without strings) and simply select 1 unit at the end when a player has finished syncing (or use ForceUICancel, depending on your use-case).

    However even if my library adds a 1000 lines it doesn't negatively affect the user or developer. The script file size increase is negligible and you will still have the code in 1 trigger regardless of size.
     
  17. Blarto

    Blarto

    Joined:
    Jan 17, 2010
    Messages:
    150
    Resources:
    1
    Spells:
    1
    Resources:
    1
    I need to tie a callback function to the sync event.

    For example here are 2 different sync events
    Code (Text):

    SyncInteger(p1, 1, callback1)
    SyncInteger(p1, 2, callback2)
     
    For cache based solution, I'd have to push the callback handleids to sync with the value I'm syncing (so that their triggers can be retrieved on successful sync and evaluated). this doubles the load per sync event, making it comparable to SelectUnit sync + a polling timer on each and every cell of the game cache. On bigger sync loads (~+ 4 values) this load becomes negligible, which lead me to attempt to do this with your game cache library. For this to happen I think I have to create 2 instances (of your Sync Struct) for 1 player and hook 1 callback to each, + do some extra lines of code to flush table, and make this work smoothly (not to mention a select unit event at the end). At some point I failed in the worst way possible (it works but then fails after some time), then I decided to just use selection sync instead.

    About code bloat: I mentioned code bloat because your gamecache api claims to replace selection sync but still has dependency on it, which means the developer automatically gets selection based sync as soon as they import your library, and in that case they might just use selection based sync without gamecache if their use cases are small enough, which makes the gamecahce library unnecessary bloat. I'd rewrite it to remove dependency on it completely and also support aforementioned callback api but I have no time to figure out what's going wrong. I don't think rewriting gamecache based sync it is worthwhile, as for 1.31+ we have new Blz Sync natives which are presumably faster, and I'm pretty sure for oldcraft3 the gamecache vs selection argument is moot in 99.99% of cases.
     
    Last edited: Apr 14, 2020
  18. LeP

    LeP

    Joined:
    Feb 13, 2008
    Messages:
    478
    Resources:
    0
    Resources:
    0
    Yeah i'm (somewhat) aware of those libraries. They're quite big so i haven't had the motivation to port them to the jhcr style. Also while they're ofcourse compatible with all versions i wouldn't mind using new natives if i target a new patch. And then i have to see if i can keep using strings or if i had to convert to some integer scheme.
    But i guess my struggles are not quite on topic; just wanted to vent a bit.