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

[Script] A Lua save/load system similar to TriggerHappy's GUI-friendly system

Level 4
Joined
Jul 31, 2019
Messages
19
Awesome, I'll check it out now. One thing I did notice is that you're using Player objects as the [index] in your tables. Isn't this prone to desyncs since the handle of the Player will be different for each client? I know this is an issue with Unit objects and as such requires you to stick with something like Unit Indexing. I could be wrong but I know my map still has some random desyncs and I'm pretty sure the issue lies in what I just described.

Edit: I've talked to some others and they said it was fine. It appears to only have to do with using these tables inside of a Pairs function, in which case there's a safe synced version available to use.
That's good to know, haven't experienced any desyncs yet but also haven't put it through the wringer.
 
Level 13
Joined
Jul 12, 2018
Messages
510
Just wanted to say that i'm willing to help test or otherwise use my time or pay if necessary to help move this along. i have zero coding skills but i'm in the same boat as Ravecharmer, i have a map that really needs this and it's now Lua.

i met @Trokkin on BNet and made a deal with him to set it up for me, but while i was disabling my 2k triggers for Lua conversion he went offline and hasn't been back for about 20 days now. i've continued working on the map and turning it back to Jass would be a big hassle at this point. if it turns into a month i'll open it up to anyone who's interested in the same deal and offer a few hundred dollars or euros for a customized system with different values saved at various times depending on game mode. the loading can be done all at once at map init or when a player tries to load a hero. i'll do my gui interactions of course.


I would like to chip in on the payment for anyone who could solve this, as I need it in my map.
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,537
Just wanted to say that i'm willing to help test or otherwise use my time or pay if necessary to help move this along. i have zero coding skills but i'm in the same boat as Ravecharmer, i have a map that really needs this and it's now Lua.

i met @Trokkin on BNet and made a deal with him to set it up for me, but while i was disabling my 2k triggers for Lua conversion he went offline and hasn't been back for about 20 days now. i've continued working on the map and turning it back to Jass would be a big hassle at this point. if it turns into a month i'll open it up to anyone who's interested in the same deal and offer a few hundred dollars or euros for a customized system with different values saved at various times depending on game mode. the loading can be done all at once at map init or when a player tries to load a hero. i'll do my gui interactions of course.
Trokkin's system apparently works fine but I can't say for certain, my friend (Ravecharmer) was the one who tested it and I believe it was only with 2 people. I still have to look into Rhobox's system some more.

Anyway, Trokkin's system is pretty simple to setup, just follow his examples and replace the data that he was saving with your own. If you want it to be more GUI friendly then replace his save/load variables with GUI versions.

This loads all of the players:
  • Custom script: PlayerState.Load()
This saves all of the players:
  • Custom script: PlayerState.Save()
Then the only code you need to tinker with is in the PlayerState file.
Here's a modified version I'm using to save and load my GUI variables for MMR, Wins, Losses, and Games Played:
Lua:
OnInit("PlayerState", function()
    local internal = {}
    PlayerState = { Internal = internal }


    internal.SYNC_PREFIX = "P"
    internal.SyncTrigger = CreateTrigger()

    local playingCount = 0

    local maxPlayers = 24 -- SET THE MAX PLAYERS

    for i = 0, maxPlayers - 1 do
        local p = Player(i)
        local id = GetPlayerId(p)
        --- BNet tag calculation
        local name = GetPlayerName(p)
        local bnetName, tag = name:match('([^#]+)#(%%d+)')
        if bnetName then
            bnetName = bnetName
            tag = tonumber(tag)
        else
            bnetName = name
            tag = 0
        end
        PlayerState[id] = {
            name = bnetName,
            bnetTag = tag
        }
        if GetPlayerController(p) == MAP_CONTROL_USER
            and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING then
            playingCount = playingCount + 1
            PlayerState[id].data = {
                game_count = 1,
                run_count = 0,
            }
        end
        BlzTriggerRegisterPlayerSyncEvent(
            PlayerState.Internal.SyncTrigger,
            p,
            PlayerState.Internal.SYNC_PREFIX,
            false)
    end

    PlayerState.Multiplayer = true -- playingCount > 1

    internal.SaveLocation = "PutYourMapNameHere.pld"
    internal.saveInfo = {
        key = Object64.Field.String.new('key', 6),
        playerName = Object64.Field.String.new('player', 32),
    }
    internal.CURRENT_VERSION = 1
    internal.MAP_KEY = "rG!B@n"
 
    TriggerAddAction(internal.SyncTrigger, function()
        local p = GetTriggerPlayer()
        local id = GetPlayerId(p)
        local state = PlayerState[id]
        local data = BlzGetTriggerSyncData()
        local decoder = Base64.Decoder.create(data)
        local version = decoder:readBitString(8)
        local key = PlayerState.Internal.saveInfo.key:decode(decoder)
        local playerName = PlayerState.Internal.saveInfo.playerName:decode(decoder)
        local tag = decoder:readBitString(14)
 

        -- LOAD !!!

        local pn = id + 1 -- Get their GUI player number
        udg_MMR[pn] = decoder:readBitString(24)
        udg_MMR_Wins[pn] = decoder:readBitString(24)
        udg_MMR_Losses[pn] = decoder:readBitString(24)
        udg_MMR_GamesPlayed[pn] = decoder:readBitString(24)


        if not PlayerState.Multiplayer then
            print("You are playing in singleplayer; your progress will not be saved.")
        end
    end)

    function PlayerState.Save()
        if not PlayerState.Multiplayer then return end
        local p = GetLocalPlayer()
        local id = GetPlayerId(p)
        local state = PlayerState[id]
        local encoder = Base64.Encoder.create()
        encoder:writeBitString(PlayerState.Internal.CURRENT_VERSION, 8)
        PlayerState.Internal.saveInfo.key:encode(encoder, PlayerState.Internal.MAP_KEY)
        PlayerState.Internal.saveInfo.playerName:encode(encoder, state.name)
        encoder:writeBitString(state.bnetTag, 14)


        -- SAVE !!!

        local pn = id + 1 -- Get their GUI player number
        encoder:writeBitString(udg_MMR[pn], 24)
        encoder:writeBitString(udg_MMR_Wins[pn], 24)
        encoder:writeBitString(udg_MMR_Losses[pn], 24)
        encoder:writeBitString(udg_MMR_GamesPlayed[pn], 24)


        local data = encoder:buildString()
        FileIO.Save(PlayerState.Internal.SaveLocation, data)
    end

    function PlayerState.Load()
        local p = GetLocalPlayer()
        local id = GetPlayerId(p)
        local state = PlayerState[id]
        local data = FileIO.Load(PlayerState.Internal.SaveLocation)
        --print(("Read %%s <- %%s"):format(data, PlayerState.Internal.SaveLocation))
        if not data then return end
        local decoder = Base64.Decoder.create(data)
        local version = decoder:readBitString(8)
        if version > PlayerState.Internal.CURRENT_VERSION then
            --print('Bad savefile: was created on a newer version')
            return
        end
        local key = PlayerState.Internal.saveInfo.key:decode(decoder)
        if key ~= PlayerState.Internal.MAP_KEY then
            --print('Bad savefile: corrupted data (1)')
            return
        end
        local playerName = PlayerState.Internal.saveInfo.playerName:decode(decoder)
        if playerName ~= PlayerState[id].name then
            --print('Bad savefile: corrupted data (2)')
            return
        end
        local tag = decoder:readBitString(14)
        if tag ~= state.bnetTag then
            --print('Bad savefile: corrupted data (3)')
            return
        end
        --print('File is valid, syncing...')
        BlzSendSyncData(PlayerState.Internal.SYNC_PREFIX, data)
    end
 
    function PlayerState.OnNewRun()
        -- ForForce(playing, function()
        --     local data = pState[GetPlayerId(GetEnumPlayer())].data
        --     data.run_count = data.run_count + 1
        -- end)
    end
end)
Just look for the part that says SAVE !!! and LOAD !!!, that's where you can see me saving/loading my GUI variables that I mentioned earlier.

Also, there's a line that says PlayerState.Multiplayer = true, this should be set to false if you're playing singleplayer to prevent people from cheating. But worry about that later.

So for example, here's me loading everyone's data and then displaying Player 1's data in text messages:
  • Custom script: PlayerState.Load()
  • Custom script: print(udg_MMR[1])
  • Custom script: print(udg_MMR_Wins[1])
  • Custom script: print(udg_MMR_Losses[1])
  • Custom script: print(udg_MMR_GamesPlayed[1])
Let's say Player 1 just won the game, I can update the save files like so:
  • Set Variable MMR_Wins[1] = MMR_Wins[1] + 1
  • Custom script: PlayerState.Save()
I can do all of this for you if you'd like. Here's my Discord: uncle#4859
You'll need to be on the latest patch though.
 
Last edited:
Level 4
Joined
Jul 31, 2019
Messages
19
Yeah I've had mine load the Bee Movie Script with three people across North America and Europe, but had to cut it down so the save file was only ~50kb. This is quite a lot larger than necessary.

It's not gui compatible right now though largely because I pretty much never touch gui.

This function here is the dummy, can replace it to handle any updates to load state after load syncing.

Lua:
--- This is the command that is used elsewhere to handle loaded data i.e. setting multiboards or unit stats
--- It hooks into the load sync with this function defined as a global variable
function HandleLoadData(loadData, player)
    PlayerData[player] = loadData
    for i, v in pairs(loadData) do
       print(i, v)
    end
end

This is an example save state for a player, PlayerData is the lua table reserved for handling player state for saves

Lua:
PlayerData[Player(i)] = {
    MMR = math.random(500, 1500),
    MMR_Wins = wins,
    MMR_Losses = losses,
    MMR_GamesPlayed = games
}

Haven't experienced any desyncs related to saving or loading with this system in multiplayer yet, up to 3 players testing
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,537
Would making it GUI "compatible" be as simple as this?
Lua:
local id = i + 1 -- GUI player number
PlayerData[Player(i)] = {
    MMR = udg_MMR[id],
    MMR_Wins = udg_MMR_Wins[id],
    MMR_Losses = udg_MMR_Losses[id],
    MMR_GamesPlayed = udg_MMR_GamesPlayed[id]
}
Then when you load these PlayerData.variables you additionally Set the GUI variables to match them:
Lua:
function HandleLoadData(loadData, player)
    PlayerData[player] = loadData

    local id = GetPlayerId(player) + 1 -- GUI player number
    udg_MMR[id] = loadData[1]
    udg_MMR_Wins[id] = loadData[2]
    udg_MMR_Losses[id] = loadData[3]
    udg_MMR_GamesPlayed[id] = loadData[4]
end
I assume the loadData maintains it's original save order?
 
Last edited:
Level 4
Joined
Jul 31, 2019
Messages
19
@Uncle has done a bunch of testing which led to me doing a bunch of bugfixing.
Here's a version that is in a workable state, with what we need so far. Attaching to GUI is manual but doable, and all data is saved by storing a table as a string and loading it back up.
If arrays are stored they will lose their index and be loaded with generated sequential indexes (Any numerical key in a table is lost and generated, as the lua load command won't accept it).
 

Attachments

  • LuaCodelessSaveLoad.w3m
    114.2 KB · Views: 10

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,537
Oh nice I never got around to that, thanks for taking the initiative.
I know it's been a while, but there are some issues with the current implementation of the save/load system that cause desyncs. I don't think the solution is all that difficult but I'm failing miserably to get anything to work.

Using a Player object as an [index] in a table causes a desync. This is done throughout all of the code.

Iterating over pairs causes a desync. I was doing this myself in my own save/load code to get the different save file names from PlayerData.

If you could help with this that'd be amazing, thanks.
 
Level 6
Joined
Mar 27, 2019
Messages
51
I know it's been a while, but there are some issues with the current implementation of the save/load system that cause desyncs. I don't think the solution is all that difficult but I'm failing miserably to get anything to work.

Using a Player object as an [index] in a table causes a desync. This is done throughout all of the code.

Iterating over pairs causes a desync. I was doing this myself in my own save/load code to get the different save file names from PlayerData.

If you could help with this that'd be amazing, thanks.
I planned to use Rhobox's version with your GUI Friendly system, how do I prevent the desyncs or does a better Save/Load System exist that doesn't desync with LUA?
 
Top