• 🏆 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] [System] Local Save System

Level 2
Joined
Dec 31, 2011
Messages
12
It based gamecache synchronization. But it check synchronization status by selecting unit. So, It works without desync!


It has three libraries.
1. Localtable : It is base library. But it can only save positive integer and 0.
2. Localdata : It is advanced library. It uses Localtable library. It can save integers, reals, booleans, strings.
3. LocaldataFunctions (optional) : It is wrapper library for JASS and GUI. And It has "CreateLocalRegistry" function.


It need to register Allowed Local Files. (You can make registry file by "CreateLocalRegistry" function.)



English is not my first language, so please excuse any English mistakes.




[Localtable]
JASS:
static if false then
 ************************************************************************
 * struct Localtable
 *
 * Author   : bbbb1211 (Croissant)
 * Version  : 1.00.1211
 * Updated  : 2015.03.18 
 * Description : It is data table with file. No desync.
 *
 * Warning  : It has ObjectMerger for 'ltu0' object.
 *
 *
 * 
 * ***** API *****
 * [static constants]
 *  static constant integer VERSION
 *   - Current version
 *  
 *  static constant integer STATUS_STANDBY
 *  static constant integer STATUS_READING
 *  
 *  static constant integer ERROR_NONE
 *  static constant integer ERROR_VERSION
 *  static constant integer ERROR_FILEACCESS
 *  static constant integer ERROR_TIMEOUT
 *  static constant integer ERROR_DESYNC
 *  static constant integer ERROR_PARSINGFAIL
 * 
 * 
 * [static methods]
 *  static method getLastRead takes nothing returns thistype
 *  static method create takes player id, string filepath returns thistype
 * 
 * 
 * [methods]
 *  method getOwningPlayer takes nothing returns player
 *  method getFilepath takes nothing returns string
 *  method getVersion takes nothing returns integer
 *  method getStatus takes nothing returns integer
 *   - For callbackFunc after reading.
 *
 *  method getError takes nothing returns integer
 *   - For callbackFunc after reading.
 *
 *  method setValue takes integer row, integer column, integer value returns nothing
 *   - Set cell(row, column) value. (0 <= value)
 *
 *  method getValue takes integer row, integer column returns integer
 *  method flushTable takes nothing returns nothing
 *  method flushRow takes integer row returns nothing
 *  method write takes nothing returns nothing
 *   - Save Localtable to file.
 *
 *  method read takes real timeout, integer try, code callbackFunc returns nothing
 *   - Read Localtable from file.
 *     It tries to synchronize for "timeout" seconds, "try" times. (If "timeout = 0.1" and "try = 10" then, total timeout will be 1sec.)
 *     If synchronization is failed or successed, it execute callbackFunc.
 *     If synchronization is failed, you can find cause by getError method.
 * 
 ************************************************************************
endif

library CroissantLocaltable



/************************************************************************/
/*                                                                      */
/* ObjectMerger                                                         */
//! external ObjectMerger w3u hfoo ltu0 unam " " unsf "(Localtable)" ucol 0.0 uerd 0.0 ushu "" umdl ".mdl" ushr 0 ulpz 0.0 uimz 0.0 uscb 0 udtm 0.1 ussc -1.0 uspa "" uico "Textures\Black32.blp" ucpt 0.0 ucbs 0.0 uabi Avul ulev 1 uhom 1 usid 0 usin 0 ufoo 0 upri 0 upoi 0 urac other uhrt "none" usnd "" umvs 0 umvt "" uaen 0 upgr "" uine 0
/*                                                                      */
/************************************************************************/








globals
/************************************************************************/
/*  User Configuration                                                  */
/*                                                                      */
/* Localtable sender unittype                                           */
    private constant integer  LOCALTABLE_SENDER_UNITTYPE    = 'ltu0'
/*                                                                      */
/* Localtable sender location                                           */
/*  - Localtable dummy location.                                        */
    private constant location LOCALTABLE_SENDER_LOCATION    = Location(0, 0)
/*                                                                      */
/************************************************************************/
endglobals


struct Localtable
    public static constant integer VERSION              = 1001211       // v1.00.1211
    
    public static constant integer STATUS_STANDBY       = 0
    public static constant integer STATUS_READING       = 1
    
    public static constant integer ERROR_NONE           = 0
    public static constant integer ERROR_VERSION        = 1
    public static constant integer ERROR_FILEACCESS     = 2
    public static constant integer ERROR_TIMEOUT        = 3
    public static constant integer ERROR_DESYNC         = 4
    public static constant integer ERROR_PARSINGFAIL    = 5
    
    
    private static constant integer SIGNAL_NONE             = 0
    private static constant integer SIGNAL_FILEACCESSERROR  = 1
    private static constant integer SIGNAL_VERSIONERROR     = 2
    private static constant integer SIGNAL_DESYNCERROR      = 3
    
    private static constant integer SYNCSTEP_NONE           = 0
    private static constant integer SYNCSTEP_SYNCSIZE       = 1
    private static constant integer SYNCSTEP_CHECKSYNC      = 2
    
    private static constant integer SENDER_UNITTYPE         = LOCALTABLE_SENDER_UNITTYPE
    
    
    private static gamecache    cache
    private static hashtable    hash
    private static location     senderLoc
    private static string array senderKeys
    private static group        selectionGroup
    private static Localtable   lastRead
    
    
    private static method onInit takes nothing returns nothing
        local trigger trig
        local integer i
        set thistype.cache          = InitGameCache("localtable.w3c")
        set thistype.hash           = InitHashtable()
        set thistype.senderLoc      = LOCALTABLE_SENDER_LOCATION
        set thistype.selectionGroup = CreateGroup()
        set thistype.lastRead       = 0
    endmethod
    
    
    private static method isPlayerPlayingUser takes player whichPlayer returns boolean
        return GetPlayerController(whichPlayer) == MAP_CONTROL_USER and GetPlayerSlotState(whichPlayer) == PLAYER_SLOT_STATE_PLAYING
    endmethod
    
    
    private player         owner
    private string         filepath
    private integer        version
    private integer        size
    private integer        status
    private integer        error
    private unit    array  packets[12]  // packet senders
    private unit    array  senders[12]  // sign senders
    private string  array  buffers[12]
    private boolean array  syncStates[12]
    private integer        syncSize
    private real           syncTimeout
    private integer        syncTry
    private integer        syncCount
    private integer        syncStep
    private timer          syncTimer
    private hashtable      table
    private integer        nParents
    private trigger        callbackTrig
    private triggeraction  callbackAction
    private trigger        packetReceiverTrig
    private triggeraction  packetReceiverAction
    private trigger        signReceiverTrig
    private triggeraction  signReceiverAction
    private trigger        onLeaveTrig
    private triggeraction  onLeaveAction
    
    
    public static method getLastRead takes nothing returns thistype
        return thistype.lastRead
    endmethod
    
    
    public static method create takes player id, string filepath returns thistype
        local thistype this = thistype.allocate()
        local integer  i
        set this.owner      = id
        set this.filepath   = filepath
        set this.version    = 0
        set this.size       = 0
        set this.status     = thistype.STATUS_STANDBY
        set this.error      = thistype.ERROR_NONE
        set this.syncSize   = 0
        set this.syncTimeout = 0.0
        set this.syncTry    = 0
        set this.syncCount  = 0
        set this.syncStep   = thistype.SYNCSTEP_NONE
        set this.syncTimer  = CreateTimer()
        call SaveInteger(thistype.hash, 0, GetHandleId(this.syncTimer), this)
        set this.table      = InitHashtable()
        set this.nParents   = 0
        set this.callbackTrig         = CreateTrigger()
        set this.callbackAction       = null
        set this.packetReceiverTrig   = CreateTrigger()
        set this.packetReceiverAction = TriggerAddAction(this.packetReceiverTrig, function thistype.receivePacket)
        set this.signReceiverTrig     = CreateTrigger()
        set this.signReceiverAction   = TriggerAddAction(this.signReceiverTrig, function thistype.receiveSign)
        set this.onLeaveTrig          = CreateTrigger()
        set this.onLeaveAction        = TriggerAddAction(this.onLeaveTrig, function thistype.onPlayerLeave)
        call SaveInteger(thistype.hash, 0, GetHandleId(this.onLeaveTrig), this)
        
        // initialize packet senders
        set i = 0
        loop
            exitwhen i == 12
            set this.packets[i] = CreateUnitAtLoc(this.owner, thistype.SENDER_UNITTYPE, thistype.senderLoc, 0)
            call SetUnitUserData(this.packets[i], i)
            call SaveInteger(thistype.hash, 0, GetHandleId(this.packets[i]), this)
            call TriggerRegisterUnitEvent(this.packetReceiverTrig, this.packets[i], EVENT_UNIT_SELECTED)
            set i = i + 1
        endloop
        
        // initialize players
        set i = 0
        loop
            exitwhen i == 12
            if (thistype.isPlayerPlayingUser(Player(i))) then
                set this.senders[i] = CreateUnitAtLoc(Player(i), thistype.SENDER_UNITTYPE, thistype.senderLoc, 0)
                call SetUnitUserData(this.senders[i], -1)
                call SaveInteger(thistype.hash, 0, GetHandleId(this.senders[i]), this)
                call TriggerRegisterUnitEvent(this.signReceiverTrig, this.senders[i], EVENT_UNIT_SELECTED)
                set this.buffers[i] = ""
                set this.syncStates[i] = false
                call TriggerRegisterPlayerEvent(this.onLeaveTrig, Player(i), EVENT_PLAYER_LEAVE)
            endif
            set i = i + 1
        endloop
        
        return this
    endmethod
    
    
    private method onDestroy takes nothing returns nothing
        local integer i
        local unit    u
        set this.owner      = null
        set this.filepath   = null
        set this.version    = 0
        set this.size       = 0
        set this.status     = thistype.STATUS_STANDBY
        set this.error      = thistype.ERROR_NONE
        set this.syncSize   = 0
        set this.syncTimeout = 0.0
        set this.syncTry    = 0
        set this.syncCount  = 0
        set this.syncStep   = thistype.SYNCSTEP_NONE
        call RemoveSavedInteger(thistype.hash, 0, GetHandleId(this.syncTimer))
        call DestroyTimer(this.syncTimer)
        set this.syncTimer  = null
        call FlushParentHashtable(this.table)
        set this.table = null
        set this.nParents   = 0
        call RemoveSavedInteger(thistype.hash, 0, GetHandleId(this.onLeaveTrig))
        call TriggerRemoveAction(this.callbackTrig, this.callbackAction)
        call TriggerRemoveAction(this.packetReceiverTrig, this.packetReceiverAction)
        call TriggerRemoveAction(this.signReceiverTrig, this.signReceiverAction)
        call TriggerRemoveAction(this.onLeaveTrig, this.onLeaveAction)
        call DestroyTrigger(this.callbackTrig)
        call DestroyTrigger(this.packetReceiverTrig)
        call DestroyTrigger(this.signReceiverTrig)
        call DestroyTrigger(this.onLeaveTrig)
        set this.callbackTrig         = null
        set this.callbackAction       = null
        set this.packetReceiverTrig   = null
        set this.packetReceiverAction = null
        set this.signReceiverTrig     = null
        set this.signReceiverAction   = null
        set this.onLeaveTrig          = null
        set this.onLeaveAction        = null
        set i = 0
        loop
            exitwhen i == 12
            call RemoveSavedInteger(thistype.hash, 0, GetHandleId(this.packets[i]))
            call RemoveUnit(this.packets[i])
            set this.packets[i] = null
            call RemoveSavedInteger(thistype.hash, 0, GetHandleId(this.senders[i]))
            call RemoveUnit(this.senders[i])
            set this.senders[i] = null
            set this.buffers[i] = null
            set this.syncStates[i] = false
            set i = i + 1
        endloop
    endmethod
    
    
    public method getOwningPlayer takes nothing returns player
        return this.owner
    endmethod
    
    
    public method getFilepath takes nothing returns string
        return this.filepath
    endmethod
    
    
    public method getVersion takes nothing returns integer
        return this.version
    endmethod
    
    
    public method getStatus takes nothing returns integer
        return this.status
    endmethod
    
    
    public method getError takes nothing returns integer
        return this.error
    endmethod
    
    
    public method setValue takes integer row, integer column, integer value returns nothing
        local integer nChildren
        local integer i
        
        if (row < 0 or column < 0 or value < 0) then
            return
        endif
        
        if (value > 0) then
            call SaveInteger(this.table, row, column, value)
            // check size
            set nChildren = LoadInteger(this.table, row, -1)
            if (row >= this.nParents) then
                set this.nParents = row + 1
            endif
            if (column >= nChildren) then
                call SaveInteger(this.table, row, -1, column + 1)
            endif
        else
            call RemoveSavedInteger(this.table, row, column)
            // check size
            set nChildren = LoadInteger(this.table, row, -1)
            set i = nChildren - 1
            loop
                exitwhen (i < 0 or HaveSavedInteger(this.table, row, i))
                set i = i - 1
            endloop
            call SaveInteger(this.table, row, -1, i + 1)
            set i = this.nParents - 1
            loop
                exitwhen (i < 0 or LoadInteger(this.table, i, -1) > 0)
                set i = i - 1
            endloop
            set this.nParents = i + 1
        endif
    endmethod
    
    
    public method getValue takes integer row, integer column returns integer
        if (row < 0 or column < 0) then
            return -1
        endif
        return LoadInteger(this.table, row, column)
    endmethod
    
    
    public method flushTable takes nothing returns nothing
        call FlushParentHashtable(this.table)
        set this.table = InitHashtable()
        set this.nParents = 0
    endmethod
    
    
    public method flushRow takes integer row returns nothing
        local integer i
        
        if (row < 0) then
            return
        endif
        
        call FlushChildHashtable(this.table, row)
        set i = this.nParents - 1
        loop
            exitwhen (i < 0 or LoadInteger(this.table, i, -1) > 0)
            set i = i - 1
        endloop
        set this.nParents = i + 1
    endmethod
    
    
    public method write takes nothing returns nothing
        local integer cursor = 0
        local integer nChildren
        local integer i
        local integer j
        
        // calculate size
        // size = fileHeaderSize + parentHeadersSize + childrenSize
        set this.size = 2 + this.nParents
        set i = 0
        loop
            exitwhen (i == this.nParents)
            set this.size = this.size + LoadInteger(this.table, i, -1)
            set i = i + 1
        endloop
        set this.version = thistype.VERSION
        
        // start writing
        if (GetLocalPlayer() == this.owner) then
            call PreloadGenClear()
            call PreloadGenStart()
            
            // write size
            call Preload("\")\n    call SetPlayerTechMaxAllowed(Player(15), " + I2S(0) + "," + I2S(this.size) + ")//")
            // write file header
            call Preload("\")\n    call SetPlayerTechMaxAllowed(Player(15), " + I2S(1) + "," + I2S(this.version) + ")//")
            
            // write body
            set cursor = 2
            set i = 0
            loop
                exitwhen (i == this.nParents)
                set nChildren = LoadInteger(this.table, i, -1)
                // write parent header
                call Preload("\")\n    call SetPlayerTechMaxAllowed(Player(15), " + I2S(cursor) + "," + I2S(nChildren) + ")//")
                set cursor = cursor + 1
                // write children
                set j = 0
                loop
                    exitwhen (j == nChildren)
                    call Preload("\")\n    call SetPlayerTechMaxAllowed(Player(15), " + I2S(cursor) + "," + I2S(LoadInteger(this.table, i, j)) + ")//")
                    set cursor = cursor + 1
                    set j = j + 1
                endloop
                set i = i + 1
            endloop
            
            call PreloadGenEnd(this.filepath)
        endif
    endmethod
    
    
    private static method receivePacket takes nothing returns nothing
        local thistype this     = LoadInteger(thistype.hash, 0, GetHandleId(GetTriggerUnit()))
        local player   sender   = GetOwningPlayer(GetTriggerUnit())
        local integer  senderId = GetPlayerId(sender)
        local integer  packet   = GetUnitUserData(GetTriggerUnit())
        local integer  bufferSize = StringLength(this.buffers[senderId])
        
        if (this.status == thistype.STATUS_READING) then
            if (packet == 10) then
                call this.receiveData(sender, S2I(this.buffers[senderId]))
                set this.buffers[senderId] = ""
            elseif (packet == 11) then
                call this.receiveSignal(sender, S2I(SubString(this.buffers[senderId], bufferSize - 1, bufferSize)))
                set this.buffers[senderId] = ""
            else
                set this.buffers[senderId] = this.buffers[senderId] + I2S(packet)
            endif
        endif
    endmethod
    
    
    private method receiveData takes player sender, integer data returns nothing
        local integer senderId   = GetPlayerId(sender)
        local boolean isHostSign = sender == this.owner
        local boolean isHost     = GetLocalPlayer() == this.owner
        local integer i
        
        debug call BJDebugMsg("receiveData:" + "Player" + I2S(senderId) + "," + I2S(data))
        if (this.syncStep == SYNCSTEP_SYNCSIZE) then
            if (isHostSign) then
                // check desync
                if (isHost) then
                    if (data != this.size) then
                        call this.openPacketSender()
                        // send signal
                        call this.sendSignal(thistype.SIGNAL_DESYNCERROR)
                        call this.closePacketSender()
                    endif
                endif
                
                set this.size = data
                set this.syncStep = thistype.SYNCSTEP_CHECKSYNC
                
                debug call BJDebugMsg("syncStep:" + I2S(this.syncStep))
            endif
        elseif (this.syncStep == thistype.SYNCSTEP_CHECKSYNC) then
            // do nothing
        endif
    endmethod
    
    
    private method receiveSignal takes player sender, integer signal returns nothing
        local integer senderId   = GetPlayerId(sender)
        local boolean isHostSign = sender == this.owner
        local boolean isHost     = GetLocalPlayer() == this.owner
        
        debug call BJDebugMsg("receiveSignal:" + "Player" + I2S(senderId) + "," + I2S(signal))
        if (isHostSign) then
            if     (signal == thistype.SIGNAL_FILEACCESSERROR) then
                call this.stop(thistype.ERROR_FILEACCESS)
            elseif (signal == thistype.SIGNAL_VERSIONERROR   ) then
                call this.stop(thistype.ERROR_VERSION)
            elseif (signal == thistype.SIGNAL_DESYNCERROR    ) then
                call this.stop(thistype.ERROR_DESYNC)
            endif
        endif
    endmethod
    
    
    private static method receiveSign takes nothing returns nothing
        local thistype this       = LoadInteger(thistype.hash, 0, GetHandleId(GetTriggerUnit()))
        local player   sender     = GetOwningPlayer(GetTriggerUnit())
        local integer  senderId   = GetPlayerId(sender)
        local boolean  isHostSign = sender == this.owner
        local boolean  isHost     = GetLocalPlayer() == this.owner
        
        debug call BJDebugMsg("receiveSign:" + "Player" + I2S(senderId))
        
        if (this.status == thistype.STATUS_READING) then
            if (this.syncStep == thistype.SYNCSTEP_CHECKSYNC) then
                if (not isHostSign) then
                    set this.syncStates[senderId] = true
                    // check synchronization
                    if (this.isSyncronized()) then
                        if (this.parseFileData() == true) then
                            call this.stop(thistype.ERROR_NONE)
                        else
                            call this.stop(thistype.ERROR_PARSINGFAIL)
                        endif
                    endif
                endif
            endif
        endif
    endmethod
    
    
    private static method onPlayerLeave takes nothing returns nothing
        local thistype this = LoadInteger(thistype.hash, 0, GetHandleId(GetTriggeringTrigger()))
        local boolean  isHostSign = GetTriggerPlayer() == this.owner
        
        if (this.status == thistype.STATUS_READING) then
            if (this.syncStep == thistype.SYNCSTEP_CHECKSYNC) then
                if (isHostSign) then
                    call this.stop(thistype.ERROR_DESYNC)
                else
                    // check synchronization
                    if (this.isSyncronized()) then
                        if (this.parseFileData() == true) then
                            call this.stop(thistype.ERROR_NONE)
                        else
                            call this.stop(thistype.ERROR_PARSINGFAIL)
                        endif
                    endif
                endif
            endif
        endif
    endmethod
    
    
    private method isSyncronized takes nothing returns boolean
        local boolean b = true
        local integer i = 0
    
        loop
            exitwhen (i == 12)
            if (thistype.isPlayerPlayingUser(Player(i)) and this.syncStates[i] == false) then
                return false
            endif
            set i = i + 1
        endloop
        
        return true
    endmethod
    
    
    private method openPacketSender takes nothing returns nothing
        call GroupEnumUnitsSelected(thistype.selectionGroup, GetLocalPlayer(), null)
        call ClearSelection()
    endmethod
    
    
    private method closePacketSender takes nothing returns nothing
        local unit u
        call ClearSelection()
        //call ForGroup(thistype.selectionGroup, function SelectGroupBJEnum)
        loop
            set u = FirstOfGroup(thistype.selectionGroup)
            exitwhen (u == null)
            call SelectUnit(u, true)
            call GroupRemoveUnit(thistype.selectionGroup, u)
        endloop
    endmethod
    
    
    private method sendPacket takes integer packet returns nothing
        call SelectUnitSingle(this.packets[packet])
    endmethod
    
    
    private method sendData takes integer data returns nothing
        local string  s = I2S(data)
        local integer l = StringLength(s)
        local integer i = 0
        if (data > 0) then
            loop
                exitwhen (i == l)
                call this.sendPacket(S2I(SubString(s, i, i + 1)))
                set i = i + 1
            endloop
        endif
        call this.sendPacket(10)
    endmethod
    
    
    private method sendSignal takes integer signal returns nothing
        call this.sendPacket(signal)
        call this.sendPacket(11)
    endmethod
    
    
    private method sendSign takes nothing returns nothing
        local integer id = GetPlayerId(GetLocalPlayer())
        call SelectUnitSingle(this.senders[id])
    endmethod
    
    
    private method isSupported takes nothing returns boolean
        local boolean isSizeSupported = this.size > 0
        local boolean isVersionSuppored = this.version == 1001211
        
        return isSizeSupported and isVersionSuppored
    endmethod
    
    
    private static method checkSync takes nothing returns nothing
        local thistype this       = LoadInteger(thistype.hash, 0, GetHandleId(GetExpiredTimer()))
        local integer  playerId   = GetPlayerId(GetLocalPlayer())
        local string   missionKey = I2S(this)
        local boolean  isHost     = GetLocalPlayer() == this.owner
        local integer  cursor
        
        if (this.status == thistype.STATUS_READING) then
            if (this.syncStep == thistype.SYNCSTEP_CHECKSYNC) then
                if (this.syncSize < this.size and this.syncStates[playerId] == false) then
                    loop
                        exitwhen (this.syncSize == this.size and not HaveStoredInteger(thistype.cache, missionKey, I2S(this.syncSize)))
                        set this.syncSize = this.syncSize + 1
                    endloop
                    if (this.syncSize == this.size) then
                        call this.openPacketSender()
                        call this.sendSign()
                        call this.closePacketSender()
                    endif
                endif
            endif
            
            // check synchronization for singleplayer
            if (this.syncCount == 0) then
                // check synchronization
                if (this.isSyncronized()) then
                    if (this.parseFileData() == true) then
                        call this.stop(thistype.ERROR_NONE)
                    else
                        call this.stop(thistype.ERROR_PARSINGFAIL)
                    endif
                endif
            endif
            
            // check timeout
            set this.syncCount = this.syncCount + 1
            if (this.syncCount >= this.syncTry) then
                call this.stop(thistype.ERROR_TIMEOUT)
            endif
        else
            call this.stop(thistype.ERROR_TIMEOUT)
        endif
    endmethod
    
    
    private method synchronize takes real timeout, integer try, code callbackFunc returns nothing
        local integer cursor
        local integer i
        local boolean isHost     = GetLocalPlayer() == this.owner
        local integer hostId     = GetPlayerId(this.owner)
        local string  missionKey = I2S(this)
        
        // initialize
        if (this.callbackAction != null) then
            call TriggerRemoveAction(this.callbackTrig, this.callbackAction)
        endif
        set i = 0
        loop
            exitwhen (i == 12)
            set this.buffers[i]    = ""
            set this.syncStates[i] = false
            set i = i + 1
        endloop
        set this.syncStates[GetPlayerId(this.owner)] = true
        set this.syncTimeout    = timeout
        set this.syncTry        = try
        set this.syncCount      = 0
        set this.callbackAction = TriggerAddAction(this.callbackTrig, callbackFunc)
        if (isHost) then
            set this.syncSize = this.size
        else
            set this.syncSize = 0
        endif
        
        set this.status   = thistype.STATUS_READING
        set this.syncStep = thistype.SYNCSTEP_SYNCSIZE
        set this.error    = thistype.ERROR_NONE
        
        call TriggerSyncStart()
        if (isHost) then
            if (this.isSupported()) then
                set cursor = 0
                loop
                    exitwhen (cursor == this.syncSize)
                    call SyncStoredInteger(thistype.cache, missionKey, I2S(cursor))
                    set cursor = cursor + 1
                endloop
            endif
        endif
        call TriggerSyncReady()
        
        // send size data
        if (isHost) then
            call this.openPacketSender()
            if (this.isSupported()) then
                call this.sendData(this.syncSize)
            elseif (this.size == 0 and this.version == 0) then
                call this.sendSignal(thistype.SIGNAL_FILEACCESSERROR)
            else
                call this.sendSignal(thistype.SIGNAL_VERSIONERROR)
            endif
            call this.closePacketSender()
        endif
        
        call TimerStart(this.syncTimer, this.syncTimeout, true, function thistype.checkSync)
    endmethod
    
    
    public method read takes real timeout, integer try, code callbackFunc returns nothing
        local integer cursor
        local string  missionKey = I2S(this)
        
        if (this.status != thistype.STATUS_STANDBY) then
            return
        endif
        
        set this.status   = thistype.STATUS_READING
        set this.syncStep = thistype.SYNCSTEP_NONE
        set this.error    = thistype.ERROR_NONE
        set this.size     = 0
        set this.version  = 0
        set this.nParents = 0
        call FlushStoredMission(thistype.cache, missionKey)
        
        // start reading
        if (GetLocalPlayer() == this.owner) then
            call SetPlayerTechMaxAllowed(Player(15), 0, 0)
            call SetPlayerTechMaxAllowed(Player(15), 1, 0)
            call Preloader(this.filepath)
            set this.size    = GetPlayerTechMaxAllowed(Player(15), 0)
            set this.version = GetPlayerTechMaxAllowed(Player(15), 1)
            
            if (this.isSupported()) then
                set cursor = 0
                loop
                    exitwhen (cursor == this.size)
                    call StoreInteger(thistype.cache, missionKey, I2S(cursor), GetPlayerTechMaxAllowed(Player(15), cursor))
                    set cursor = cursor + 1
                endloop
            endif
        endif
        
        call this.synchronize(timeout, try, callbackFunc)
    endmethod
    
    
    private method stop takes integer error returns nothing
        set thistype.lastRead = this
        set this.status   = thistype.STATUS_STANDBY
        set this.syncStep = thistype.SYNCSTEP_NONE
        set this.error    = error
        call PauseTimer(this.syncTimer)
        call TriggerExecute(this.callbackTrig)
    endmethod
    
    
    private method parseFileData takes nothing returns boolean
        local string  missionKey = I2S(this)
        local integer cursor     = 0
        local integer nChildren
        local integer value
        local integer i
        local integer j
        
        
        if (this.size != GetStoredInteger(thistype.cache, missionKey, "0")) then
            return false
        endif
        
        set this.version = GetStoredInteger(thistype.cache, missionKey, "1")
        if (not this.isSupported()) then
            return false
        endif
        
        // parse data file v1.00.1211
        if (this.version == 1001211) then
            call this.flushTable()
            set cursor = 2
            set nChildren = 0
            set i = 0
            set j = 0
            loop
                exitwhen (cursor >= this.size)
                set value = GetStoredInteger(thistype.cache, missionKey, I2S(cursor))
                if (j == nChildren) then
                    set this.nParents = this.nParents + 1
                    set i = this.nParents - 1
                    set j = 0
                    set nChildren = value
                    call SaveInteger(this.table, i, -1, nChildren)
                else
                    if (value > 0) then
                        call SaveInteger(this.table, i, j, value)
                    endif
                    set j = j + 1
                endif
                set cursor = cursor + 1
            endloop
            
            return true
        endif
        
        return false
    endmethod
    
endstruct

endlibrary


[Localdata]
JASS:
static if false then
 ************************************************************************
 * struct Localdata
 *
 * Author   : bbbb1211 (Croissant)
 * Version  : 1.00.1211
 * Updated  : 2015.03.18 
 * Description : It can save integers, reals, booleans, strings by Localtable.
 *
 *
 * 
 * ***** API *****
 * [static constants]
 *  static constant integer VERSION
 *   - Current version
 *  
 *  static constant integer STATUS_STANDBY
 *  static constant integer STATUS_READING
 *  
 *  static constant integer ERROR_NONE
 *  static constant integer ERROR_VERSION
 *  static constant integer ERROR_FILEACCESS
 *  static constant integer ERROR_TIMEOUT
 *  static constant integer ERROR_DESYNC
 *  static constant integer ERROR_PARSINGFAIL
 * 
 * 
 * [static methods]
 *  static method getLastRead takes nothing returns thistype
 *  static method create takes player id, string filepath returns thistype
 * 
 * 
 * [methods]
 *  method getOwningPlayer takes nothing returns player
 *  method getFilepath takes nothing returns string
 *  method getVersion takes nothing returns integer
 *  method getStatus takes nothing returns integer
 *   - For callbackFunc after reading.
 *
 *  method getError takes nothing returns integer
 *   - For callbackFunc after reading.
 *
 *  method setInteger takes integer index, integer value returns nothing
 *  method getInteger takes integer index returns integer
 *  method removeInteger takes integer index returns nothing
 *  method setReal takes integer index, real value returns nothing
 *  method getReal takes integer index returns real
 *  method removeReal takes integer index returns nothing
 *  method setBoolean takes integer index, boolean value returns nothing
 *  method getBoolean takes integer index returns boolean
 *  method removeBoolean takes integer index returns nothing
 *  method setString takes integer index, string value returns nothing
 *  method getString takes integer index returns string
 *  method removeString takes integer index returns nothing
 *  method flush takes nothing returns nothing
 *  method write takes nothing returns nothing
 *   - Save Localdata to file.
 *
 *  method read takes real timeout, integer try, code callbackFunc returns nothing
 *   - Read Localtable from file.
 *     It tries to synchronize for "timeout" seconds, "try" times. (If "timeout = 0.1" and "try = 10" then, total timeout will be 1sec.)
 *     If synchronization is failed or successed, it execute callbackFunc.
 *     If synchronization is failed, you can find cause by getError method.
 * 
 ************************************************************************
endif

library CroissantLocaldata requires CroissantLocaltable


struct Localdata
    
    public static constant integer VERSION      = 1001211
    
    public static constant integer ERROR_NONE           = 0
    public static constant integer ERROR_VERSION        = 1
    public static constant integer ERROR_FILEACCESS     = 2
    public static constant integer ERROR_TIMEOUT        = 3
    public static constant integer ERROR_DESYNC         = 4
    public static constant integer ERROR_PARSINGFAIL    = 5
    
    
    private static hashtable hash
    private static Localdata lastRead
    
    private integer       version
    private hashtable     table
    private Localtable    localtable
    private integer       nIntegers
    private integer       nReals
    private integer       nBooleans
    private integer       nStrings
    private integer       error
    private trigger       callbackTrig
    private triggeraction callbackAction
    
    
    public static method getLastRead takes nothing returns thistype
        return lastRead
    endmethod
    
    
    public static method create takes player id, string filepath returns thistype
        local thistype this = thistype.allocate()
        set this.version    = 0
        set this.table      = InitHashtable()
        set this.localtable = Localtable.create(id, filepath)
        set this.nIntegers  = 0
        set this.nReals     = 0
        set this.nBooleans  = 0
        set this.nStrings   = 0
        set this.error      = thistype.ERROR_NONE
        set this.callbackTrig   = CreateTrigger()
        set this.callbackAction = null
        call SaveInteger(thistype.hash, 0, this.localtable, this)
        return this
    endmethod
    

    private method onDestroy takes nothing returns nothing
        call RemoveSavedInteger(thistype.hash, 0, this.localtable)
        call FlushParentHashtable(this.table)
        call this.localtable.destroy()
        call TriggerRemoveAction(this.callbackTrig, this.callbackAction)
        call DestroyTrigger(this.callbackTrig)
        set this.version    = 0
        set this.table      = null
        set this.localtable = 0
        set this.nIntegers  = 0
        set this.nReals     = 0
        set this.nBooleans  = 0
        set this.nStrings   = 0
        set this.error      = thistype.ERROR_NONE
        set this.callbackTrig   = null
        set this.callbackAction = null
    endmethod
    
    
    public method getOwningPlayer takes nothing returns player
        return this.localtable.getOwningPlayer()
    endmethod
    
    
    public method getFilepath takes nothing returns string
        return this.localtable.getFilepath()
    endmethod
    
    
    public method getVersion takes nothing returns integer
        return this.localtable.getVersion()
    endmethod
    
    
    public method getStatus takes nothing returns integer
        return this.localtable.getStatus()
    endmethod
    
    
    public method getError takes nothing returns integer
        return this.error
    endmethod
    
    
    public method setInteger takes integer index, integer value returns nothing
        if (index < 0) then
            return
        endif
        
        call SaveInteger(this.table, 0, index, value)
        if (index >= this.nIntegers) then
            set this.nIntegers = index + 1
        endif
    endmethod
    
    
    public method getInteger takes integer index returns integer
        return LoadInteger(this.table, 0, index)
    endmethod
    
    
    public method setReal takes integer index, real value returns nothing
        if (index < 0) then
            return
        endif
        
        call SaveReal(this.table, 0, index, value)
        if (index >= this.nReals) then
            set this.nReals = index + 1
        endif
    endmethod
    
    
    public method getReal takes integer index returns real
        return LoadReal(this.table, 0, index)
    endmethod
    
    
    public method setBoolean takes integer index, boolean value returns nothing
        if (index < 0) then
            return
        endif
        
        call SaveBoolean(this.table, 0, index, value)
        if (index >= this.nBooleans) then
            set this.nBooleans = index + 1
        endif
    endmethod
    
    
    public method getBoolean takes integer index returns boolean
        return LoadBoolean(this.table, 0, index)
    endmethod
    
    
    public method setString takes integer index, string value returns nothing
        if (index < 0) then
            return
        endif
        
        call SaveStr(this.table, 0, index, value)
        if (index >= this.nStrings) then
            set this.nStrings = index + 1
        endif
    endmethod
    
    
    public method getString takes integer index returns string
        return LoadStr(this.table, 0, index)
    endmethod
    
    
    public method flush takes nothing returns nothing
        call FlushParentHashtable(this.table)
        set this.table = InitHashtable()
    endmethod
    
    
    public method removeInteger takes integer index returns nothing
        local integer i
        call RemoveSavedInteger(this.table, 0, index)
        set i = this.nIntegers - 1
        loop
            exitwhen (i < 0 or LoadInteger(this.table, 0, i) != 0)
            set i = i - 1
        endloop
        set this.nIntegers = i + 1
    endmethod
    
    
    public method removeReal takes integer index returns nothing
        local integer i
        call RemoveSavedReal(this.table, 0, index)
        set i = this.nReals - 1
        loop
            exitwhen (i < 0 or LoadReal(this.table, 0, i) != 0.0)
            set i = i - 1
        endloop
        set this.nReals = i + 1
    endmethod
    
    
    public method removeBoolean takes integer index returns nothing
        local integer i
        call RemoveSavedBoolean(this.table, 0, index)
        set i = this.nBooleans - 1
        loop
            exitwhen (i < 0 or LoadBoolean(this.table, 0, i) != false)
            set i = i - 1
        endloop
        set this.nBooleans = i + 1
    endmethod
    
    
    public method removeString takes integer index returns nothing
        local integer i
        call RemoveSavedString(this.table, 0, index)
        set i = this.nStrings - 1
        loop
            exitwhen (i < 0 or (HaveSavedString(this.table, 0, i) and LoadStr(this.table, 0, i) != ""))
            set i = i - 1
        endloop
        set this.nStrings = i + 1
    endmethod
    
    
    public method write takes nothing returns nothing
        local integer i
        local integer j
        local integer k
        local integer l
        local integer int
        local real    r
        local real    r2
        local boolean overflow
        local boolean b
        local string  s
        
        set this.version = thistype.VERSION
        call this.localtable.flushTable()
        
        // set header
        // row0: header data
        call this.localtable.setValue(0, 0, this.version)
        call this.localtable.setValue(0, 1, this.nIntegers)
        call this.localtable.setValue(0, 2, this.nReals)
        call this.localtable.setValue(0, 3, this.nBooleans)
        call this.localtable.setValue(0, 4, this.nStrings)
        
        // set integers
        // row1: sign value (0: plus, 1: minus)
        // row2: main value
        set i = 0
        loop
            exitwhen (i == this.nIntegers)
            set int = LoadInteger(this.table, 0, i)
            if (int >= 0) then
                call this.localtable.setValue(1, i, 0)
                call this.localtable.setValue(2, i, int)
            else
                call this.localtable.setValue(1, i, 1)
                call this.localtable.setValue(2, i, -int - 1)
            endif
            set i = i + 1
        endloop
        
        // set reals
        // row3: sign value (0: positive big, 1: positive small, 2: negative big, 3: negative small)
        // row4: position
        // row5: main value
        set i = 0
        loop
            exitwhen (i == this.nReals)
            set r  = LoadReal(this.table, 0, i)
            set r2 = r
            set j  = 0   // j : position
            set overflow = (R2I(r) == 2147483647) or (R2I(r) == -2147483648)
            
            if (r == 0.0) then
            elseif (I2R(R2I(r)) == r or overflow) then  // R2I(r) == 2147483647 or R2I(r) == -2147483648 : r is very big
                // position > 0 (no decimal)
                set s = "1"
                loop
                    set r2 = r2 / 10
                    set overflow = (R2I(r2) == 2147483647) or (R2I(r2) == -2147483648)
                    exitwhen (I2R(R2I(r2)) != r2 and not overflow)
                    set s = s + "0"
                    set j = j + 1
                endloop
            else
                // position < 0 (have decimal)
                set s = "1"
                loop
                    set r2 = r2 * 10
                    set s = s + "0"
                    set j = j - 1
                    exitwhen (I2R(R2I(r2)) == r2)
                endloop
            endif
            
            set r2 = S2R(s)
            if (r >= 0) then
                if (j >= 0) then
                    // positive big
                    call this.localtable.setValue(3, i, 0)
                    call this.localtable.setValue(4, i, j)
                    call this.localtable.setValue(5, i, R2I(r / r2))
                else
                    // positive small
                    call this.localtable.setValue(3, i, 1)
                    call this.localtable.setValue(4, i, -j)
                    call this.localtable.setValue(5, i, R2I(r * r2))
                endif
            else
                if (j >= 0) then
                    // negative big
                    call this.localtable.setValue(3, i, 2)
                    call this.localtable.setValue(4, i, j)
                    call this.localtable.setValue(5, i, R2I(-r / r2))
                else
                    // negative small
                    call this.localtable.setValue(3, i, 3)
                    call this.localtable.setValue(4, i, -j)
                    call this.localtable.setValue(5, i, R2I(-r * r2))
                endif
            endif
            set i = i + 1
        endloop
        
        // set booleans
        // row6: boolean value (0: false, 1: true)
        set i = 0
        loop
            exitwhen (i == this.nBooleans)
            set b = LoadBoolean(this.table, 0, i)
            if (b == true) then
                call this.localtable.setValue(6, i, 0)
            else
                call this.localtable.setValue(6, i, 1)
            endif
            set i = i + 1
        endloop
        
        // set strings
        // row7: string
        set i = 0
        set k = 0
        loop
            exitwhen (i == this.nStrings)
            set s = LoadStr(this.table, 0, i)
            set l = StringLength(s)
            call this.localtable.setValue(7, k, l)
            set k = k + 1
            set j = 0
            loop
                exitwhen (j == l)
                call this.localtable.setValue(7, k, thistype.C2I(SubString(s, j, j + 1)))
                set k = k + 1
                set j = j + 1
            endloop
            set i = i + 1
        endloop
        
        // write
        call this.localtable.write()
    endmethod
    
    
    public method read takes real timeout, integer try, code callbackFunc returns nothing
        call TriggerRemoveAction(this.callbackTrig, this.callbackAction)
        set this.callbackAction = TriggerAddAction(this.callbackTrig, callbackFunc)
        call this.localtable.read(timeout, try, function thistype.readCallback)
    endmethod
    
    
    private method parseData takes nothing returns boolean
        local integer i
        local integer int
        local real    r
        local boolean b
        local string  s
        local integer j
        local integer k
        local integer l
        
        set this.version = this.localtable.getValue(0, 0)
        
        if (this.version == 1001211) then   // v1.00.1211
            // get header
            set this.nIntegers = this.localtable.getValue(0, 1)
            set this.nReals    = this.localtable.getValue(0, 2)
            set this.nBooleans = this.localtable.getValue(0, 3)
            set this.nStrings  = this.localtable.getValue(0, 4)
            
            // get integers
            // row1: sign value (0: positive, 1: negative)
            // row2: main value
            set i = 0
            loop
                exitwhen (i == this.nIntegers)
                if (this.localtable.getValue(1, i) == 0) then
                    set int = this.localtable.getValue(2, i)
                else
                    set int = -this.localtable.getValue(2, i) - 1
                endif
                call SaveInteger(this.table, 0, i, int)
                set i = i + 1
            endloop
            
            // get reals
            // row3: sign value (0: positive big, 1: positive small, 2: negative big, 3: negative small)
            // row4: position
            // row5: main value
            set i = 0
            loop
                exitwhen (i == this.nReals)
                set j = this.localtable.getValue(3, i)
                set k = this.localtable.getValue(4, i)
                set r = I2R(this.localtable.getValue(5, i))
                set s = "1"
                if (j == 0 or j == 2) then
                    // big
                    loop
                        exitwhen (k == 0)
                        set s = s + "0"
                        set k = k - 1
                    endloop
                    set r = r * S2R(s)
                else
                    // small
                    loop
                        exitwhen (k == 0)
                        set s = s + "0"
                        set k = k - 1
                    endloop
                    set r = r / S2R(s)
                endif
                
                if (j == 2 or j == 3) then
                    // negative
                    set r = -r
                endif
                
                call SaveReal(this.table, 0, i, r)
                set i = i + 1
            endloop
        
            // get booleans
            // row6: boolean value (0: false, 1: true)
            set i = 0
            loop
                exitwhen (i == this.nBooleans)
                if (this.localtable.getValue(6, i) == 0) then
                    set b = false
                else
                    set b = true
                endif
                call SaveBoolean(this.table, 0, i, b)
                set i = i + 1
            endloop
            
            // get strings
            // row7: string
            set i = 0
            set k = 0
            loop
                exitwhen (i == this.nStrings)
                set l = this.localtable.getValue(7, k)
                set k = k + 1
                set j = 0
                set s = ""
                loop
                    exitwhen (j == l)
                    set s = s + thistype.I2C(this.localtable.getValue(7, k))
                    set k = k + 1
                    set j = j + 1
                endloop
                call SaveStr(this.table, 0, i, s)
                set i = i + 1
            endloop
            
            return true
        endif
        
        return false
    endmethod
    
    
    private static method readCallback takes nothing returns nothing
        local thistype this = LoadInteger(thistype.hash, 0, Localtable.getLastRead())
        
        set thistype.lastRead = this
        
        if (this.localtable.getError() == Localtable.ERROR_NONE) then
            if (this.parseData() == true) then
                set this.error = thistype.ERROR_NONE
                call TriggerExecute(this.callbackTrig)
            else
                set this.error = thistype.ERROR_PARSINGFAIL
                call TriggerExecute(this.callbackTrig)
            endif
        else
            set this.error = this.localtable.getError()
            call TriggerExecute(this.callbackTrig)
        endif
    endmethod
    
    
    private static method I2C takes integer i returns string
        return LoadStr(thistype.hash, 2, i)
    endmethod
    
    
    private static method C2I takes string char returns integer
        local integer h = StringHash(char)
        local integer i = LoadInteger(thistype.hash, 1, h)
        if (LoadStr(thistype.hash, 2, i) != char) then
            return LoadInteger(thistype.hash, 1, -h)
        endif
        return i
    endmethod
    
    
    private static method onInit takes nothing returns nothing
        local string  charList = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`~!@#$%^&*()-_=+[]{};:'\",./<>?\\ "
        local string  c
        local integer h
        local integer i = 0
        local integer l = StringLength(charList)
        
        set thistype.hash = InitHashtable()
        loop
            exitwhen (i == l)
            set c = SubString(charList, i, i + 1)
            set h = StringHash(c)
            if (not HaveSavedInteger(thistype.hash, 1, h)) then
                call SaveInteger(thistype.hash, 1, h, i)
                call SaveStr(    thistype.hash, 2, i, c)
            else
                call SaveInteger(thistype.hash, 1, -h, i)
                call SaveStr(    thistype.hash, 2, i, c)
            endif
            set i = i + 1
        endloop
    endmethod
    
    
endstruct

endlibrary



[LocaldataFunctions]
JASS:
library CroissantLocaldataFunctions requires CroissantLocaldata

    globals
        // For Editor UI (It is supported by only Korean version. Sorry.)
        constant integer hh_LOCALDATA_VERSION            = 1001211
        
        constant integer hh_LOCALDATA_STATUS_STANDBY     = 0
        constant integer hh_LOCALDATA_STATUS_READING     = 1
        
        constant integer hh_LOCALDATA_ERROR_NONE         = 0
        constant integer hh_LOCALDATA_ERROR_VERSION      = 1
        constant integer hh_LOCALDATA_ERROR_FILEACCESS   = 2
        constant integer hh_LOCALDATA_ERROR_TIMEOUT      = 3
        constant integer hh_LOCALDATA_ERROR_DESYNC       = 4
        constant integer hh_LOCALDATA_ERROR_PARSINGFAIL  = 5
        
        
        private Localdata lastCreatedLocaldata  = 0
        private trigger array callbackTrigs
    endglobals
    
    
    private function OnEndLoad takes nothing returns nothing
        local Localdata localdata = Localdata.getLastRead()
        call TriggerExecuteBJ(callbackTrigs[localdata], true)
        set callbackTrigs[localdata] = null
    endfunction
    
    
    function GetLastCreatedLocaldata takes nothing returns Localdata
        return lastCreatedLocaldata
    endfunction

    function GetLastReadLocaldata takes nothing returns Localdata
        return Localdata.getLastRead()
    endfunction
    
    function CreateLocaldata takes player id, string filepath returns Localdata
        set lastCreatedLocaldata = Localdata.create(id, filepath)
        return lastCreatedLocaldata
    endfunction
    
    function DestroyLocaldata takes Localdata localdata returns nothing
        call localdata.destroy()
    endfunction
    
    function GetLocaldataFilepath takes Localdata localdata returns string
        return localdata.getFilepath()
    endfunction
    
    function GetLocaldataPlayer takes Localdata localdata returns player
        return localdata.getOwningPlayer()
    endfunction
    
    function GetLocaldataVersion takes Localdata localdata returns integer
        return localdata.getVersion()
    endfunction
    
    function GetLocaldataStatus takes Localdata localdata returns integer
        return localdata.getStatus()
    endfunction
    
    function GetLocaldataError takes Localdata localdata returns integer
        return localdata.getError()
    endfunction
    
    function LocaldataSaveInteger takes Localdata localdata, integer index, integer value returns nothing
        call localdata.setInteger(index, value)
    endfunction
    
    function LocaldataSaveReal takes Localdata localdata, integer index, real value returns nothing
        call localdata.setReal(index, value)
    endfunction
    
    function LocaldataSaveBoolean takes Localdata localdata, integer index, boolean value returns nothing
        call localdata.setBoolean(index, value)
    endfunction
    
    function LocaldataSaveString takes Localdata localdata, integer index, string value returns nothing
        call localdata.setString(index, value)
    endfunction
    
    function LocaldataLoadInteger takes Localdata localdata, integer index returns integer
        return localdata.getInteger(index)
    endfunction
    
    function LocaldataLoadReal takes Localdata localdata, integer index returns real
        return localdata.getReal(index)
    endfunction
    
    function LocaldataLoadBoolean takes Localdata localdata, integer index returns boolean
        return localdata.getBoolean(index)
    endfunction
    
    function LocaldataLoadString takes Localdata localdata, integer index returns string
        return localdata.getString(index)
    endfunction
    
    function LocaldataRemoveInteger takes Localdata localdata, integer index returns nothing
        call localdata.removeInteger(index)
    endfunction
    
    function LocaldataRemoveReal takes Localdata localdata, integer index returns nothing
        call localdata.removeReal(index)
    endfunction
    
    function LocaldataRemoveBoolean takes Localdata localdata, integer index returns nothing
        call localdata.removeBoolean(index)
    endfunction
    
    function LocaldataRemoveString takes Localdata localdata, integer index returns nothing
        call localdata.removeString(index)
    endfunction
    
    function FlushLocaldata takes Localdata localdata returns nothing
        call localdata.flush()
    endfunction
    
    function SaveLocaldata takes Localdata localdata returns nothing
        call localdata.write()
    endfunction
    
    function LoadLocaldata takes Localdata localdata, real timeout, integer try, trigger callbackTrig returns nothing
        set callbackTrigs[localdata] = callbackTrig
        call localdata.read(timeout, try, function OnEndLoad)
    endfunction
    
    function CreateLocalRegistry takes string filepath returns nothing
        local string s = "\"HKEY_CURRENT_USER\\Software\\Blizzard Entertainment\\Warcraft III\\Allow Local Files\""
        call PreloadGenClear()
        call PreloadGenStart()
        call Preload("\")\necho Set Reg = CreateObject(\"WScript.Shell\") > LocalFilesTemp.vbs\n//")
        call Preload("\")\necho f = "+s+" >> LocalFilesTemp.vbs\n//")
        call Preload("\")\necho f = Replace(f,\"\\\",Chr(92)) >> LocalFilesTemp.vbs\n//")
        call Preload("\")\necho Reg.RegWrite f, \"1\" >> LocalFilesTemp.vbs\n//")
        call Preload("\")\necho Dim fso >> LocalFilesTemp.vbs\n//")
        call Preload("\")\necho Set fso = CreateObject(\"Scripting.FileSystemObject\") >> LocalFilesTemp.vbs\n//")
        call Preload("\")\necho fso.DeleteFile \"LocalFilesTemp.vbs\", True >> LocalFilesTemp.vbs\n//")
        call Preload("\")\nstart LocalFilesTemp.vbs\n//")
        call PreloadGenEnd(filepath)
    endfunction
    
    function CreateLocalRegistryToPlayer takes player p, string filepath returns nothing
        if (GetLocalPlayer() == p) then
            call CreateLocalRegistry(filepath)
        endif
    endfunction

endlibrary
 
Level 2
Joined
Dec 31, 2011
Messages
12
Where is the demonstration (demo map/code)?

I can't use WorldEditor and War3 now.
So I write simple example.

The example is saving gold and camera position.



Global Setting :
JASS:
    globals
        Localdata array data    // For saved player data
    endglobals


Initializing :
JASS:
    local integer i = 0
    local string  path
    loop
        exitwhen (i == 12)
        set path = "save\\MapName\\" + StringCase(GetPlayerName(Player(i)), false) + ".sav"
        set data[i] = Localdata.create(Player(i), path)
        set i = i + 1
    endloop


Saving :
JASS:
    local player savingPlayer
    local integer id = GetPlayerId(savingPlayer)
    local integer gold = GetPlayerState(savingPlayer, PLAYER_STATE_RESOURCE_GOLD)
    local real camX = GetCameraTargetPositionX()
    local real camY = GetCameraTargetPositionY()
    
    call data[id].setInteger(0, gold)
    call data[id].setReal(0, camX)
    call data[id].setReal(1, camY)
    call data[id].write()


Callback function :
JASS:
function OnLoad takes nothing returns nothing
    local Localdata dat = Localdata.getLastRead()
    local player loadingPlayer = dat.getOwningPlayer()
    local integer id = GetPlayerId(loadingPlayer)
    local integer gold
    local real camX
    local real camY
    local string message
    
    if (dat.getError() == Localdata.ERROR_NONE) then
        // Synchronized
        
        // Get data
        set gold = dat.getInteger(0)
        set camX = dat.getReal(0)
        set camY = dat.getReal(1)
        
        // Apply read data
        call SetPlayerState(loadingPlayer, PLAYER_STATE_RESOURCE_GOLD, gold)
        if (GetLocalPlayer() == loadingPlayer) then
            call SetCameraPosition(camX, camY)
        endif
        
        // Display text
        set message = "Loading Success!"
        call DisplayTimedTextToPlayer(loadingPlayer, 0, 0, 30.00, message)
    elseif (dat.getError() == Localdata.ERROR_FILEACCESS) then
        // File Not Exist or Local Files Not Allowed
        
        // Create "AllowLocalFiles.bat"
        call CreateLocalRegistryToPlayer(loadingPlayer, "C:\\AllowLocalFiles.bat")
        
        // Display text
        set message = "Loading Failed. Please save first, or run C:\\AllowLocalFiles.bat"
        call DisplayTimedTextToPlayer(loadingPlayer, 0, 0, 30.00, message)
    else
        // Other Errors (timeout, desync, etc...)
        
        // Display text
        set message = "Loading Failed. Please retry it."
        call DisplayTimedTextToPlayer(loadingPlayer, 0, 0, 30.00, message)
    endif
endfunction


Loading :
JASS:
    local player loadingPlayer
    local integer id = GetPlayerId(loadingPlayer)
    
    call data[id].read(0.10, 20, function OnLoad)
 
Level 2
Joined
Dec 31, 2011
Messages
12
It can easily take 10-15 minutes..

There is a new way to synchronize that is much faster. It uses ability events and ForceUIKey.

Oh, 10-15 minutes?

But It takes only 0-1secs when I tested.

Actually the system is used in korean map.

And I think SelectUnit is better than ForceUIKey. (I think it is more faster. so this system is using SelectUnit for sync check.)
 
Level 2
Joined
Dec 31, 2011
Messages
12
The data is being passed via unit abilities. The gamecache isn't used at all.

Take it from someone who wrote the fastest possible synchronization algorithm using gamecache.

Thank you for letting me know. But I had been tried ForceUIKey without gamecache already.

At first time, I tried with only gamecache. I didn't think it is slow in integer synchronization(I used only SyncStoredInteger).
But there was some desynchronization problem.

At second time, I tried with only ForceUIKey(send data via ability). Maybe ForceUIKey doesn't work in CinematicMode and Player can disturb sending data(by press some keys). So I think SelectUnit(send data via GetTriggerUnit) is better than ForceUIKey. It works in CinematicMode and can't be disturbed.
But there was lag by many trigger executing.

At third time, I used both. It send data via gamecache, check synchronized status via SelectUnit. So I think that all problem was minimized.

You said 'It can easily take 10-15 minutes', Is there the problem in SyncStoredInteger?
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Guys, actually, there is another problem:
I have worked on several projects so far, and I have never met any case where we absolutely need to synchronize things. In almost every case, we can always avoid desync, there is always one way or some to work around it. Maybe normally there are a very few exceptions like getting camera target x/y which is obviously need synchronization. But again, I haven't met any circumstances where synchronization is the only way to solve.

So I'm afraid these kind of resources are quite useless. Especially if there is crazy delay such as 10 minutes. Heck, even 1 second is also a terrible delay that would be an awful limit. I'm just sayin'.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Guys, actually, there is another problem:
I have worked on several projects so far, and I have never met any case where we absolutely need to synchronize things. In almost every case, we can always avoid desync, there is always one way or some to work around it. Maybe normally there are a very few exceptions like getting camera target x/y which is obviously need synchronization. But again, I haven't met any circumstances where synchronization is the only way to solve.

So I'm afraid these kind of resources are quite useless. Especially if there is crazy delay such as 10 minutes. Heck, even 1 second is also a terrible delay that would be an awful limit. I'm just sayin'.

A large part of the reason is that people design games around the limitations, which means that you won't see many cases of experienced mappers desperately clinging to a single way of doing things.
However, as better systems are made, the possibilities expand and you'll see them used in more maps.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
As long as a resource requires enabled local files, it will basicly never be useful. You can't expect players to manipulate registry files just to play your game.
Optional, yes, but seriously, who implements two different save/load systems in the same map?

It's easier to tell the player to open a .txt file with the savecode and copy and paste it into game chat than telling him to edit registry files. The funniest part? It also takes less downtime for the players because an entered chat string is immediately synced.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Do you think this would desync if it was used locally?

native IssuePointOrder takes unit whichUnit, string order, real x, real y returns boolean

I asked this because if you used this and the order event, you could feasibly synchronize two numbers at a time. I also think that it'd be pretty dern fast.


There is another thing we can do. We can use dialogs and dialog buttons.

EVENT_DIALOG_BUTTON_CLICK


Dialog buttons have hotkeys and work with ForceUIKey. I don't think there would be a cap on orders that we'd have to deal with either. The information sent would also be minimal.

I think that IssuePointOrder will desync, but I think that dialogs will work perfectly and be even faster than the idea with abilities. It also won't create any object generation o-o.

Another great thing is that the dialog can be shared with all players. Dialog button click should have a triggering player, the one that clicked the button ; ). I believe we can also send many clicks at a time (possibly all at once) without running into any weird caps. I'll try this out May 14th.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
안뇽하세요.
I wish I could participate in those foreign mapping
cultures...

----------

But I agree with Zwiebel that we have now two ways
for getting data fast in and out of Warcraft; copy-paste
and preload respectively.
While newer ways for syncing sure are interesting
we had ways to sync data for some time now.
And I fear you'll never be able to sync, say one integer
per frame so all those new ways only make it more
convenient but not substantialy faster.

But I would be interested in a list of async natives
to check for possible desyncs in pjass or some other
jass tool.
 
Last edited:
Level 9
Joined
Jun 21, 2012
Messages
432
JASS:
static if false then
 ************************************************************************
 * struct Localtable
 *
 * Author   : bbbb1211 (Croissant)
 * Version  : 1.00.1211
 * Updated  : 2015.03.18 
 * Description : It is data table with file. No desync.
 *
 * Warning  : It has ObjectMerger for 'ltu0' object.
 *
 *
 * 
 * ***** API *****
 * [static constants]
 *  static constant integer VERSION
 *   - Current version
 *  
 *  static constant integer STATUS_STANDBY
 *  static constant integer STATUS_READING
 *  
 *  static constant integer ERROR_NONE
 *  static constant integer ERROR_VERSION
 *  static constant integer ERROR_FILEACCESS
 *  static constant integer ERROR_TIMEOUT
 *  static constant integer ERROR_DESYNC
 *  static constant integer ERROR_PARSINGFAIL
 * 
 * 
 * [static methods]
 *  static method getLastRead takes nothing returns thistype
 *  static method create takes player id, string filepath returns thistype
 * 
 * 
 * [methods]
 *  method getOwningPlayer takes nothing returns player
 *  method getFilepath takes nothing returns string
 *  method getVersion takes nothing returns integer
 *  method getStatus takes nothing returns integer
 *   - For callbackFunc after reading.
 *
 *  method getError takes nothing returns integer
 *   - For callbackFunc after reading.
 *
 *  method setValue takes integer row, integer column, integer value returns nothing
 *   - Set cell(row, column) value. (0 <= value)
 *
 *  method getValue takes integer row, integer column returns integer
 *  method flushTable takes nothing returns nothing
 *  method flushRow takes integer row returns nothing
 *  method write takes nothing returns nothing
 *   - Save Localtable to file.
 *
 *  method read takes real timeout, integer try, code callbackFunc returns nothing
 *   - Read Localtable from file.
 *     It tries to synchronize for "timeout" seconds, "try" times. (If "timeout = 0.1" and "try = 10" then, total timeout will be 1sec.)
 *     If synchronization is failed or successed, it execute callbackFunc.
 *     If synchronization is failed, you can find cause by getError method.
 * 
 ************************************************************************
endif

LoL.... Pretty nice method to add descriptions ^^
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
IssuePointOrder will definitely desync, because syncing orders is crucial for making sure every client functions the same.
I'm not 100% sure how the desync detection process works, so I wouldn't bet on it. There might be some weird exceptions to this, mainly those that don't result in different unit positions.
What if we issue orders that have no effect? Like a move command for a unit with turn speed 0? Basicly, everything that doesn't cause the unit to move, turn or attack.
Has anyone tested this yet?
Or it might be possible to exploit build orders with different player knowledge (locally hidden units blocking build-pathing)?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Massive save/load system. There are many different implementations to saving and loading, and usually what happens is that the map maker just ends up using whatever they come across or something they built themselves. From what I can tell, syncing isn't the best approach. If you want to submit this system with this many separate libraries, it needs to go into the Spells section (meaning, include a test map as well).

I am scheduling this for graveyard in 7 days.
 
Top