- Joined
- Jan 3, 2022
- Messages
- 498
Leading by a hardcoded example you here are. Replace it with the global max_slots constant you must. Only I know not which one.--creating data
OnInit.map(function()
PlayerData = {}
PlayerData2 = {}
for i = 0, 23 do
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
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
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
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
SaveLoad.saveHelperDynamic, SaveLoad.loadHelperDynamic and SaveLoad.saveHelperIndex, SaveLoad.loadHelperIndex.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.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
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 ofFileIO.Load: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
str = "takes 5< damage, 600< more hp"
local result = string.gsub(str, "<", "\x25\x25")
print(result) --prints "takes 5% damage, 600% more hp"
SyncStream.sync returns early if the string happens to be nil.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_ since they are being used as delimiters by the system.