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

SyncInteger/SyncString (Codeless Save/Load System)

Status
Not open for further replies.
Uploaded to resource section

JASS Submission Thread


Tested in battle.net with 2 players (high ping) and 32 integers were sent in under a second (thanks to Solu9 for testing)


I wanted to post here before submitting to the spells section. At the core of this there are two libraries, SyncInteger and SyncString. They use selection events to sync values to all players in the map. This has been attempted before with game cache or the trigger sync natives but they were reported to be too slow. I haven't looked at their implementations but my script isn't using any of those natives.

This is useful for many different reasons but the first thing that came to mind was a code-less save / load system. This would offer the ability to carry progress or data between maps (in multiplayer) without having to type in a long code.

Anyway, here are the core libraries used to actually sync the data.

(Code is a WIP)


JASS:
library SyncInteger initializer Init // requires SyncString

        globals
            private constant player DUMMY_PLAYER = Player(PLAYER_NEUTRAL_PASSIVE)
            
            private trigger T=CreateTrigger()
            private integer LastSynced=0
            private player LastPlayer
            
            unit array SyncIntegerDummy
        endglobals
        
        function GetSyncedInteger takes nothing returns integer
            return LastSynced
        endfunction
        
        function GetSyncedPlayer takes nothing returns player
            return LastPlayer
        endfunction
        
        function OnSyncInteger takes code func returns nothing
            call TriggerAddCondition(T, Filter(func))
        endfunction
        
        function SyncInteger takes integer playerId, integer number returns boolean
            local group g
            local unit u
            
            if (number > SYNC_MAX_NUM) then
                return false
            endif
            
            set g = CreateGroup()
            call GroupEnumUnitsSelected(g, Player(playerId), null)
            set u = FirstOfGroup(g)
            call DestroyGroup(g)
            set g = null
            
            if (GetLocalPlayer() == Player(playerId)) then
                call ClearSelection()
                call SelectUnit(SyncIntegerDummy[number], true)
                call ClearSelection()
                call SelectUnit(u, true)
            endif
            
            set u = null
            
            return true
        endfunction
        
        private function OnSelect takes nothing returns boolean
            local unit u = GetTriggerUnit()
            local player p = GetTriggerPlayer()
            local integer id = GetPlayerId(p)
            
            if (SyncIntegerDummy[GetUnitUserData(u)] == u) then
                set LastSynced=GetUnitUserData(u)
                set LastPlayer=p
                call TriggerEvaluate(T)
            endif
            
            set u = null
            
            return false
        endfunction
        
        private function CreateDummies takes nothing returns nothing
            local integer i = 0
            
            static if (LIBRARY_UnitDex) then
                set UnitDex.Enabled = false
            endif
            
            loop
                exitwhen i > SYNC_MAX_NUM
                set SyncIntegerDummy[i]=CreateUnit(DUMMY_PLAYER, XE_DUMMY_UNITID, 0, 0, i)
                call SetUnitUserData(SyncIntegerDummy[i], i)
                set i = i + 1
            endloop
            
            static if (LIBRARY_UnitDex) then
                set UnitDex.Enabled = true
            endif
        endfunction
    
        //===========================================================================
        private function Init takes nothing returns nothing
            local trigger t = CreateTrigger()
            local integer i = 0
            
            loop
            
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SELECTED, null)
                
                set i = i + 1
                exitwhen i==bj_MAX_PLAYER_SLOTS
            endloop

            call TriggerAddCondition(t, Filter(function OnSelect))
            
            call TimerStart(CreateTimer(), 0, false, function CreateDummies)
        endfunction

endlibrary

JASS:
library SyncString initializer Init requires SyncInteger

    globals
        private constant string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 abcdefghijklmnopqrstuvwxyz.,!?@#$%^&*()-=<>/\\[]{};:'\"`"
        
        constant integer MAX_STRING_LEN = 32
        constant string SYNC_TERM_CHAR = "`"
        constant integer SYNC_MAX_NUM = StringLength(ALPHABET)
            
        private string array SyncedStr
        private boolean array Syncing
        private timer array SyncTimer
        private integer array SyncCounter
        
        private string Last=""
        
        private trigger E=CreateTrigger()
    endglobals
    
    function IsSyncingString takes player p returns boolean
        return Syncing[GetPlayerId(p)]
    endfunction
    
    function GetSyncedString takes nothing returns string
        return Last
    endfunction
    
    function OnSyncString takes code func returns nothing
        call TriggerAddCondition(E, Filter(func))
    endfunction
    
    function I2Char takes integer i returns string
        return SubString(ALPHABET, i, i + 1)
    endfunction
    
    function Char2I takes string c returns integer
        local integer i = 0
        local string s

        loop
            set s = I2Char(i)
            exitwhen i > SYNC_MAX_NUM
            if (s == c) then
                return i
            endif
            set i = i + 1
        endloop
        return 0
    endfunction
    
    private function OnSyncAction takes nothing returns nothing
        local integer pid = GetPlayerId(GetSyncedPlayer())
        local string char = I2Char(GetSyncedInteger())

        set SyncCounter[pid]=SyncCounter[pid]+1
        if (char == SYNC_TERM_CHAR) then
            if (SyncCounter[pid] >= MAX_STRING_LEN) then
                set Last=SyncedStr[pid]
                set Syncing[pid] = false
                set SyncedStr[pid] = ""
                set SyncCounter[pid]=0
                call TriggerEvaluate(E)
            endif
        else
            set SyncedStr[pid] = SyncedStr[pid] + I2Char(GetSyncedInteger())
        endif
    endfunction
    
    function SyncChar takes integer playerId, string c returns boolean
        return SyncInteger(playerId, Char2I(c))
    endfunction
    
    function SyncString takes integer pid, string s, integer len returns boolean
        local integer i = 0
        
        if (Syncing[pid]) then
            return false
        endif
        
        set Syncing[pid] = true

        loop
            exitwhen i > len
            
            call SyncChar(pid, SubString(s, i, i + 1))
            
            set i = i + 1
        endloop
        
        call SyncChar(pid, SYNC_TERM_CHAR)
        
        return true
    endfunction
    
    private function Init takes nothing returns nothing
        local integer i = 0
        
        loop
            exitwhen i > 15
            set SyncedStr[i]=""
            set Syncing[i]=false
            set SyncCounter[i]=0
            set i = i + 1
        endloop
        
        call OnSyncInteger(function OnSyncAction)
    endfunction
    
endlibrary


I tested these libraries using 4 different computers, two of which were on a completely different non-local connection (4G network). All of them had a ping of about 200, yet the data was synced basically instantly. I tested this on W3Arena (simulated battle.net) because I couldn't test four computers simultaneously on battle.net, so if someone could help confirm the speeds on B.NET it would be appreciated.

Here's a simple example:

JASS:
scope Example initializer Init
    
    globals
        private boolean array IsSyncExample
    endglobals
    
    private function OnRecieveString takes nothing returns boolean
        local player p = GetSyncedPlayer()
        local string s = GetSyncedString()
        local integer pid = GetPlayerId(p)
        
        if (not IsSyncExample[pid]) then
            return false
        endif
        
        set IsSyncExample[pid] = false
        
        call BJDebugMsg("Recieved \"" + s + "\" from " + GetPlayerName(p))
        
        return false
    endfunction
    
    private function OnGameStart takes nothing returns nothing
        local player from = Player(0)
        local integer pid = GetPlayerId(from)
        
        local string s = ""
        
        if (GetLocalPlayer() == from) then
            set s = "Hello"
        endif
        
        set IsSyncExample[pid] = true
        
        call SyncString(pid, PopulateString(s), MAX_STRING_LEN) // must define a length
    endfunction
    
    private function Init takes nothing returns nothing
        call TimerStart(CreateTimer(), 0, false, function OnGameStart)
        call OnSyncString(function OnRecieveString)
    endfunction

endscope

I also stumbled upon another way to sync integers.

http://www.hiveworkshop.com/forums/pastebin.php?id=36k0u8

Attached below is an example of a codeless save/load system. It's technically not codeless, you just don't have to type anything. I will hopefully upload a much more advanced example to the spells section.
 
Last edited:
Have you tested the ForceUIKey?

I thought that was faster than unit selection

I have tested theForceUIKeynative (code here) and even if it were faster I think the speed would be negligible. The only way to know for sure is to test with a 10-12 player map on B.NET.

That method has some issues though.
  1. The player can press the ESC key to abuse or break the data being sent
  2. The number that is being synced is equal to the number of requests being synced. So if you sync the number 50 that's 50 "requests"
  3. The only way I'm aware of to know when the request is over, is to use a timer without a static timeout.

The current implementation with selections is proving to be more than fast enough, but I want to test with more people.

I'm working on a proper demo map for the systems section.
 
I'm gonna give this a try later, maybe. Seems interesting; clever approach if that truly works.



EDIT: Tried it out now, but can't get it to work. I used only the integer part to sync a boolean (so my integer is either 0 or 1), but no matter what, there is no syncing happening. None of the debug messages will be displayed.


JASS:
function SyncExtensionEnable takes nothing returns boolean
    local integer pid = GetPlayerId(GetSyncedPlayer())
    if GetSyncedInteger() == 1 then
        set HAS_EXTENSION_PACK[pid] = true
        call BJDebugMsg("synced: true")
    else
        set HAS_EXTENSION_PACK[pid] = false
        call BJDebugMsg("synced: false")
    endif
    return false
endfunction

function Trig_SyncExtensionPackage_Actions takes nothing returns nothing
    local integer var = 0
    local integer i = 0

    loop
        exitwhen i > 5
        if GetLocalPlayer() == Player(i) and GetSoundFileDuration("GaiasRetaliation\\Voice\\Intro\\01.wav") > 0 then
            set var = 1
        endif
        set HAS_EXTENSION_PACK[i] = false
        if GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(Player(i)) == MAP_CONTROL_USER then
            call SyncInteger(i, var)
        endif
        set i = i + 1
    endloop
endfunction


//===========================================================================
function InitTrig_SyncExtensionPackage takes nothing returns nothing
    set gg_trg_SyncExtensionPackage = CreateTrigger(  )
    call TriggerRegisterTimerEventSingle( gg_trg_SyncExtensionPackage, 0.2 )
    call TriggerAddAction( gg_trg_SyncExtensionPackage, function Trig_SyncExtensionPackage_Actions )
    call OnSyncInteger(function SyncExtensionEnable)
endfunction
 
Last edited:
Uploading a better demo map shortly :thumbs_up:

The system performance has been really good so far, but I haven't tested with more than 5 players. I still expect it to perform well even at max players.
I edited my post above. Please read it; it's not working for me and I don't understand why.
 
I edited my post above. Please read it; it's not working for me and I don't understand why.

I removedGetSoundFileDuration("GaiasRetaliation\\Voice\\Intro\\01.wav") > 0and it worked for me.

Also make sure you set the correct dummy ID in the sync int library.

I'm going to be posting these libraries in the JASS section as well (they will be more polished).
 
I removedGetSoundFileDuration("GaiasRetaliation\\Voice\\Intro\\01.wav") > 0and it worked for me.
This native just checks if a file exists or not. No matter what, it should show something regardless.

Also make sure you set the correct dummy ID in the sync int library.
I don't use xedummy, so I simply replaced the rawcode with a rawcode pointing to a unit using the dummy.mdx model.
What exactly are the requirements for this to work? Is the unit allowed to have locust? Must the unit be visible to the player?

I'm going to be posting these libraries in the JASS section as well (they will be more polished).
That would be nice, because they are still a mess right now, like missing dependencies and globals declared in the wrong library.


EDIT: Okay found the culprit. Changed the dummy unit to 'hfoo' and it works now.
 
I just tested and locust breaks the system.

Use "Ghost (Visible)" instead, or copy the xe dummy unit over.

I will add some debug messages for that and include it in the documentation.
There should also be a function to disable the whole system, because right now it spams selection events even when the user doesn't want to sync anything. Also you should remove the dummies when they are not used.
 
There should also be a function to disable the whole system, because right now it spams selection events even when the user doesn't want to sync anything.

I will add an enabler flag (and possibly aDisableTriggerwrapper), but the system checks if the selected unit is the specific dummy related to their ID before doing anything. You could also add a check to all your current selection events to make sure the unit being selected isn't a dummy.
 
I will add an enabler flag (and possibly aDisableTriggerwrapper), but the system checks if the selected unit is the specific dummy related to their ID before doing anything. You could also add a check to all your current selection events to make sure the unit being selected isn't a dummy.
It doesn't matter if the selection event has a condition.
You still have 16 selection events registered by the system; these will always fire whenever a player selects any unit, regardless if there is syncing going on or not.
This adds pointless overhead imho.
Also, since you create 1 dummy per character in the word length, this system also creates a huge number of useless dummies that can even be accidentally selected by the players since they can not have locust.

It's a good idea to at least hide these units if no syncing is going on. But to be honest, I'd rather remove them.

I don't think that there are many use-cases in which users want to constantly sync data. Mostly they want to do that only in very specific situations, like loading or detecting if a local file exists, etc.


Here's what I did:
JASS:
         function SyncTerminate takes nothing returns nothing
            //this cleans up all dummies and triggers created by the system to reduce overhead
            local integer i = 0
            call DestroyTrigger(OnSelectTrigger)
            static if (LIBRARY_UnitDex) then
                set UnitDex.Enabled = false
            endif
            loop
                exitwhen i > SYNC_MAX_NUM
                call RemoveUnit(SyncIntegerDummy[i])
                set SyncIntegerDummy[i] = null
                set i = i + 1
            endloop
            static if (LIBRARY_UnitDex) then
                set UnitDex.Enabled = true
            endif
         endfunction

I can basicly call this function to destroy the trigger and all the dummies generated by the sync once I have everything synced that I needed. No reason to carry performance overhead around.
 
It doesn't matter if the selection event has a condition.
You still have 16 selection events registered by the system; these will always fire whenever a player selects any unit, regardless if there is syncing going on or not.
This adds pointless overhead imho.

Sure which is why I suggestedDisableTrigger:thumbs_up:

They shouldn't fire then, right?

Also, since you create 1 dummy per character in the word length, this system also creates a huge number of useless dummies that can even be accidentally selected by the players since they can not have locust.

You can easily reduce the number of characters to something like 16 and the system will still function, I just wanted to have smaller codes stored on disk.

Supposedly pausing units reduces the amount of resources they're using (which I do in my current implementation)

I also create them at the edge of the map. I will have to do some more tests about preventing them from being manually selected but I think that should do.

It's a good idea to at least hide these units if no syncing is going on. But to be honest, I'd rather remove them.

I don't think that there are many use-cases in which users want to constantly sync data. Mostly they want to do that only in very specific situations, like loading or detecting if a local file exists, etc.

Eh, in my demo I needed the syncing to be available whenever and I figured re-creating the dummy units was more costly than just pausing them.

Here's what I did:

I can basicly call this function to destroy the trigger and all the dummies generated by the sync once I have everything synced that I needed. No reason to carry performance overhead around.

I suppose I should add that option since I know some people will only be using this at the start of their map.
 
Thanks for making a formal library on it! It looks pretty good.

How does this interact with your current selection? I noticed it does ClearSelection(), but it only reselects the first of group--not all the units you had selected, right?

In my current implementation (not in first post) I require the user to manually handle re-selection after calling SyncInteger.

Calling SyncInteger will clear selection.

EDIT: http://www.hiveworkshop.com/forums/submissions-414/syncinteger-syncstring-278674/#post2820180
 
Last edited:
Status
Not open for further replies.
Top