• 🏆 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] - Sync Local Booleans

Status
Not open for further replies.
Level 6
Joined
Jan 17, 2010
Messages
149
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:
JASS:
// 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
JASS:
// bad
if GetLocalPlayer() == Player(0) then
   // desync
   call SyncBoolean(Player(0), b, function ...)
endif

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

JASS:
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:
Level 6
Joined
Jan 17, 2010
Messages
149
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:
JASS:
call SyncBoolean(p,b,function asdf)



Tertiary purpose is just to make some easy library to demonstrate [vJASS] - Detect Reforged
 
Level 6
Joined
Jan 17, 2010
Messages
149
I'll try to rewrite [vJASS] - Detect Reforged to use ESC cinematic trigger to remove dependency.

creating a unit to synchronize selections.

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.
 
Level 6
Joined
Jan 17, 2010
Messages
149
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.

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:
Level 6
Joined
Jan 17, 2010
Messages
149
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.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
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.
 
Level 6
Joined
Jan 17, 2010
Messages
149
sync string

[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)
 
Level 6
Joined
Jan 17, 2010
Messages
149
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.

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:
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:
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.
 
Level 6
Joined
Jan 17, 2010
Messages
149
I need to tie a callback function to the sync event.

For example here are 2 different sync events
Code:
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:

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
[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)

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.
 
Status
Not open for further replies.
Top