Repository navigation
Expand file tree
/
Copy pathSafeSaveLoad.lua
More file actions
286 lines (242 loc) · 8.3 KB
/
Copy pathSafeSaveLoad.lua
File metadata and controls
286 lines (242 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
-- for safe files
local SafeSaveLoad = {}
-- checks if a table is a list
-- a list has consecutive numeric keys starting from 1 and ending at the table's length
local function isList(tbl)
local n = #tbl
for i, v in pairs(tbl) do
if type(i) ~= "number" -- [i] must be a number
or i ~= math.floor(i) -- [i] must be an integer
or i < 1 or i > n then -- [i] must be within the range 1 to n
return false
end
end
return true
end
-- checks if a table is a list of numbers
-- returns false if the table is not a list or contains non-number values
local function isNumberList(tbl)
if isList(tbl) then
for key, value in ipairs(tbl) do
if type(value) ~= "number" then -- [value] must be a number
return false
end
end
return true
end
end
-- checks if a table is a list of strings
-- returns false if the table is not a list or contains non-string values
local function isStringList(tbl)
if isList(tbl) then
for key, value in ipairs(tbl) do
if type(value) ~= "string" then -- [value] must be a string
return false
end
end
return true
end
end
local function isEmptyList(tbl)
if isList(tbl) and (#tbl == 0) then
return true
end
end
-- checks if a table is a list of numbers and serializes it into a space-separated string
-- returns the serialized string if the table is valid, otherwise returns nil
local function serializeNumberList(tbl)
if isNumberList(tbl) then -- check if the table is a list of numbers
return table.concat(tbl, " ") -- join all numbers with spaces
end
return nil -- return nil if the table is not a valid number list
end
-- serializes a table into a formatted string representation
-- supports string and number keys, as well as string, number, and nested table values
-- unsupported key or value types will raise an error
function SafeSaveLoad.serializeTable(tabl, level)
-- print ('SafeSaveLoad.serializeTable:', 'start')
level = level or 0
local result = {}
local indent = string.rep(" ", level)
if isEmptyList(tabl) then
table.insert(result, indent .. "emptyList")
return table.concat(result, "\n")
end
-- check if the table is a list of numbers
if isNumberList(tabl) then
-- print ('serializeTable: level '..level .. ' was numbers list')
-- handle ListNumbers type
table.insert(result, indent .. "start numbers list")
table.insert(result, indent .. table.concat(tabl, " "))
table.insert(result, indent .. "end numbers list")
return table.concat(result, "\n")
end
-- check if the table is a list of strings
if isStringList(tabl) then
-- handle ListStrings type
table.insert(result, indent .. "start strings list")
table.insert(result, table.concat(tabl, '\n'))
table.insert(result, indent .. "end strings list")
return table.concat(result, "\n")
end
-- append the start of a table marker
table.insert(result, indent .. "start table")
for key, value in pairs(tabl) do
local typeKey = type(key)
local typeValue = type(value)
if (typeKey == 'number') or (typeKey == 'string') then
-- if typeValue == "table" and isEmptyList(value) then
-- -- stringIndex numberIndex
-- table.insert(result, indent .. typeKey.."Index")
-- table.insert(result, indent .. key) -- Index (key)
-- table.insert(result, indent .. typeValue.."Value")
-- table.insert(result, indent .. ' ' .. 'emptyList')
-- elseif typeValue == "table" then
if typeValue == "table" then
table.insert(result, indent .. typeKey.."Index")
table.insert(result, indent .. key) -- Index (key)
-- tableValue numberValue booleanValue stringValue
table.insert(result, indent .. typeValue.."Value")
table.insert(result, SafeSaveLoad.serializeTable(value, level + 1)) -- recursively serialize
elseif (typeValue == 'string') or (typeValue == 'number') then
table.insert(result, indent .. typeKey.."Index")
table.insert(result, indent .. key)
table.insert(result, indent .. typeValue.."Value")
table.insert(result, indent .. value)
elseif (typeValue == 'boolean')then
table.insert(result, indent .. typeKey.."Index")
table.insert(result, indent .. key)
table.insert(result, indent .. typeValue.."Value")
table.insert(result, indent .. tostring(value))
else
error ('value type [' .. typeValue .. '] not supported')
end
else
error ('key type [' .. typeKey .. '] not supported')
end
end
table.insert(result, indent .. "end table")
-- print ('SafeSaveLoad.serializeTable:', 'end')
return table.concat(result, "\n")
end
-- new
-- deserializes a formatted string representation back into a Lua table
-- supports nested tables with string and number keys and values
-- maintains the same structure as the serialized table
-- deserializes a formatted string representation back into a Lua table
-- supports nested tables with string and number keys and values
-- maintains the same structure as the serialized table
function SafeSaveLoad.deserializeString(str)
-- [input string with serialized data]
local stack = {} -- stack for nested tables
local namesStack = {} -- stack for nested tables
local currentTable -- current table we are working with
local tempList
local tempIndex
local state = 'tableValue'
for line in str:gmatch("(.-)\n") do
line = line:match("^%s*(.-)%s*$") -- remove leading/trailing spaces
-- print (line)
-- state machine logic
-- print ('current state:', state)
if state == 'tableValue' then
if line == 'emptyList' then
currentTable[tempIndex] = {}
-- print ('added empty list: '..tempIndex)
state = 'indexType'
elseif line == 'start table' then
local newTable = {}
table.insert(stack, newTable) -- push current table to stack
if #namesStack == 0 then
table.insert(namesStack, 'root')
-- print ('created root table')
else
table.insert(namesStack, 'tempIndex')
-- print ('created ['..tempIndex..'] table')
end
if currentTable and tempIndex then
currentTable[tempIndex] = newTable
end
currentTable = newTable
-- print ('new table, #stack:', #stack)
state = 'indexType'
elseif line == 'start numbers list' then
tempList = {} -- reset content
state = 'listNumbers'
elseif line == 'start strings list' then
tempList = {} -- reset content
state = 'listStrings'
else
print ('SafeSaveLoad.deserializeString', 'comment:')
print ('################')
print (line)
print ('################')
end
elseif state == 'indexType' then
-- print ('State: indexType', 'line: ' .. line)
if line == 'end table' then
table.remove(stack) -- pop current table from stack
table.remove(namesStack)
-- print ('stack removed! #stack:', #stack)
if #namesStack > 0 then
-- print ('back to table: '.. namesStack[#namesStack])
end
currentTable = stack[#stack] -- restore previous table
else
state = line -- 'numberIndex' or 'stringIndex'
end
elseif state == 'numberIndex' then
tempIndex = tonumber(line)
state = 'valueType' -- waiting for value type
elseif state == 'stringIndex' then
tempIndex = line
state = 'valueType'
elseif state == 'valueType' then
state = line -- 'numberValue' or 'stringValue'
elseif state == 'numberValue' then
currentTable[tempIndex] = tonumber(line)
tempIndex = nil
state = 'indexType' -- or end of table!
elseif state == 'stringValue' then
currentTable[tempIndex] = line
tempIndex = nil
state = 'indexType' -- or end of table!
elseif state == 'booleanValue' then
-- print (line, tostring(line == 'true'))
currentTable[tempIndex] = (line == 'true')
tempIndex = nil
state = 'indexType' -- or end of table!
-- new states:
elseif state == 'listNumbers' then
if line == 'end numbers list' then
currentTable[tempIndex] = tempList
tempList = nil
state = 'indexType'
else -- line or multiline values
for num in line:gmatch("([%-?%d%.]+)") do
table.insert(tempList, tonumber(num))
end
end
elseif state == 'listStrings' then
if line == 'end strings list' then
currentTable[tempIndex] = tempList
tempList = nil
state = 'indexType'
else -- line or multiline values
table.insert(tempList, line)
end
else
error ('not state:' .. state .. '; line: ' .. line)
end
end
--[[
print ('')
print ('currentTable:')
for i, v in pairs (currentTable) do
print (i, v)
end
print ('end of currentTable')
--]]
return currentTable -- return the deserialized table
end
return SafeSaveLoad