- Joined
- Jun 23, 2007
- Messages
- 4,066
Documentation
Uses:
Demo Map: Codeless Save and Load (Multiplayer)
Core System
Uses:
- SyncInteger (Required)
- PlayerUtils (Optional)
Demo Map: Codeless Save and Load (Multiplayer)
Core System
JASS:
library Sync requires SyncInteger, optional PlayerUtils
/***************************************************************
*
* v1.3.0, by TriggerHappy
* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*
* This library allows you to quickly synchronize async data such as
* the contents of a local file to all players in the map by using the game cache.
*
* Full Documentation: -http://www.hiveworkshop.com/forums/pastebin.php?id=p4f84s
*
* _________________________________________________________________________
* 1. Installation
* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
* Copy the script to your map and save it (requires JassHelper *or* JNGP)
*
* SyncInteger: https://www.hiveworkshop.com/threads/syncinteger.278674/
* PlayerUtils: https://www.hiveworkshop.com/threads/playerutils.278559/
* _________________________________________________________________________
* 2. API
* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
* struct SyncData
*
* method start takes nothing returns nothing
* method startChunk takes integer i, integer end returns nothing
* method refresh takes nothing returns nothing
* method destroy takes nothing returns nothing
*
* method addInt takes integer i returns nothing
* method addReal takes integer i returns nothing
* method addString takes string s, integer len returns nothing
* method addBool takes booleanflag returns nothing
*
* method readInt takes integer index returns integer
* method readReal takes integer index returns integer
* method readString takes integer index returns string
* method readBool takes integer index returns boolean
*
* method hasInt takes integer index returns boolean
* method hasReal takes integer index returns boolean
* method hasString takes integer index returns boolean
* method hasBool takes integer index returns boolean
*
* method isPlayerDone takes player p returns boolean
* method isPlayerIdDone takes integer pid returns boolean
*
* method addEventListener takes filterfunc func returns nothing
*
* ---------
*
* filterfunc onComplete
* filterfunc onError
* filterfunc onUpdate
* trigger trigger
*
* readonly player from
*
* readonly real timeStarted
* readonly real timeFinished
* readonly real timeElapsed
*
* readonly integer intCount
* readonly integer boolCount
* readonly integer strCount
* readonly integer realCount
* readonly integer playersDone
*
* readonly boolean buffering
*
* readonly static integer last
* readonly static player LocalPlayer
* readonly static boolean Initialized
*
* static method create takes player from returns SyncData
* static method destroy takes nothing returns nothing
* static method gameTime takes nothing returns real
*
* function GetSyncedData takes nothing returns SyncData
*
***************************************************************/
globals
// characters that can be synced (ascii)
private constant string ALPHABET = " !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\"\t\n"
// safe characters for use in game cache keys
// (case sensitive)
private constant string SAFE_KEYS = " !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`{|}~\"\t\n"
// stop reading the string buffer when reaching this char (this char cannot be synced)
private constant string TERM_CHAR = "~"
// how fast the buffer updates
private constant real UPDATE_PERIOD = 0.03125
// automatically recycle indices when the syncing player leaves
private constant boolean AUTO_DESTROY_ON_LEAVE = true
// automatically stop buffering when an error occurs
private constant boolean STOP_BUFFERING_ON_ERROR = true
// preload game cache key strings on init
private constant boolean PRELOAD_STR_CACHE = true
// size of the alphabet
private constant integer ALPHABET_BASE = StringLength(ALPHABET)
// maximum number of strings *per instance*
private constant integer MAX_STRINGS = 8192 // or any arbitrary high number
// filenames for gc (short names have faster sync time)
private constant string CACHE_FILE = "i"
private constant string CACHE_FILE_STR = "s"
// don't edit below this line
constant integer EVENT_SYNC_CACHE = 3
constant integer SYNC_ERROR_TIMEOUT = 1
constant integer SYNC_ERROR_PLAYERLEFT = 2
private SelectionSync Synchronizer
endglobals
//**************************************************************
function GetSyncedData takes nothing returns SyncData
return SyncData(SyncData.Last)
endfunction
public function I2Char takes string alphabet, integer i returns string
return SubString(alphabet, i, i + 1)
endfunction
public function Char2I takes string alphabet, string c returns integer
local integer i = 0
local string s
local integer l = StringLength(alphabet)
loop
set s = I2Char(alphabet, i)
exitwhen i == l
if (s == c) then
return i
endif
set i = i + 1
endloop
return 0
endfunction
public function ConvertBase takes string alphabet, integer i returns string
local integer b
local string s = ""
local integer l = StringLength(alphabet)
if i < l then
return I2Char(alphabet, i)
endif
loop
exitwhen i <= 0
set b = i - ( i / l ) * l
set s = I2Char(alphabet, b) + s
set i = i / l
endloop
return s
endfunction
public function PopulateString takes string s, integer makeLen returns string
local integer i = 0
local integer l = StringLength(s)
if (l == makeLen) then
return s
endif
set l = makeLen-l
loop
exitwhen i > l
set s = s + TERM_CHAR
set i = i + 1
endloop
return s
endfunction
//**************************************************************
globals
// string table keys
private constant integer KEY_STR_POS = (0*MAX_STRINGS)
private constant integer KEY_STR_LEN = (1*MAX_STRINGS)
// pending data storage space
private constant integer KEY_STR_CACHE = (2*MAX_STRINGS)
endglobals
struct SyncData
real timeout
filterfunc onComplete
filterfunc onError
filterfunc onUpdate
trigger trigger
readonly integer lastError
readonly player from
readonly real timeStarted
readonly real timeFinished
readonly real timeElapsed
readonly integer intCount
readonly integer boolCount
readonly integer strCount
readonly integer realCount
readonly integer playersDone
readonly boolean buffering
readonly static boolean Initialized = false
readonly static integer Last = 0
readonly static player LocalPlayer
readonly static integer LocalPlayerID
private static integer Running = 0
private static real timeCounter = 0.00
private static trigger EventTrig = CreateTrigger()
private static hashtable Table
private static hashtable CharTable
private static gamecache array Cache
private static integer array PendingCount
private static timer Elapsed
private static timer BufferTimer
private static integer AlphaHash
private integer strBufferLen
private trigger eventTrig
private string mkey
private boolean localFinished
private thistype next
private thistype prev
static method bool2I takes boolean b returns integer
if (b) then
return 1
endif
return 0
endmethod
private static method hashString takes string c returns integer
return StringHash(I2S(bool2I(StringCase(c, true) == c)) + c)
endmethod
static method char2I takes string alphabet, string c returns integer // requires preloading table with data
return LoadInteger(SyncData.CharTable, .AlphaHash, .hashString(c))
endmethod
private method resetVars takes nothing returns nothing
set this.intCount = 0
set this.strCount = 0
set this.boolCount = 0
set this.realCount = 0
set this.playersDone = 0
set this.strBufferLen = 0
set this.timeStarted = 0
set this.timeFinished = 0
set this.lastError = 0
set this.onComplete = null
set this.onError = null
set this.onUpdate = null
set this.timeout = 0.00
set this.buffering = false
set this.localFinished = false
set this.trigger = null
endmethod
private static method getKey takes integer pos returns string
local string position=""
if (HaveSavedString(Table, KEY_STR_CACHE, pos)) then
return LoadStr(Table, KEY_STR_CACHE, pos)
endif
set position = ConvertBase(SAFE_KEYS, pos)
call SaveStr(Table, KEY_STR_CACHE, pos, position)
return position
endmethod
static method create takes player from returns thistype
local thistype this
// Player has to be playing because of GetLocalPlayer use.
if (GetPlayerController(from) != MAP_CONTROL_USER or GetPlayerSlotState(from) != PLAYER_SLOT_STATE_PLAYING) then
return 0
endif
set this = thistype.allocate()
set this.from = from
set this.mkey = getKey(this-1)
call this.resetVars()
set thistype(0).next.prev = this
set this.next = thistype(0).next
set thistype(0).next = this
set this.prev = 0
return this
endmethod
method refresh takes nothing returns nothing
local integer i = 0
local integer p = 0
loop
static if (LIBRARY_PlayerUtils) then
exitwhen i == User.AmountPlaying
set p = User.fromPlaying(i).id
else
exitwhen i == bj_MAX_PLAYER_SLOTS
set p = i
endif
call RemoveSavedInteger(Table, this, KEY_STR_POS + p)
call RemoveSavedInteger(Table, this, KEY_STR_LEN + p)
call RemoveSavedBoolean(Table, p, this) // playerdone
set i = i + 1
endloop
call FlushStoredMission(Cache[0], this.mkey)
call FlushStoredMission(Cache[1], this.mkey)
call this.resetVars()
endmethod
method destroy takes nothing returns nothing
if (this.eventTrig != null) then
call DestroyTrigger(this.eventTrig)
set this.eventTrig=null
endif
call this.refresh()
set this.next.prev = this.prev
set this.prev.next = this.next
call this.deallocate()
endmethod
method hasInt takes integer index returns boolean
return HaveStoredInteger(Cache[0], this.mkey, getKey(index))
endmethod
method hasReal takes integer index returns boolean
return HaveStoredReal(Cache[0], this.mkey, getKey(index))
endmethod
method hasBool takes integer index returns boolean
return HaveStoredBoolean(Cache[0], this.mkey, getKey(index))
endmethod
method hasString takes integer index returns boolean
local integer i = LoadInteger(Table, this, KEY_STR_POS+index)
if (index > 0 and i == 0) then
return false
endif
return HaveStoredInteger(Cache[1], this.mkey, getKey(i + LoadInteger(Table, this, KEY_STR_LEN+index)))
endmethod
method addInt takes integer i returns nothing
local string position=getKey(intCount)
if (LocalPlayer == this.from) then
call StoreInteger(Cache[0], this.mkey, position, i)
endif
set intCount=intCount+1
endmethod
method addReal takes real i returns nothing
local string position=getKey(realCount)
if (LocalPlayer == this.from) then
call StoreReal(Cache[0], this.mkey, position, i)
endif
set realCount=realCount+1
endmethod
method addBool takes boolean flag returns nothing
local string position=getKey(boolCount)
if (LocalPlayer == this.from) then
call StoreBoolean(Cache[0], this.mkey, position, flag)
endif
set boolCount=boolCount+1
endmethod
// SyncStoredString doesn't work
method addStringEx takes string s, integer maxLen, boolean doSync returns nothing
local string position
local integer i = 0
local integer strPos = 0
local integer strLen = 0
if (StringLength(s) < maxLen) then
set s = PopulateString(s, maxLen)
endif
// store the string position in the table
if (strCount == 0) then
call SaveInteger(Table, this, KEY_STR_POS, 0)
else
set strLen = LoadInteger(Table, this, KEY_STR_LEN + (strCount-1)) + 1
set strPos = LoadInteger(Table, this, KEY_STR_POS + (strCount-1)) + strLen
call SaveInteger(Table, this, KEY_STR_POS + strCount, strPos)
endif
// convert each character in the string to an integer
loop
exitwhen i > maxLen
set position = getKey(strPos + i)
if (LocalPlayer == this.from) then
call StoreInteger(Cache[1], this.mkey, position, .char2I(ALPHABET, SubString(s, i, i + 1)))
if (doSync and LocalPlayer == this.from) then
call SyncStoredInteger(Cache[1], this.mkey, position)
endif
endif
set i = i + 1
endloop
set strBufferLen = strBufferLen + maxLen
call SaveInteger(Table, this, KEY_STR_LEN+strCount, maxLen) // store the length as well
set strCount=strCount+1
endmethod
method addString takes string s, integer maxLen returns nothing
call addStringEx(s, maxLen, false)
endmethod
method readInt takes integer index returns integer
return GetStoredInteger(Cache[0], this.mkey, getKey(index))
endmethod
method readReal takes integer index returns real
return GetStoredReal(Cache[0], this.mkey, getKey(index))
endmethod
method readBool takes integer index returns boolean
return GetStoredBoolean(Cache[0], this.mkey, getKey(index))
endmethod
method readString takes integer index returns string
local string s = ""
local string c
local integer i = 0
local integer strLen = LoadInteger(Table, this, KEY_STR_LEN+index)
local integer strPos
if (not hasString(index)) then
return null
endif
set strLen = LoadInteger(Table, this, KEY_STR_LEN+index)
set strPos = LoadInteger(Table, this, KEY_STR_POS+index)
loop
exitwhen i > strLen
set c = I2Char(ALPHABET, GetStoredInteger(Cache[1], this.mkey, getKey(strPos + i)))
if (c == TERM_CHAR) then
return s
endif
set s = s + c
set i = i + 1
endloop
return s
endmethod
private method fireListeners takes nothing returns nothing
set Last = this
if (this.eventTrig != null) then
call TriggerEvaluate(this.eventTrig)
endif
if (this.trigger != null and TriggerEvaluate(this.trigger)) then
call TriggerExecute(this.trigger)
endif
endmethod
private method fireEvent takes filterfunc func returns nothing
set Last = this
call TriggerAddCondition(EventTrig, func)
call TriggerEvaluate(EventTrig)
call TriggerClearConditions(EventTrig)
endmethod
method addEventListener takes filterfunc func returns nothing
if (this.eventTrig == null) then
set this.eventTrig = CreateTrigger()
endif
call TriggerAddCondition(this.eventTrig, func)
endmethod
public static method gameTime takes nothing returns real
return timeCounter + TimerGetElapsed(Elapsed)
endmethod
private method error takes integer errorId returns nothing
set this.lastError = errorId
if (this.onError != null) then
call this.fireEvent(this.onError)
endif
call this.fireListeners()
static if (STOP_BUFFERING_ON_ERROR) then
set this.buffering = false
endif
endmethod
private static method readBuffer takes nothing returns nothing
local boolean b = true
local integer i = 0
local thistype data = thistype(0).next
loop
exitwhen data == 0
// find the nearest instance that is still buffering
loop
exitwhen data.buffering or data == 0
set data=data.next
endloop
// if none are found, exit
if (not data.buffering) then
return
endif
set data.timeElapsed = data.timeElapsed + UPDATE_PERIOD
if (data.onUpdate != null) then
call data.fireEvent(data.onUpdate)
endif
if (data.timeout > 0 and data.timeElapsed > data.timeout) then
call data.error(SYNC_ERROR_TIMEOUT)
endif
// if the player has left, destroy the instance
if (GetPlayerSlotState(data.from) != PLAYER_SLOT_STATE_PLAYING) then
call data.error(SYNC_ERROR_PLAYERLEFT)
static if (AUTO_DESTROY_ON_LEAVE) then
call data.destroy()
endif
endif
set b = true
// make sure all integers have been synced
if (data.intCount > 0 and not data.hasInt(data.intCount-1)) then
set b = false
endif
// make sure all reals have been synced
if (data.realCount > 0 and not data.hasReal(data.realCount-1)) then
set b = false
endif
// check strings too
if (data.strCount > 0 and not data.hasString(data.strCount-1)) then
set b = false
endif
// and booleans
if (data.boolCount > 0 and not data.hasBool(data.boolCount-1)) then
set b = false
endif
// if everything has been synced
if (b) then
if (not data.localFinished) then // async
set data.localFinished = true
// notify everyone that the local player has recieved all of the data
call Synchronizer.syncValue(LocalPlayer, data)
endif
endif
set data = data.next
endloop
endmethod
public method initInstance takes nothing returns nothing
if (this.timeStarted != 0.00) then
return
endif
set this.timeStarted = gameTime()
set this.playersDone = 0
set this.buffering = true
set this.timeElapsed = (UPDATE_PERIOD - TimerGetElapsed(BufferTimer)) * -1
if (Running==0) then
call TimerStart(BufferTimer, UPDATE_PERIOD, true, function thistype.readBuffer)
call thistype.readBuffer()
endif
set Running=Running+1
endmethod
method syncInt takes integer i returns nothing
local string position = getKey(intCount)
call this.addInt(i)
if (LocalPlayer == this.from) then
call SyncStoredInteger(Cache[0], this.mkey, position)
endif
call this.initInstance()
endmethod
method syncReal takes real r returns nothing
local string position = getKey(realCount)
call this.addReal(r)
if (LocalPlayer == this.from) then
call SyncStoredReal(Cache[0], this.mkey, position)
endif
call this.initInstance()
endmethod
method syncBoolean takes boolean b returns nothing
local string position = getKey(boolCount)
call this.addBool(b)
if (LocalPlayer == this.from) then
call SyncStoredReal(Cache[0], this.mkey, position)
endif
call this.initInstance()
endmethod
method syncString takes string s, integer maxLen returns nothing
local string position = getKey(strCount)
call this.addStringEx(s, maxLen, true)
call this.initInstance()
endmethod
method startChunk takes integer i, integer end returns boolean
local integer n = 0
local integer j = 0
local integer p = 0
local string position
if (this.timeStarted != 0.00) then
return false
endif
// Begin syncing
loop
exitwhen i > end
set position = LoadStr(Table, KEY_STR_CACHE, i)
if (i < intCount and LocalPlayer == this.from) then
call SyncStoredInteger(Cache[0], this.mkey, position)
endif
if (i < realCount and LocalPlayer == this.from) then
call SyncStoredReal(Cache[0], this.mkey, position)
endif
if (i < boolCount and LocalPlayer == this.from) then
call SyncStoredBoolean(Cache[0], this.mkey, position)
endif
if (i < strCount and LocalPlayer == this.from) then
set n = LoadInteger(Table, this, KEY_STR_LEN + i)
set p = LoadInteger(Table, this, KEY_STR_POS + i)
set j = 0
loop
exitwhen j > n
set position = LoadStr(Table, KEY_STR_CACHE, p + j)
if (LocalPlayer == this.from) then
call SyncStoredInteger(Cache[1], this.mkey, position)
endif
set j = j + 1
endloop
endif
set i = i + 1
endloop
call this.initInstance()
return true
endmethod
method start takes nothing returns boolean
local integer l = intCount
// Find the highest count
if (l < realCount) then
set l = realCount
endif
if (l < strCount) then
set l = strCount
endif
if (l < boolCount) then
set l = boolCount
endif
return startChunk(0, l)
endmethod
method isPlayerIdDone takes integer pid returns boolean
return LoadBoolean(Table, pid, this)
endmethod
method isPlayerDone takes player p returns boolean
return isPlayerIdDone(GetPlayerId(p))
endmethod
private static method updateStatus takes nothing returns boolean
local integer i = 0
local integer p = GetSyncedPlayerId()
local boolean b = true
local boolean c = true
local thistype data = GetSyncedInteger()
local triggercondition tc
if (GetSyncedInstance() != Synchronizer or not data.buffering) then
return false
endif
set data.playersDone = data.playersDone + 1
call SaveBoolean(Table, p, data, true) // set playerdone
// check if everyone has received the data
loop
static if (LIBRARY_PlayerUtils) then
exitwhen i == User.AmountPlaying
set p = User.fromPlaying(i).id
set c = User.fromPlaying(i).isPlaying
else
exitwhen i == bj_MAX_PLAYER_SLOTS
set p = i
set c = (GetPlayerController(Player(p)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(p)) == PLAYER_SLOT_STATE_PLAYING)
endif
if (c and not data.isPlayerIdDone(p)) then
set b = false // someone hasn't
endif
set i = i + 1
endloop
// if everyone has recieved the data
if (b) then
set Running = Running-1
if (Running == 0) then
call PauseTimer(BufferTimer)
endif
set data.buffering = false
set data.timeFinished = gameTime()
set data.timeElapsed = data.timeFinished - data.timeStarted
// fire events
if (data.onComplete != null) then
call data.fireEvent(data.onComplete)
endif
call data.fireListeners()
call SyncInteger_FireEvents(EVENT_SYNC_CACHE)
endif
return false
endmethod
private static method trackTime takes nothing returns nothing
set timeCounter = timeCounter + 10
endmethod
private static method preloadChar2I takes nothing returns nothing
local integer i = 0
local string c
set .AlphaHash = .hashString(ALPHABET)
loop
exitwhen i >= ALPHABET_BASE
set c = I2Char(ALPHABET, i)
call SaveInteger(SyncData.CharTable, .AlphaHash, .hashString(c), Char2I(ALPHABET, c))
set i = i + 1
endloop
endmethod
private static method onInit takes nothing returns nothing
static if (SyncInteger_DEFAULT_INSTANCE) then
set Synchronizer = SyncInteger_DefaultInstance
else
set Synchronizer = SelectionSync.create()
endif
set Table = InitHashtable()
set CharTable = InitHashtable()
set Cache[0] = InitGameCache(CACHE_FILE)
set Cache[1] = InitGameCache(CACHE_FILE_STR)
set Elapsed = CreateTimer()
set BufferTimer = CreateTimer()
static if (LIBRARY_PlayerUtils) then
set LocalPlayer = User.Local
set LocalPlayerID = User.fromLocal().id
else
set LocalPlayer = GetLocalPlayer()
set LocalPlayerID = GetPlayerId(LocalPlayer)
endif
call OnSyncInteger(Filter(function thistype.updateStatus))
call TimerStart(Elapsed, 10., true, function thistype.trackTime)
static if (PRELOAD_STR_CACHE) then
loop
exitwhen Last == ALPHABET_BASE
call getKey(Last)
set Last = Last + 1
endloop
set Last = 0
endif
call preloadChar2I()
set Initialized = true
endmethod
endstruct
endlibrary
Attachments
Last edited: