Skip to content

Commit 4059491

Browse files
committed
ESSM: Fix luamod-related serialization issues.
- Fix serialization and deserialization failing after a mod has been reloaded using luamod. - Log error when we are unable to serialize or deserialize ESSM save, instead of failing silently.
1 parent 6b5ae58 commit 4059491

File tree

3 files changed

+171
-55
lines changed

3 files changed

+171
-55
lines changed

repentogon/resources/scripts/main_ex.lua

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,70 @@ REPENTOGON = {
77

88
collectgarbage("generational")
99

10+
--- Each module is a { module, loaded } pair
11+
local s_modules = {}
12+
13+
local debug_getinfo = debug.getinfo
14+
15+
local function cleanTraceback(level) -- similar to debug.traceback but breaks at xpcall, uses spaces instead of tabs, and doesn't call local functions upvalues
16+
level = level + 1
17+
local msg = "Stack Traceback:\n"
18+
while true do
19+
local info = debug_getinfo(level, "Sln")
20+
if not info then break end
21+
if info.what == "C" then
22+
if info.name == "xpcall" then break end
23+
msg = msg .. " in method " .. tostring(info.name) .. "\n"
24+
else
25+
if info.name then
26+
msg = msg ..
27+
" " ..
28+
tostring(info.short_src) ..
29+
":" .. tostring(info.currentline) .. ": in function '" .. tostring(info.name) .. "'\n"
30+
else
31+
msg = msg ..
32+
" " ..
33+
tostring(info.short_src) ..
34+
":" .. tostring(info.currentline) .. ": in function at line " .. tostring(info.linedefined) .. "\n"
35+
end
36+
end
37+
38+
level = level + 1
39+
end
40+
41+
return msg
42+
end
43+
44+
local errorHandler = function(msg)
45+
return msg .. "\n" .. cleanTraceback(2)
46+
end
47+
48+
---@param modName string
49+
function _GetModule(modName)
50+
local cachedMod = s_modules[modName]
51+
if cachedMod then
52+
if not cachedMod[2] then
53+
error(string.format("[ERROR] [REPENTOGON] module \"%s\" caused a recursive dependency", modName), 0) -- use level 0 to not print a source line
54+
end
55+
56+
return cachedMod[1]
57+
end
58+
59+
cachedMod = {nil, false}
60+
s_modules[modName] = cachedMod
61+
local ok, mod = xpcall(include, errorHandler, modName)
62+
if not ok then
63+
s_modules[modName] = nil
64+
error(string.format("[ERROR] [REPENTOGON] Failed to load module \"%s\": %s", modName, mod), 0) -- use level 0 to not print a source line
65+
end
66+
67+
cachedMod[1] = mod
68+
cachedMod[2] = true
69+
return mod
70+
end
71+
1072
local RaiseModError = _CBindings.RaiseModError
73+
local ModManager = _GetModule("repentogon_extras.mod_manager")
1174

1275
local function MeetsVersion(targetVersion)
1376
if (REPENTOGON.Version == "dev build") then return true end
@@ -39,8 +102,6 @@ end
39102

40103
REPENTOGON.MeetsVersion = MeetsVersion;
41104

42-
local debug_getinfo = debug.getinfo
43-
44105
local callbackIDToName = {}
45106
for k, v in pairs(ModCallbacks) do
46107
callbackIDToName[v] = k
@@ -663,36 +724,6 @@ local err_dupecount = 1
663724
---@type string[]
664725
local printedWarnings = {}
665726

666-
local function cleanTraceback(level) -- similar to debug.traceback but breaks at xpcall, uses spaces instead of tabs, and doesn't call local functions upvalues
667-
level = level + 1
668-
local msg = "Stack Traceback:\n"
669-
while true do
670-
local info = debug_getinfo(level, "Sln")
671-
if not info then break end
672-
if info.what == "C" then
673-
if info.name == "xpcall" then break end
674-
msg = msg .. " in method " .. tostring(info.name) .. "\n"
675-
else
676-
if info.name then
677-
msg = msg ..
678-
" " ..
679-
tostring(info.short_src) ..
680-
":" .. tostring(info.currentline) .. ": in function '" .. tostring(info.name) .. "'\n"
681-
else
682-
msg = msg ..
683-
" " ..
684-
tostring(info.short_src) ..
685-
":" .. tostring(info.currentline) .. ": in function at line " .. tostring(info.linedefined) .. "\n"
686-
end
687-
end
688-
689-
level = level + 1
690-
end
691-
692-
return msg
693-
end
694-
695-
696727
local function logWarning(callbackID, modName, warn)
697728
local cbName = callbackIDToName[callbackID] or callbackID
698729
for _, printedWarning in pairs(printedWarnings) do
@@ -1192,6 +1223,7 @@ function _UnloadMod(mod)
11921223
end
11931224

11941225
RemoveAllCallbacksForMod(mod)
1226+
ModManager.detail.UnloadMod(mod)
11951227
end
11961228

11971229
-- Runs a single callback function and checks the results.
@@ -1826,7 +1858,6 @@ end
18261858
---- MISC STUFF
18271859
-----------------------------------------------------------------------------------------------------
18281860

1829-
18301861
--Menuman Hub
18311862
MenuManager.MainMenu = MainMenu
18321863
MenuManager.CharacterMenu = CharacterMenu
@@ -1863,6 +1894,8 @@ local oldregmod = RegisterMod
18631894
function RegisterMod(name, ver)
18641895
local out = oldregmod(name, ver)
18651896
out.Repentogon = REPENTOGON
1897+
ModManager.detail.RegisterMod(out)
1898+
18661899
return out
18671900
end
18681901

@@ -2014,11 +2047,7 @@ pcall(require, "repentogon_extras/bestiary_menu")
20142047
-- pcall(require, "repentogon_extras/onlinestub") let's not load it
20152048
pcall(require, "repentogon_extras/mods_menu_tweaks")
20162049

2017-
---@type boolean, REPENTOGON._Internals.ESSM
2018-
local essmSuccess, ESSM = pcall(include, "repentogon_extras.entity_save_state_manager")
2019-
if not essmSuccess then
2020-
error(string.format("[ERROR] Failed to load ESSM script: %s", ESSM))
2021-
end
2050+
local ESSM = _GetModule("repentogon_extras.entity_save_state_manager")
20222051

20232052
local ESSM_OnNewEntity = ESSM._OnNewEntity
20242053
local ESSM_OnDeleteEntity = ESSM._OnDeleteEntity
@@ -2072,4 +2101,7 @@ EntitySaveStateManager = {
20722101
GetEntitySaveStateData = ESSM.GetEntitySaveStateData,
20732102
TryGetEntityData = ESSM.TryGetEntityData,
20742103
TryGetEntitySaveStateData = ESSM.TryGetEntitySaveStateData,
2075-
}
2104+
}
2105+
2106+
_GetModule = nil
2107+
s_modules = nil

repentogon/resources/scripts/repentogon_extras/entity_save_state_manager.lua

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
---@class REPENTOGON._Internals.ESSM
2-
local Module = {}
3-
41
--#region Data
52

63
---@class REPENTOGON._Internals.ESSM.ModLookup
@@ -20,8 +17,8 @@ local Module = {}
2017

2118
local SAVE_DATA_VERSION = 1
2219

23-
---Map of registered mod ids and the ModReference that will be used for save and load
24-
---@type table<string, ModReference>
20+
---Map of registered mod ids and it's name, used to check if a modId has been registered and for some debug messages.
21+
---@type table<string, string>
2522
local s_registeredMods = {}
2623

2724
---Set of entities PtrHash that exist
@@ -44,6 +41,7 @@ local s_modLookup = {}
4441
--#endregion
4542

4643
local json = require("json")
44+
local ModManager = _GetModule("repentogon_extras.mod_manager")
4745

4846
local type = type
4947
local pairs = pairs
@@ -58,7 +56,6 @@ local json_encode = json.encode
5856
local json_decode = json.decode
5957
local GetPtrHash = GetPtrHash
6058
local Isaac_DebugString = Isaac.DebugString
61-
local GetModId = _CBindings.GetModId
6259
local GetEntitySaveStateId = _CBindings.ESSM.GetEntitySaveStateId
6360
local SaveEntityData = _CBindings.ESSM.SaveData
6461
local LoadEntityData = _CBindings.ESSM.LoadData
@@ -126,7 +123,7 @@ local function register_mod_id(mod, modId)
126123
local saveStateData
127124

128125
if not exists then
129-
s_registeredMods[modId] = mod
126+
s_registeredMods[modId] = mod.Name
130127
entityData = {}
131128
saveStateData = {}
132129
s_entityData[modId] = entityData
@@ -143,13 +140,9 @@ end
143140
---@return boolean, string
144141
local function register_mod(mod)
145142
local varType = type(mod)
146-
if varType ~= "table" then
147-
return false, string_format("ModReference expected, got %s", varType)
148-
end
149-
150-
local modId = GetModId(mod)
143+
local modId = ModManager.GetModIdByReference(mod)
151144
if not modId then
152-
return false, "ModReference expected, got table"
145+
return false, string_format("ModReference expected, got %s", varType)
153146
end
154147

155148
register_mod_id(mod, modId)
@@ -333,14 +326,21 @@ end
333326
---@param checksum integer
334327
local function _Serialize(idMap, fileName, checksum)
335328
for modId, saveStateTbl in pairs(s_saveStateData) do
329+
local mod = ModManager.GetModReferenceById(modId)
330+
if not mod then
331+
local modName = s_registeredMods[modId] -- use the first ever registered mod reference name
332+
Isaac_DebugString(string_format("[ERROR] [ESSM] Registered mod \"%s\" no longer has a valid ModReference, cannot save data", modName))
333+
goto continue
334+
end
335+
336336
local success, result = pcall(serialize_data, saveStateTbl, idMap, checksum)
337-
local mod = s_registeredMods[modId]
338337
if not success then
339-
Isaac_DebugString(string_format("[ERROR] [ESSM] Unable to save data for Mod '%s' in '%s': %s", mod.Name, fileName, result))
338+
Isaac_DebugString(string_format("[ERROR] [ESSM] Unable to save data for Mod \"%s\" in \"%s\": %s", mod.Name, fileName, result))
340339
DeleteEntityData(mod, fileName)
341340
else
342341
SaveEntityData(mod, fileName, result)
343342
end
343+
::continue::
344344
end
345345
end
346346

@@ -450,7 +450,13 @@ end
450450
local function _Deserialize(serializedIds, destIds, fileName, checksum)
451451
assert(#serializedIds, #destIds)
452452
for modId, saveStateTbl in pairs(s_saveStateData) do
453-
local mod = s_registeredMods[modId]
453+
local mod = ModManager.GetModReferenceById(modId)
454+
if not mod then
455+
local modName = s_registeredMods[modId] -- use the first ever registered mod reference name
456+
Isaac_DebugString(string_format("[ERROR] [ESSM] Registered mod \"%s\" no longer has a valid ModReference, cannot load data", modName))
457+
goto continue
458+
end
459+
454460
local data = LoadEntityData(mod, fileName)
455461

456462
if not data then
@@ -604,6 +610,8 @@ end
604610

605611
--#endregion
606612

613+
local Module = {}
614+
607615
Module._OnNewEntity = _OnNewEntity
608616
Module._OnDeleteEntity = _OnDeleteEntity
609617
Module._OnStoreEntity = _OnStoreEntity
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---@alias REPENTOGON.ModId string
2+
3+
--#region Data
4+
5+
---@type table<ModReference, REPENTOGON.ModId>
6+
local s_modIds = {}
7+
8+
---@type table<REPENTOGON.ModId, ModReference[]>
9+
local s_modReferences = {}
10+
11+
--#endregion
12+
13+
local table_insert = table.insert
14+
local table_remove = table.remove
15+
local GetModId = _CBindings.GetModId
16+
17+
---@param modReference ModReference
18+
local function _RegisterMod(modReference)
19+
local id = GetModId(modReference)
20+
if not id then
21+
return
22+
end
23+
24+
assert(s_modIds[modReference] == nil, "The same mod has been registered multiple times")
25+
s_modIds[modReference] = id
26+
local modReferences = s_modReferences[id]
27+
if not modReferences then
28+
modReferences = {}
29+
s_modReferences[id] = modReferences
30+
end
31+
32+
table_insert(modReferences, modReference)
33+
end
34+
35+
---@param modReference ModReference
36+
local function _UnloadMod(modReference)
37+
local id = s_modIds[modReference]
38+
if not id then
39+
return
40+
end
41+
42+
s_modIds[modReference] = nil
43+
44+
local modReferences = s_modReferences[id]
45+
for i = 1, #modReferences, 1 do
46+
if modReferences[i] == modReference then
47+
table_remove(modReferences, i)
48+
break -- there should never be multiple of the same mod references in the list
49+
end
50+
end
51+
end
52+
53+
---@param modReference ModReference
54+
---@return REPENTOGON.ModId?
55+
local function GetModIdByReference(modReference)
56+
return s_modIds[modReference]
57+
end
58+
59+
---@param modId REPENTOGON.ModId
60+
---@return ModReference?
61+
local function GetModReferenceById(modId)
62+
local modReferences = s_modReferences[modId]
63+
if modReferences then
64+
return modReferences[1]
65+
end
66+
end
67+
68+
Module = {}
69+
Module.detail = {}
70+
71+
Module.detail.RegisterMod = _RegisterMod
72+
Module.detail.UnloadMod = _UnloadMod
73+
Module.GetModIdByReference = GetModIdByReference
74+
Module.GetModReferenceById = GetModReferenceById
75+
76+
return Module

0 commit comments

Comments
 (0)