• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Lua Save and Load 1.6

This bundle is marked as high quality. It exceeds standards and is highly desirable.
Description

This system combines the FileIO, Encoder62 and SyncStream in order to create the Save/Load system we all know and adore.

Requirements
Trokkin - FileIO - [Lua] - FileIO (Lua-optimized) (it's init is outdated, look the one below)
j
Lua:
if Debug then Debug.beginFile "FileIO" end
--[[
    FileIO v1a (Trokkin)

    Provides functionality to read and write files, optimized with lua functionality in mind.

    API:

        FileIO.Save(filename, data)
            - Write string data to a file

        FileIO.Load(filename) -> string?
            - Read string data from a file. Returns nil if file doesn't exist.

        FileIO.SaveAsserted(filename, data, onFail?) -> bool
            - Saves the file and checks that it was saved successfully.
              If it fails, passes (filename, data, loadResult) to onFail.

        FileIO.enabled : bool
            - field that indicates that files can be accessed correctly.

    Optional requirements:
        DebugUtils by Eikonium                          @ https://www.hiveworkshop.com/threads/330758/
        Total Initialization by Bribe                   @ https://www.hiveworkshop.com/threads/317099/

    Inspired by:
        - TriggerHappy's Codeless Save and Load         @ https://www.hiveworkshop.com/threads/278664/
        - ScrewTheTrees's Codeless Save/Sync concept    @ https://www.hiveworkshop.com/threads/325749/
        - Luashine's LUA variant of TH's FileIO         @ https://www.hiveworkshop.com/threads/307568/post-3519040
        - HerlySQR's LUA variant of TH's Save/Load      @ https://www.hiveworkshop.com/threads/331536/post-3565884

    Updated: 8 Mar 2023
--]]
OnInit.map("FileIO", function()
    local RAW_PREFIX = ']]i([['
    local RAW_SUFFIX = ']])--[['
    local RAW_SIZE = 256 - #RAW_PREFIX - #RAW_SUFFIX
    local LOAD_ABILITY = FourCC('ANdc')
    local LOAD_EMPTY_KEY = '!@#$, empty data'
    local name = nil ---@type string?

    local function open(filename)
        name = filename
        PreloadGenClear()
        Preload('")\nendfunction\n//!beginusercode\nlocal p={} local i=function(s) table.insert(p,s) end--[[')
    end

    local function write(s)
        for i = 1, #s, RAW_SIZE do
            Preload(RAW_PREFIX .. s:sub(i, i + RAW_SIZE - 1) .. RAW_SUFFIX)
        end
    end

    local function close()
        Preload(']]BlzSetAbilityTooltip(' .. LOAD_ABILITY .. ', table.concat(p), 0)\n//!endusercode\nfunction a takes nothing returns nothing\n//')
        PreloadGenEnd(name --[[@as string]])
        name = nil
    end

    ---
    ---@param filename string
    ---@param data string
    local function savefile(filename, data)
        open(filename)
        write(data)
        close()
    end

    ---@param filename string
    ---@return string?
    local function loadfile(filename)
        local s = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
        BlzSetAbilityTooltip(LOAD_ABILITY, LOAD_EMPTY_KEY, 0)
        Preloader(filename)
        local loaded = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
        BlzSetAbilityTooltip(LOAD_ABILITY, s, 0)
        if loaded == LOAD_EMPTY_KEY then
            return nil
        end
        return loaded
    end

    ---@param filename string
    ---@param data string
    ---@param onFail function?
    ---@return boolean
    local function saveAsserted(filename, data, onFail)
        savefile(filename, data)
        local res = loadfile(filename)
        if res == data then
            return true
        end
        if onFail then
            onFail(filename, data, res)
        end
        return false
    end

    local fileIO_enabled = saveAsserted('TestFileIO.pld', 'FileIO is Enabled')

    FileIO = {
        Save = savefile,
        Load = loadfile,
        SaveAsserted = saveAsserted,
        enabled = fileIO_enabled,
    }
end)
if Debug then Debug.endFile() end
Trokkin - SyncStream - [Lua] - SyncStream (I made a modified version from this, below))
Lua:
if Debug then Debug.beginFile "SyncStream" end
    --[[
    By Trokkin
    Provides functionality to designed to safely sync arbitrary amounts of data.
    Uses timers to spread BlzSendSyncData calls over time.
    Wrda's version - not using his encoder
    API:
    ---@param whichPlayer player
    ---@param getLocalData string | fun():string
    ---@param callBackFunctionName string
    function SyncStream.sync(whichPlayer, getLocalData, callBackFunctionName)
       - Adds getLocalData (string or function that returns string) to the queue to be synced. Once completed, fires the funcName function.
       - your callBackFunctionName function MUST take the synced string and the owner of the string as parameters.
    ]]
OnInit.module("Sync Stream", function()
    --CONFIGURATION
    local PREFIX = "Sync"
    local CHUNK_SIZE = 200              --string length per chunk
    local PACKAGE_PER_TICK = 32         --amount of packages per interval
    local PACKAGE_TICK_PER_SECOND = 32  --interval in which the syncing takes place
    local MAX_IDS = 999
    local MAX_CHUNKS = 99
    local DELIMITER = "!"               --function delimiter character
    --END CONFIGURATION 
    --internal
    local streams = {}
    ---Returns the function from the given string. Also works with functions within tables, the keys MUST be of string type.
    ---@param funcName string
    ---@return function
    local function getFunction(funcName)
        local f = _G
        for v in funcName:gmatch("[^\x25.]+") do
            f = f[v]
        end
        return type(f) == "function" and f or error("value is not a function")
    end

    ---@param maxNum number
    ---@param currentAmount number
    local function fillBlankDigits(maxNum, currentAmount)
        local digits = #tostring(maxNum) - #tostring(currentAmount)
        local blank = ""
        for i = 1, digits do
            blank = blank .. "0"
        end
        return blank
    end
    ---@class syncQueue
    ---@field id integer
    ---@field idLength integer
    ---@field length integer
    ---@field chunks string[]
    ---@field next_chunk integer
    ---@field callbackName string
    local syncQueue = {}
    syncQueue.__index = syncQueue
    ---@param id integer The id of the promise.
    ---@param data string Data to be sent from the local player.
    function syncQueue.create(id, data)
        local queue = setmetatable({
            id = id,
            chunks = {},
            next_chunk = 0,
            length = #data,
            callbackName = ""
        }, syncQueue)
        for i = 1, #data, CHUNK_SIZE do
            queue.chunks[#queue.chunks + 1] = data:sub(i, i + CHUNK_SIZE - 1)
        end
        if #queue.chunks > MAX_CHUNKS then
            error("WARNING: Max CHUNK digits reached!")
        end
        return queue
    end
    function syncQueue:pop()
        if self.next_chunk > #self.chunks then
            self = nil
            return
        end
        --assign id to chunk
        local idDigit0 = fillBlankDigits(MAX_IDS, self.id)
        local chunkDigit0 = fillBlankDigits(MAX_CHUNKS, self.next_chunk)
        local package = idDigit0 .. tostring(self.id) .. chunkDigit0 .. tostring(self.next_chunk)
        --print("SYNC POP")
        --print(self.id, self.next_chunk)
        if self.next_chunk == 0 then
            local maxChunkDigit0 = fillBlankDigits(MAX_CHUNKS, #self.chunks)
            package = DELIMITER .. self.callbackName .. DELIMITER .. package .. maxChunkDigit0 .. #self.chunks .. self.length
            --print("OVERALL PACKAGE LENGTH: " .. self.length)
            --print(package)
        else
            --print("THIS PACKAGE")
            package = package .. self.chunks[self.next_chunk]
        end
        -- print(">", self.next_chunk, package)
        if BlzSendSyncData(PREFIX, package) then
            self.next_chunk = self.next_chunk + 1
        end
    end
    --[ PROMISE CLASS ]--
    ---@class promise
    ---@field id integer
    ---@field length integer?
    ---@field next_chunk integer
    ---@field chunks string[]
    ---@field queue syncQueue?
    local promise = {}
    promise.__index = promise
    ---@param id integer The id of the promise.
    function promise.create(id)
        return setmetatable({
            id = id,
            chunks = {},
            next_chunk = 0,
            length = nil,
            queue = nil,
        }, promise)
    end
    
    function promise:consume(chunk_id, package)
        --print("prev: " .. self.next_chunk)
        if self.length and self.length <= (self.next_chunk - 1) * CHUNK_SIZE then
            return
        end
        -- print("<", chunk_id, package)
        self.chunks[chunk_id] = package
        while self.next_chunk <= chunk_id and self.chunks[self.next_chunk] ~= nil do
            self.next_chunk = self.next_chunk + 1
        end
        --print("now: " .. self.next_chunk)
        --new DISABLED
        --if self.length and self.length <= (self.next_chunk - 1) * CHUNK_SIZE then
        --    self.callback(table.concat(self.chunks), GetTriggerPlayer())
        --end
    end
    local syncTimer
    --[ SYNC STREAM CLASS ]--
    local syncTrigger ---@type trigger
    local localPlayer
    --- Sends or receives player's data assymentrically
    ---@class SyncStream
    ---@field owner player
    ---@field is_local boolean
    ---@field next_promise integer
    ---@field promises promise[]
    SyncStream = {}
    SyncStream.__index = SyncStream
    ---@param owner player The player owning the data from the stream
    local function CreateSyncStream(owner)
        return setmetatable({
            owner = owner,
            is_local = owner == localPlayer,
            next_promise = 1,
            promises = {}
        }, SyncStream)
    end
    ---Adds getLocalData (string or function that returns string) to the queue to be synced. Once completed, fires the callBackFunctionName function.
    ---your callBackFunctionName function MUST take the synced string and the owner of the string as parameters.
    ---@param whichPlayer player
    ---@param getLocalData string | fun():string
    ---@param callBackFunctionName string
    function SyncStream.sync(whichPlayer, getLocalData, callBackFunctionName)
        if not getLocalData then return end
        local self = streams[GetPlayerId(whichPlayer)]  ---@type SyncStream
        if #self.promises == MAX_IDS then
            error("WARNING: Max ID digits reached!")
            return
        end
        local promise = promise.create(#self.promises + 1)
        --print("created promise id:" .. promise.id)
        if self.is_local then
            if type(getLocalData) == "function" then
                getLocalData = getLocalData()
            end
            if type(getLocalData) ~= "string" then
                getLocalData = "sync error: bad data type provided " .. type(getLocalData)
            end
            promise.queue = syncQueue.create(promise.id, getLocalData)
            promise.queue.callbackName = callBackFunctionName
            --print("created queue id:" .. promise.queue.id)
        end
        self.promises[promise.id] = promise
    end
    OnInit.final(function()
        syncTimer = CreateTimer()
        localPlayer = GetLocalPlayer()
        local playerSyncedPromises = {}
        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            streams[i] = CreateSyncStream(Player(i))    ---@type SyncStream
            --new
            playerSyncedPromises[Player(i)] = {}
        end
        --- Setup sender timer
        local s = streams[GetPlayerId(GetLocalPlayer())]    ---@type SyncStream
        if not s.is_local then
            print("SyncStream panic: local stream is not local")
            return
        end
        TimerStart(syncTimer, 1 / PACKAGE_TICK_PER_SECOND, true, function()
            for i = 1, PACKAGE_PER_TICK do
                while s.next_promise <= #s.promises and s.promises[s.next_promise].queue == nil do
                    s.next_promise = s.next_promise + 1
                end
                if s.promises[s.next_promise] == nil then
                    return
                end
                local q = s.promises[s.next_promise].queue
                if q == nil then
                    return
                end
                --process sync queue
                q:pop()
                if q.next_chunk > #q.chunks then
                    s.promises[s.next_promise].queue = nil
                    s.promises[s.next_promise].queue = s.promises[#s.promises].queue
                    s.promises[#s.promises] = nil
                    s.next_promise = math.max(s.next_promise - 1, 1)
                end
            end
        end)
        --- Setup receiver trigger
        syncTrigger = CreateTrigger()
        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            BlzTriggerRegisterPlayerSyncEvent(syncTrigger, Player(i), PREFIX, false)
        end
        TriggerAddAction(syncTrigger, function()
            local owner = GetTriggerPlayer()
            local package = BlzGetTriggerSyncData()
            local stream = streams[GetPlayerId(owner)]
            if stream == nil then
                print("SyncStream panic: no stream found for player" .. GetPlayerName(owner) .. "but got 'nothing'")
                return
            end
            --print("START")
            --print(#package, package)
            local _, startPos, funcName = nil, 0, nil
            --check if string starts with the delimiter, then it's the first time the promise is getting synced
            --and the position will be adjusted.
            --if not, then default position is 1.
            if package:sub(1, 1):match(DELIMITER) then
                _, startPos, funcName = package:find( DELIMITER .. "(\x25a[\x25w_.]*)" .. DELIMITER)
            end
            local id = tonumber(string.sub(package, startPos + 1, startPos + #tostring(MAX_IDS)))
            local promise = stream.promises[id]
            --local chunk_id = promise.queue.id ignore this comment
            local chunk_id = tonumber(string.sub(package, startPos + #tostring(MAX_IDS) + 1 , startPos + #tostring(MAX_IDS) + #tostring(MAX_CHUNKS)))
            --new
            if chunk_id == 0 then
                local max_chunks = tonumber(string.sub(package, startPos + #tostring(MAX_IDS) + #tostring(MAX_CHUNKS) + 1 , startPos + #tostring(MAX_IDS) + #tostring(MAX_CHUNKS)*2))
                playerSyncedPromises[owner][id] = {}
                playerSyncedPromises[owner][id].maxChunks = max_chunks
                playerSyncedPromises[owner][id].callback = funcName
            else
                playerSyncedPromises[owner][id][chunk_id] = package:sub(#tostring(MAX_IDS) + #tostring(MAX_CHUNKS) + 1)
                if chunk_id == playerSyncedPromises[owner][id].maxChunks then
                    --execute callback, inputs data and player
                    getFunction(playerSyncedPromises[owner][id].callback)(table.concat(playerSyncedPromises[owner][id]), owner)
                    playerSyncedPromises[owner][id] = nil
                    return
                end
            end
            if not promise then
                --async area
                --triggers for player B when player A is getting synced data
                --print("SyncStream panic: no promise found for id", id)
                return
            end
            --print("CHUNK ID: ")
            --print(chunk_id)
            if chunk_id == 0 then
                if not promise.queue then return end
                promise.length = promise.queue.length or 0   --data_length
                promise.next_chunk = 1
                return
            end
            --print("CONSUME")
            --print(package:sub(#tostring(MAX_IDS) + #tostring(MAX_CHUNKS) + 1))
            promise:consume(chunk_id, package:sub(#tostring(MAX_IDS) + #tostring(MAX_CHUNKS) + 1))
        end)
    end)
end)
if Debug then Debug.endFile() end
Encoder62
Lua:
if Debug then Debug.beginFile "Encoder62" end
OnInit.root("Encoder62", function()
    -- Alphanumeric character set for Base62
    local base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    local fmod = math.fmod
    Base62 = {}
    ---Convert a number to a Base62 string
    ---@param num number
    ---@return string
    function Base62.toBase62(num)
        local base = 62
        local result = ""
   
        local isNegative = false
        if num < 0 then
            num = -num
            isNegative = true
        end
        repeat
            -- Get the remainder when dividing the number by Base62
            local remainder = fmod(num, base)
            -- Map the remainder to the corresponding Base62 character
            result = base62Chars:sub(remainder + 1, remainder + 1) .. result
            -- Update the number (integer division by base)
            num = math.tointeger(num // base)
        until num == 0
        return isNegative and "-" .. result or result
    end
    ---Convert a Base62 string back to a number
    ---@param base62Str string
    ---@return number
    function Base62.fromBase62(base62Str)
        local base = 62
        local num = 0
        local isNegative = false
        if base62Str:sub(1, 1) == "-" then
            isNegative = true
            base62Str = base62Str:sub(2, base62Str:len())
        end
        for i = 1, #base62Str do
            -- Get the value of the current character in Base62
            local char = base62Str:sub(i, i)
            local value = base62Chars:find(char) - 1 -- find returns 1-based index
            -- Accumulate the value into the result
            num = num * base + value
        end
        return isNegative and -num or num
    end
end)
if Debug then Debug.endFile() end
Bribe - Total Initialization - [Lua] - Total Initialization

Features

  • Can save and load huge strings;
  • Long strings are loaded by splitting into smaller parts and then synchronize over time in a queue, ensuring no crashes that otherwise would happen if loading the huge string;
  • Isn't limited to integers representing other data;
  • Can load multiple files (like any other save load system?).
  • Ensures the retrieved values have the same data types as saved previously (this only includes primitive types)
  • Anti-tampering.
Installation

Method 1: Copy the requirements to your map. And download the main script:
Lua:
if Debug then Debug.beginFile "SaveLoadHelper" end
OnInit.module("SaveLoadHelper", function(require)
    require "Encoder62"
    require "FileIO"
--[[
    SaveLoadHelper version 1.3 by Wrda
    (Special thanks to Antares)
    This system is responsible in squeezing a player's data from a table and then retrieving it matching
    how it was saved in. For example, you save a player's data such as PlayerSave (table) which has the fields
    set to some value you set:
    kills; gems; lives; locationX; locationY
    Loading the file will result in a new table, with those exact same fields, and their respective values.
    There are different methods to save data, a table with string keys, as described above, and a table with
    indexed keys. Both have their strengths.
    WARNING: You can't use \x25 (percentage sign) on a string. The loading will fail.
    The workaround for this is to think of a character you'll never going to use and then replace it
    with \x25\x25 (double because it escapes the sign).
    Example: str = "takes 5< damage, 600< more hp"
    local result = string.gsub(str, "<", "\x25\x25")
    print(result)      -> "takes 5% damage, 600% more hp"
    API:
        ---@param playerName string
        ---@return string
        function SaveLoad.getDefaultPath(playerName)
            - Gets the default string format path.
        ---@param p player
        ---@param list table
        ---@param playerName string?
        ---@param filePath string?
        SaveLoad.saveHelperDynamic(p, list, playerName?, filePath?)
            - Saves data to a single player. "list" must be a string-key table.
            - If not given a playerName, it is saved with the current player name.
            - Returns the resulting table in key-indexed format.
        ---@param p player
        ---@param list table<integer, any>
        ---@param playerName string?
        ---@param filePath string?
        SaveLoad.saveHelperIndex(p, list, playerName?, filePath?)
            - Saves data to a single player. "list" must be an indexed-key table.
            - If not given a playerName, it is saved with the current player name.
            - Returns the "list" table, may be useful in when one uses SaveLoad.saveHelperDynamic because it
              calls SaveLoad.saveHelperIndex inside.
        ---@param data string
        ---@return table
        SaveLoad.loadHelperIndex(data)
            return loadDataIndex(data)
            - Loads data into a table. The table will have indexed keys.
        ---@param data string
        ---@return table<string, any>
        function SaveLoad.loadHelperDynamic(data)
            - Loads data into a table. The table will have string keys.     
    ]]
    SaveLoad = {}
--[[----------------------------------------------------------------------------------------------------
                            CONFIGURATION                                                             ]]
    SaveLoad.FOLDER = "TEST MAP"         -- Name of the folder. Not required, but serves as a default.
    SaveLoad.FILE_PREFIX = "TestCode-"  -- You can have none. Use empty string and NOT nil. Not required, but serves as a default.
    SaveLoad.FILE_SUFFIX = "-0"         -- You can have none. Use empty string and NOT nil. Not required, but serves as a default.
    SAVE_LOAD_SEED = 1                  -- This is used for generating a random permutation of the scrambled string. Set it to any integer unique for your map. You're not supposed to change your mind on this later on.
 
    ---Gets the default string format path.
    ---@param playerName string
    ---@return string
    function SaveLoad.getDefaultPath(playerName)
        return SaveLoad.FOLDER .. "\\" .. SaveLoad.FILE_PREFIX .. playerName .. SaveLoad.FILE_SUFFIX .. ".pld"
    end
    --------------------------------------------------------------------------------------------------------
    local pack = string.pack
    local unpack = string.unpack
    local byte = string.byte
    local pseudoRandomPermutation
    local delimiterList = {
        ["integer"] = "#",
        ["float"] = "_",
        ["string"] = "&",
        ["true"] = "!",
        ["false"] = "@",
        --reverse
        ["#"] = "integer",
        ["_"] = "float",
        ["&"] = "string",
        ["!"] = "true",
        ["@"] = "false"
    }
    ---@param value any
    local function getDelimiterType(value)
        local mathType = math.type(value)
        if delimiterList[mathType] then
            return delimiterList[mathType]
        elseif type(value) == "string" then
            return delimiterList[type(value)]
        elseif type(value) == "boolean" then
            return delimiterList[tostring(value)]
        else
            error("Unrecognized delimiter type.")
        end
        return nil
    end
    ---@param str string
    ---@param pos integer
    ---@return string|nil
    local function findDelimiterTypeIndex(str, pos)
        local found
        found = str:match("([#_&!@])\x25d+", pos)
        return found
    end
    ---@param str string
    ---@param pos integer
    ---@return string|nil
    local function findDelimiterTypeDynamic(str, pos)
        local found
        found = str:match("([#_&!@])\x25w+", pos)
        return found
    end
    --compress
    ---@param float number
    ---@return integer
    local function binaryFloat2Integer(float)
        return unpack("i4", pack("f", float))
    end
    ---@param integer integer
    ---@return number
    local function binaryInteger2Float(integer)
        return string.unpack("f", string.pack("i4", integer))
    end
    --validating parts of the file
    ---@param str string
    ---@return integer
    local function getCheckNumber(str)
        local checkNum = 0
        for i = 1, str:len() do
            checkNum = checkNum + byte(str:sub(i, i))
        end
        return checkNum
    end
    ---@param str string
    ---@return string
    local function addCheckNumber(str)
        return Base62.toBase62(getCheckNumber(str)) .. "-" .. str
    end
    ---@param str string
    ---@return string, boolean
    local function separateAndValidateCheckNumber(str)
        local separatedString = str:sub(str:find("-") + 1, str:len())
        return separatedString, getCheckNumber(separatedString) == Base62.fromBase62(str:sub(1, str:find("-") - 1))
    end
    ---@param str string
    ---@param seed integer
    ---@return string
    pseudoRandomPermutation = function(str, seed)
        local oldSeed = math.random(0, 2147483647)
        math.randomseed(seed)
 
        local chars = {}
        for i = 1, #str do
            table.insert(chars, str:sub(i, i))
        end
 
        for i = #chars, 2, -1 do
            local j = math.random(i)
            chars[i], chars[j] = chars[j], chars[i]
        end
 
        math.randomseed(oldSeed)
 
        return table.concat(chars)
    end
    --scrambler
    local chars = [[!#$&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~]]
    local scrambled = pseudoRandomPermutation(chars, SAVE_LOAD_SEED)
    local SCRAMBLED = {}
    local UNSCRAMBLED = {}
    for i = 1, chars:len() do
        SCRAMBLED[chars:sub(i, i)] = scrambled:sub(i, i)
        UNSCRAMBLED[scrambled:sub(i, i)] = chars:sub(i, i)
    end
    local function scrambleString(whichString)
        local scrambledString = ""
        for i = 1, whichString:len() do
            scrambledString = scrambledString .. (SCRAMBLED[whichString:sub(i, i)] or whichString:sub(i, i))
        end
        return scrambledString
    end
    local function unscrambleString(whichString)
        local unscrambledString = ""
        for i = 1, whichString:len() do
            unscrambledString = unscrambledString .. (UNSCRAMBLED[whichString:sub(i, i)] or whichString:sub(i, i))
        end
        return unscrambledString
    end
    local function convertToIndexedTable(dynamicTable)
        --you may use a table recycler here
        local indexedTable = {}
        for key, value in pairs(dynamicTable) do
            indexedTable[#indexedTable + 1] = key
            indexedTable[#indexedTable + 1] = value
        end
        return indexedTable
    end
    local function convertToDictionary(indexedTable)
        --you may use a table recycler here
        local dynamicTable = {}
        for i = 1, #indexedTable, 2 do
            dynamicTable[indexedTable[i]] = indexedTable[i + 1]
        end
        return dynamicTable
    end
    ---Saves data to a single player. "list" must be a string-key table.
    ---If not given a playerName, it is saved with the current player name.
    ---Returns the resulting table in key-indexed format.
    ---@param p player
    ---@param list table
    ---@param playerName string?
    ---@param filePath string?
    ---@return table
    function SaveLoad.saveHelperDynamic(p, list, playerName, filePath)
        local indexedTable = convertToIndexedTable(list)
        return SaveLoad.saveHelperIndex(p, indexedTable, playerName, filePath)
    end
    ---Saves data to a single player. "list" must be an indexed-key table.
    ---If not given a playerName, it is saved with the current player name.
    ---Returns the "list" table, may be useful in when one uses SaveLoad.saveHelperDynamic because it calls SaveLoad.saveHelperIndex inside.
    ---@param p player
    ---@param list table<integer, any>
    ---@param playerName string?
    ---@param filePath string?
    ---@return table
    function SaveLoad.saveHelperIndex(p, list, playerName, filePath)
        local data = ""
        local delimiterType
        local value
        for _, v in ipairs(list) do
            delimiterType = getDelimiterType(v)
            if delimiterList[delimiterType] == "float" then
                value = binaryFloat2Integer(v)
                value = Base62.toBase62(value)
            elseif delimiterList[delimiterType] == "integer" then
                value = Base62.toBase62(v)
            else
                value = tostring(v)
            end
            if type(v) == "boolean" then
                data = data .. delimiterType .. Base62.toBase62(0) .. delimiterType
            else
                data = data .. delimiterType .. Base62.toBase62(string.len(value)) .. delimiterType .. value
            end
        end
        data = addCheckNumber(data)
        local encData = scrambleString(data)
        if not playerName then
            playerName = GetPlayerName(p)
        end
        local path = type(filePath) == "string" and filePath or SaveLoad.getDefaultPath(playerName)
        if GetLocalPlayer() == p then
            FileIO.Save(path, encData)
        end
        return list
    end
 
    ---Loads data into a table. The table will have indexed keys.
    ---@param scrambledData string
    ---@return table<integer, any>|nil
    function SaveLoad.loadHelperIndex(scrambledData)
        local unscrambled = unscrambleString(scrambledData)
        local oldpos = 1
        local i = 1
        local data, isValid = separateAndValidateCheckNumber(unscrambled)
        if not isValid then
            --tampering detected
            return nil
        end
        local max = data:len()
        --you may use a table recycler here
        output = {}
        repeat
            local delType = findDelimiterTypeIndex(data, oldpos)
            local _, fin, length = data:find(delType .. "(\x25w+)" .. delType, oldpos) --\x25w+ because base62
            length = Base62.fromBase62(length)
            oldpos = fin + length + 1
            local value
            if length == 0 then     --boolean data always has 0 length
                value = (delimiterList[delType] == "true") and true or false
                goto skip
            else
                value = string.sub(data, fin + 1, length + fin)
            end
            if delimiterList[delType] == "float" then
                value = binaryInteger2Float(Base62.fromBase62(value))
            elseif delimiterList[delType] == "integer" then
                value = math.tointeger(Base62.fromBase62(value))
            end
            ::skip::    --skip if delimiter type was a boolean
            output[i] = value
            i = i + 1
        until oldpos >= max
        return output
    end
    ---Loads scrambledData into a table. The table will have string keys.
    ---@param scrambledData string
    ---@return table<string, any>|nil
    function SaveLoad.loadHelperDynamic(scrambledData)
        local unscrambled = unscrambleString(scrambledData)
        local oldpos = 1
        local i = 1
        local data, isValid = separateAndValidateCheckNumber(unscrambled)
        if not isValid then
            --tampering detected
            return nil
        end
        local max = data:len()
        --you may use a table recycler here
        output = {}
        repeat
            local delType = findDelimiterTypeDynamic(data, oldpos)
            local _, fin, length = data:find(delType .. "(\x25w+)" .. delType, oldpos) --\x25w+ because base62
            length = Base62.fromBase62(length)
            oldpos = fin + length + 1
            local value
            if length == 0 then     --boolean data always has 0 length
                value = (delimiterList[delType] == "true") and true or false
                goto skip
            else
                value = string.sub(data, fin + 1, length + fin)
            end
            if delimiterList[delType] == "float" then
                value = binaryInteger2Float(Base62.fromBase62(value))
            elseif delimiterList[delType] == "integer" then
                value = math.tointeger(Base62.fromBase62(value))
            end
            ::skip::    --skip if delimiter type was a boolean
            output[i] = value
            i = i + 1
        until oldpos >= max
        local dictionaryTable = convertToDictionary(output)
        --recycle the table if you have a table recycler
        output = nil
        return dictionaryTable
    end
end)
if Debug then Debug.endFile() end
Method 2: Download what's called "binaries" at the end of the resource page, you don't need the Test maps. Get Total Initialization from the requirements section (Insanity_AI's fix).
Method 3: Download one of the test maps, copy the "Libraries" folder to your map.
Total Initialization script should always be on top of all these other scripts (Below Debug Utils).
Always use the custom version of Total Initialization at Total Initialization (Insanity_AI's fix) to avoid nightmares and depression while coding.

How does it work?

You have 2 methods to save data, a table with string keys and a table with indexed keys. Both have their strengths. Loading the file will result in a new table, with those exact same keys, and their respective values. SaveLoad.saveHelperDynamic, SaveLoad.loadHelperDynamic and SaveLoad.saveHelperIndex, SaveLoad.loadHelperIndex.
If you're going to use SaveLoad.saveHelperIndex to save data on a file, then use SaveLoad.loadHelperIndex to load the same file. You can use a different method for a different file.

Firstly you have your data that you're going to keep track of, as in the test map example:
Lua:
if Debug then Debug.beginFile "Data" end
--creating data
OnInit.map(function()
    PlayerData = {}
    PlayerData2 = {}
    for i = 0, 23 do
        PlayerData[i] = {points = 0, gems = 0, negativeNumber = -1, EnteredRegion = false, longString = [[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]]} --1st method
        PlayerData2[i] = {0, 0, -1, false, [[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]]} --2nd method
    end
    local t = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DEATH)
    actions = function()
        local i = GetPlayerId(GetOwningPlayer(GetKillingUnit()))
        PlayerData[i].points = PlayerData[i].points + 0.25 --points index
        PlayerData[i].gems = PlayerData[i].gems + 1 --gems index
    end
    TriggerAddAction(t, actions)
    t = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_PICKUP_ITEM)
    function actions2()
        local i = GetPlayerId(GetTriggerPlayer())
        PlayerData2[i][1] = PlayerData2[i][1] + 0.25 --points index
        PlayerData2[i][2] = PlayerData2[i][2] + 1 --gems index
    end
    TriggerAddAction(t, actions2)
end)
if Debug then Debug.endFile() end
Kill footmen, award points, get the item, now you hold gems for some reason. PlayerData table is going to be used for the dynamic method, while PlayerData2 for the indexed one.
So to save, you call SaveLoad.saveHelperDynamic(p, PlayerData[id], GetPlayerName(p)). 3rd argument is optional, you may also use the 4th argument if you want to specify you path instead of using the default on the configuration part of
SaveLoadHelper script.
While SaveLoadHelper doesn't require SyncStream, you will need it to sync it after FileIO.Load:
Lua:
function Sync(totalChunk, player)
        local id = GetPlayerId(player)
        --finished syncing the whole file
        print(GetPlayerName(player) .. " has finished loading.")
        local myData = SaveLoad.loadHelperDynamic(totalChunk)
        --data decoded and retrieved
        --check if data was validated
        if not myData then
            print("NOOB CHEATER")
            return
        end
        --DO SOMETHING WITH IT: set player properties and etc.
        for key, v in pairs(myData) do
            print("VIEW VALUES:", key, v) --prints values
            PlayerData[id][key] = v   --the table that holds player stuff to save/load
        end
    end
    local function TypeChat()
        local str = GetEventPlayerChatString()
        local p = GetTriggerPlayer()
        local id = GetPlayerId(p)
        if str:sub(1, 5) == "-save" then
            --save the file
            SaveLoad.saveHelperDynamic(p, PlayerData[id], GetPlayerName(p))
        elseif str:sub(1, 5) == "-load" then
            local file
            if GetLocalPlayer() == p then
                if FileIO.Load(SaveLoad.getDefaultPath(GetPlayerName(p))) then
                    --load file
                    file = FileIO.Load(SaveLoad.getDefaultPath(GetPlayerName(p)))
                    DisplayTextToPlayer(p, 0, 0, "Loading...")
                else
                    DisplayTextToPlayer(p, 0, 0, "File doesn't exist.")
                end
            end
            --sync it
            SyncStream.sync(p, file, "Sync")
        elseif str:sub(1, 6) == "-nick " then     --change nickname for testing.
            SetPlayerName(p, str:sub(7, str:len()))
            PlayerData[id][1] = str:sub(7, str:len())
        end
    end
SyncStream.sync function takes the player who holds the data to sync with other players, following a string which is the player's data, and the last parameter is the name of the function that you want to execute when the syncing is done. The function you create MUST be a global variable. The parameter supports functions within a table.
You can further test by disabling the current enabled method and enable the other one on the example map, and to complete a "very difficult mission" to enter the rockish square terrain on your right on map 1, which tells you you need play the 2nd map first and enter the rockish square to trigger something back when you return to map 1. Useful for map transitions.
WARNING: The % character can't be loaded from the file, so don't attempt to use it in save load. The workaround is to think of a character you'll never use and then replace all of those characters with \x25\x25.
Example:
Lua:
str = "takes 5< damage, 600< more hp"
local result = string.gsub(str, "<", "\x25\x25")
print(result)    --prints "takes 5% damage, 600% more hp"

Special thanks to Antares ChatGPT for finding techniques for string compression, anti-tampering and such 😎

Changelog
1.6 - Minor update: SyncStream.sync returns early if the string happens to be nil.

1.5 - SyncStream.sync function's last parameter (string of function name) now supports functions within a table, with string keys only. Fixed a deindexing problem on SyncStream library, which would cause the system to stop functioning if there were more than 99 SyncStream.sync calls on the same player

1.4 - Simplified the syncing API and documented a possible undesired error.

1.3 - Something went wrong with SyncStream before, now it's fully working.

1.2 - Fixed an issue that negative values weren't possible to save due to an oversight on Base62 script. New function
SaveLoad.getDefaultPath that acccess the default path for the saved data, and loading it. Fixed an issue with not being able to save some specific special characters as keys such as _ since they are being used as delimiters by the system.

1.1 - the retrieved values now have the same data types as saved previously (this only includes primitive types).
better encoder, string compression, and anti-tampering mechanism.

1.0 - Inital Release.
Contents

Encoder (Binary)

File IO (Binary)

Save Load and Encoder Map (Map)

Save Load and Encoder Map 2 (Map)

SaveLoadHelper (Binary)

SyncStream (Binary)

Reviews
Antares
After a long and arduous journey, the Lua Save and Load has finally arrived! This system lets you store any primitve types and isn't limited (mostly) in the amount of data you can store. It achieves a reasonable compression and is integrated into a...

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
1,993
Update: Fixed an issue that negative values weren't possible to save due to an oversight on Base62 script; New function
SaveLoad.getDefaultPath that acccess the default path for the saved data, and loading it; Fixed an issue with not being able to save some specific special characters as keys such as _ since they are being used as delimiters by the system.

Previous update that wasn't mentioned, but it's on changelog:
1. the retrieved values now have the same data types as saved previously (this only includes primitive types).
2. better encoder, string compression, and anti-tampering mechanism.
 
After a long and arduous journey, the Lua Save and Load has finally arrived!

This system lets you store any primitve types and isn't limited (mostly) in the amount of data you can store. It achieves a reasonable compression and is integrated into a syncing library to allow it to be easily used for multiplayer maps.

Finally, Lua has achieved total supremacy!

High Quality
 

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
1,993
Update: SyncStream.sync function's last parameter (string of function name) now supports functions within a table, with string keys only. Fixed a deindexing problem on SyncStream library, which would cause the system to stop functioning if there were more than 99 SyncStream.sync calls on the same player, and probably more weirdness.
 
Last edited:
Level 8
Joined
Sep 16, 2016
Messages
226
Just imported this, gonna tinker with it in the future when I have data to save and load in my RPG map :)

Hopefully its good :infl_thumbs_up:

edit: Running into a problem when starting the game, did I import wrongly, or bad hierarchy?

1732586853675.png
1732586878965.png
 
Last edited:
The OnInit.module initializer that the library uses checks if a library that is being declared is actually used. Since you didn't use it yet, it raises an error. This is done so you know which libraries you have imported are unused you you can get rid of them.

But I question the sanity of anyone who thinks it's a good idea to use this feature the way it is implemented right now. There's no reason for it to raise an error when a warning would completely suffice. Hopefully it can be changed or deprecated in the future.
 
Level 8
Joined
Sep 16, 2016
Messages
226
You can change it to OnInit.main if it troubles you too much :)
The problem if you have too many OnInit.modules scripts is that it fills up the Chat Log with possibly some other legit warnings that broke your code on top on these module init errors.
I'll change it to OnInit.main for now, then to module the day I'll use it :) Or is it safe to have it OnInit.main and keep it there forever?
 
Level 8
Joined
Sep 16, 2016
Messages
226
I have to say, I'm really enjoying this library. Nevertheless, I do have some questions, like if I load, but then save, then load again, it doesn't seem like my data "loaded" again from that save, do I have to implement a "repick" system, like some RPG games have? On the other hand, Loading, then save, then restarting game, then load, my data do get updated.

I am also not sure if my code is good, but I tried my best with my limited lua knowledge and using the documentation to aid me, but this is what I have so far to store player kill count:

Lua:
   function createDatatable(player)

       return {Kills = 0, Gold = 0, Player = GetPlayerName(player)}

   end

   PlayerData = MDTable.create(1, createDatatable, true)

Lua:
if Debug and Debug.beginFile then Debug.beginFile("SaveLoad") end
OnInit.final("SaveLoad", function()
    function Sync(totalChunk, player)
        print(GetPlayerName(player) .. " has finished loading.")
        local myData = SaveLoad.loadHelperDynamic(totalChunk)
        if not myData then
            print("Validation failed")
            return
        end
        for key, v in pairs(myData) do
            print("Key", key, "Value", v)
            PlayerData[player][key] = v
        end
    end
    local function SaveLoadInit()
        trig = CreateTrigger()
        for i = 0, 9 do
            TriggerRegisterPlayerChatEvent(trig, Player(i), "-save", true)
            TriggerRegisterPlayerChatEvent(trig, Player(i), "-load", true)
        end
        TriggerAddAction(trig, function()
            player = GetTriggerPlayer()
            if GetEventPlayerChatString() == "-save" then
                print("Saved")
                SaveLoad.saveHelperDynamic(player, PlayerData[player], GetPlayerName(player))
            elseif GetEventPlayerChatString() == "-load" then
                local file
                if GetLocalPlayer() == player then
                    if FileIO.Load(SaveLoad.getDefaultPath(GetPlayerName(player))) then
                        file = FileIO.Load(SaveLoad.getDefaultPath(GetPlayerName(player)))
                        DisplayTextToPlayer(player, 0, 0, "Loading...")
                    else
                        DisplayTextToPlayer(player, 0, 0, "File doesn't exist.")
                    end
                end
                SyncStream.sync(player, file, "Sync")
            end
        end)
    end
    SaveLoadInit()
end)
if Debug and Debug.endFile then Debug.endFile() end

Lua:
if Debug and Debug.beginFile then Debug.beginFile("PlayerData") end
OnInit.final("PlayerData", function()
    trig = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DEATH)
   -- I have < 100 because all creeps will have PointValue below 100, and 100 and up is for heroes, although 100 is just a placeholder for now
    TriggerAddCondition(trig, Condition(function() if GetUnitPointValue(GetDyingUnit()) < 100 then return true else return false end end))
    TriggerAddAction(trig, function ()
        Player = GetOwningPlayer(GetKillingUnit())
        PlayerData[Player].Kills = PlayerData[Player].Kills + 1
        print("Kills: ", PlayerData[Player].Kills)
    end)
end)
if Debug and Debug.endFile then Debug.endFile() end

Also, does SAVE_LOAD_SEED integer matter in terms of "size"? like is SAVE_LOAD_SEED = 2 just as "good" as SAVE_LOAD_SEED = 52734845? Or can it be any number, but make sure I never change it again when I decided on a number for it? Unless I want saves to be broken?
 
Last edited:

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
1,993
I have to say, I'm really enjoying this library. Nevertheless, I do have some questions, like if I load, but then save, then load again, it doesn't seem like my data "loaded" again from that save, do I have to implement a "repick" system, like some RPG games have? On the other hand, Loading, then save, then restarting game, then load, my data do get updated.
Unfortunately you can only load once per file per game session. (Game session being the moment you start the map and get defeated or leave). It's a blizzard issue. However you saving repeatedly DOES work. Not sure what that repick system would be exactly for, but you could allow players to save their progress by saving in different file names such as modifying the SaveLoad.FILE_SUFFIX (locally) to the number of times the player has saved and then let them type -load # where # is the load slot. or no longer use the default path if you're going for more complicated set ups depending on your use SaveLoad.saveHelperDynamic(player, PlayerData[player], SaveLoad.FOLDER .. "\\" .. SaveLoad.FILE_PREFIX .. playerName .. GetHeroName(hero):lower() .. SavedAmountTimes[player] .. ".pld)
I am also not sure if my code is good, but I tried my best with my limited lua knowledge and using the documentation to aid me, but this is what I have so far to store player kill count:

Lua:
   function createDatatable(player)

       return {Kills = 0, Gold = 0, Player = GetPlayerName(player)}

   end

   PlayerData = MDTable.create(1, createDatatable, true)

Lua:
if Debug and Debug.beginFile then Debug.beginFile("SaveLoad") end
OnInit.final("SaveLoad", function()
    function Sync(totalChunk, player)
        print(GetPlayerName(player) .. " has finished loading.")
        local myData = SaveLoad.loadHelperDynamic(totalChunk)
        if not myData then
            print("Validation failed")
            return
        end
        for key, v in pairs(myData) do
            print("Key", key, "Value", v)
            PlayerData[player][key] = v
        end
    end
    local function SaveLoadInit()
        trig = CreateTrigger()
        for i = 0, 9 do
            TriggerRegisterPlayerChatEvent(trig, Player(i), "-save", true)
            TriggerRegisterPlayerChatEvent(trig, Player(i), "-load", true)
        end
        TriggerAddAction(trig, function()
            player = GetTriggerPlayer()
            if GetEventPlayerChatString() == "-save" then
                print("Saved")
                SaveLoad.saveHelperDynamic(player, PlayerData[player], GetPlayerName(player))
            elseif GetEventPlayerChatString() == "-load" then
                local file
                if GetLocalPlayer() == player then
                    if FileIO.Load(SaveLoad.getDefaultPath(GetPlayerName(player))) then
                        file = FileIO.Load(SaveLoad.getDefaultPath(GetPlayerName(player)))
                        DisplayTextToPlayer(player, 0, 0, "Loading...")
                    else
                        DisplayTextToPlayer(player, 0, 0, "File doesn't exist.")
                    end
                end
                SyncStream.sync(player, file, "Sync")
            end
        end)
    end
    SaveLoadInit()
end)
if Debug and Debug.endFile then Debug.endFile() end
Seems just fine to me :D
Lua:
if Debug and Debug.beginFile then Debug.beginFile("PlayerData") end
OnInit.final("PlayerData", function()
    trig = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DEATH)
   -- I have < 100 because all creeps will have PointValue below 100, and 100 and up is for heroes, although 100 is just a placeholder for now
    TriggerAddCondition(trig, Condition(function() if GetUnitPointValue(GetDyingUnit()) < 100 then return true else return false end end))
    TriggerAddAction(trig, function ()
        Player = GetOwningPlayer(GetKillingUnit())
        PlayerData[Player].Kills = PlayerData[Player].Kills + 1
        print("Kills: ", PlayerData[Player].Kills)
    end)
end)
if Debug and Debug.endFile then Debug.endFile() end

Also, does SAVE_LOAD_SEED integer matter in terms of "size"? like is SAVE_LOAD_SEED = 2 just as "good" as SAVE_LOAD_SEED = 52734845? Or can it be any number, but make sure I never change it again when I decided on a number for it? Unless I want saves to be broken?
It really doesn't matter, can be any number. Changing it will be a code wipe on everyone's saves indeed.
It can be useful if more maps use this same save system, but have different SAVE_LOAD_SEED so one couldn't just use their save file on your map, but the chances that even having the same value and it loading on your map is so minimal that it would require extreme effort to yank it to work 😜
 
Level 8
Joined
Sep 16, 2016
Messages
226
Unfortunately you can only load once per file per game session. (Game session being the moment you start the map and get defeated or leave). It's a blizzard issue. However you saving repeatedly DOES work. Not sure what that repick system would be exactly for, but you could allow players to save their progress by saving in different file names such as modifying the SaveLoad.FILE_SUFFIX (locally) to the number of times the player has saved and then let them type -load # where # is the load slot. or no longer use the default path if you're going for more complicated set ups depending on your use SaveLoad.saveHelperDynamic(player, PlayerData[player], SaveLoad.FOLDER .. "\\" .. SaveLoad.FILE_PREFIX .. playerName .. GetHeroName(hero):lower() .. SavedAmountTimes[player] .. ".pld)

Seems just fine to me :D

It really doesn't matter, can be any number. Changing it will be a code wipe on everyone's saves indeed.
It can be useful if more maps use this same save system, but have different SAVE_LOAD_SEED so one couldn't just use their save file on your map, but the chances that even having the same value and it loading on your map is so minimal that it would require extreme effort to yank it to work 😜
Ohh I understand, thanks :D

Regarding the repick system thing, lets say its an RPG map and you pick a Warrior, you level it from 1 to 200 and then saves the character. Afterwards you repick and chooses a Mage this time, and level it from 1 to 100, then saves this one as well (both the Warrior and Mage should be saved, since saving repeatedly does work).

Here comes the question, you now "-load" and get the option to pick Warrior (Lv. 200) or Mage (Lv. 100) to continue on. Does this work since its the first load of the game session? What if you continue leveling the Warrior from 200 to 300 this time, save it then repicks, and then lastly "-load", you will get the lvl 200 Warrior in this case? Theoretically it would be the 2nd "-load" you do this game session.

Unfortunately you can only load once per file per game session.
What if I delete the file, and re-make it? Even with the same file-name? Or perhaps move the old savefile to a folder "Backup", and then create the new savefile?
 
Last edited:

Uncle

Warcraft Moderator
Level 70
Joined
Aug 10, 2018
Messages
7,391
Ohh I understand, thanks :D

Regarding the repick system thing, lets say its an RPG map and you pick a Warrior, you level it from 1 to 200 and then saves the character. Afterwards you repick and chooses a Mage this time, and level it from 1 to 100, then saves this one as well (both the Warrior and Mage should be saved, since saving repeatedly does work).

Here comes the question, you now "-load" and get the option to pick Warrior (Lv. 200) or Mage (Lv. 100) to continue on. Does this work since its the first load of the game session? What if you continue leveling the Warrior from 200 to 300 this time, save it then repicks, and then lastly "-load", you will get the lvl 200 Warrior in this case? Theoretically it would be the 2nd "-load" you do this game session.


What if I delete the file, and re-make it? Even with the same file-name? Or perhaps move the old savefile to a folder "Backup", and then create the new savefile?
You should already have all of that data stored in that game session, so there's no need to use the system to Load anything. Just reference the variables directly instead of interfacing with the system.
 
Level 8
Joined
Sep 16, 2016
Messages
226
You should already have all of that data stored in that game session, so there's no need to use the system to Load anything. Just reference the variables directly instead of interfacing with the system.
Actually you're right I should have thought of that, thank you. :)
 

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
1,993
Yeah what Uncle said, ideally you'd have something like PlayerData[player].Warrior.somedata etc. So when you "load" the second time then you can input the values again easily.
What if I delete the file, and re-make it? Even with the same file-name? Or perhaps move the old savefile to a folder "Backup", and then create the new savefile?
No idea. "Moving" and creating the new save file should be doable I guess.
 
Level 8
Joined
Sep 16, 2016
Messages
226
Yeah what Uncle said, ideally you'd have something like PlayerData[player].Warrior.somedata etc. So when you "load" the second time then you can input the values again easily.

No idea. "Moving" and creating the new save file should be doable I guess.
Yep, I know of a way of doing what I want to do, thanks for making this library/system, I really love it.
 
Top