diff --git a/EEex/copy/EEex_scripts/EEex_Assembly.lua b/EEex/copy/EEex_scripts/EEex_Assembly.lua index d368165e..28d04015 100644 --- a/EEex/copy/EEex_scripts/EEex_Assembly.lua +++ b/EEex/copy/EEex_scripts/EEex_Assembly.lua @@ -1287,25 +1287,32 @@ end ----------------------- EEex_UserDataAuxiliary = {} +EEex_UserDataAuxiliaryPersistent = {} function EEex_DeleteUserDataAuxiliary(ud) if type(ud) ~= "userdata" then EEex_Error("ud is not a userdata object ("..type(ud)..")!") end - EEex_UserDataAuxiliary[EEex_UDToLightUD(ud)] = nil + EEex_UDAux_Private_DeleteByLightUD(EEex_UDToLightUD(ud)) end EEex_DeleteUDAux = EEex_DeleteUserDataAuxiliary -function EEex_GetUserDataAuxiliary(ud) +function EEex_GetUserDataAuxiliary(ud, persistent) if type(ud) ~= "userdata" then EEex_Error("ud is not a userdata object ("..type(ud)..")!") end + if persistent ~= nil and type(persistent) ~= "boolean" then + EEex_Error("persistent must be nil or a boolean ("..type(persistent)..")!") + end local lud = EEex_UDToLightUD(ud) local auxiliary = EEex_UserDataAuxiliary[lud] if not auxiliary then auxiliary = {} EEex_UserDataAuxiliary[lud] = auxiliary end + if persistent then + EEex_UserDataAuxiliaryPersistent[lud] = true + end return auxiliary end EEex_GetUDAux = EEex_GetUserDataAuxiliary diff --git a/EEex/copy/EEex_scripts/EEex_Main.lua b/EEex/copy/EEex_scripts/EEex_Main.lua index d2bb710a..525ff0be 100644 --- a/EEex/copy/EEex_scripts/EEex_Main.lua +++ b/EEex/copy/EEex_scripts/EEex_Main.lua @@ -38,6 +38,8 @@ EEex_Main_Private_StartupFiles = { "EEex_Resource", -- "EEex_Script", -- "EEex_Script_Patch", -- + "EEex_Marshal_Codec", -- + "EEex_UDAux", -- "EEex_Sprite", -- "EEex_Sprite_Patch", -- "EEex_Stats", -- @@ -54,6 +56,7 @@ EEex_Main_Private_StartupFiles = { "EEex_Fix", -- "EEex_Fix_Patch", -- "EEex_Marshal", -- + "EEex_Marshal_Patch", -- "EEex_Module", -- "EEex_OptionsLate", -- Here so it can register listeners provided by other EEex files "EEex_Profiler", -- diff --git a/EEex/copy/EEex_scripts/EEex_Marshal.lua b/EEex/copy/EEex_scripts/EEex_Marshal.lua index 84c18d56..da42215d 100644 --- a/EEex/copy/EEex_scripts/EEex_Marshal.lua +++ b/EEex/copy/EEex_scripts/EEex_Marshal.lua @@ -15,10 +15,85 @@ EEex_Sprite_AddMarshalHandlers("EEex", end ) +EEex_Sprite_AddMarshalHandlers("EEex_UDAux", + function(sprite) + + if EEex.IsMarshallingCopy() then + return nil + end + + local toMarshal = {} + + local exportEffectList = function(list, path) + local exported = {} + local i = 1 + EEex_Utility_IterateCPtrList(list, function(effect) + exported[i] = EEex_UDAux_Private_Export(effect, path.."["..i.."]") + i = i + 1 + end) + return next(exported) and exported or nil + end + + local timedEffects = exportEffectList(sprite.m_timedEffectList, "CGameSprite.m_timedEffectList") + if timedEffects then + toMarshal["timedEffects"] = timedEffects + end + + local equippedEffects = exportEffectList(sprite.m_equipedEffectList, "CGameSprite.m_equipedEffectList") + if equippedEffects then + toMarshal["equippedEffects"] = equippedEffects + end + + local items = {} + for i = 0, 38 do + local item = sprite.m_equipment.m_items:get(i) + if item then + items[i] = EEex_UDAux_Private_Export(item, "CGameSprite.m_equipment.m_items["..i.."]") + end + end + if next(items) then + toMarshal["items"] = items + end + + return next(toMarshal) and toMarshal or nil + end, + function(sprite, read) + + local importEffectList = function(list, data) + if not data then + return + end + local i = 1 + EEex_Utility_IterateCPtrList(list, function(effect) + local auxiliary = data[i] + if auxiliary then + EEex_UDAux_Private_Import(effect, auxiliary) + end + i = i + 1 + end) + end + + importEffectList(sprite.m_timedEffectList, read["timedEffects"]) + importEffectList(sprite.m_equipedEffectList, read["equippedEffects"]) + + local items = read["items"] + if items then + -- The extra creature marshal block is read while the sprite is still being rebuilt. + -- Equipment item pointers can be unavailable at this point, so keep slot data on the + -- sprite and retry from normal post-load/update hooks until each item exists. + EEex_GetUDAux(sprite)["EEex_UDAux_PendingItemAux"] = items + EEex_UDAux_Private_ApplyPendingItemAux(sprite) + end + end +) + function EEex_Marshal_Private_OnSummonerLoaded(sprite, loadedSprite) sprite.m_lSummonedBy:Set(loadedSprite:virtual_GetAIType()) end +EEex_Sprite_AddLoadedListener(EEex_UDAux_Private_ApplyPendingItemAux) +EEex_Opcode_AddListsResolvedListener(EEex_UDAux_Private_ApplyPendingItemAux) + EEex_Sprite_AddLoadedListener(function(sprite) local summonerUUID = EEex_GetUDAux(sprite)["EEex_SummonerUUID"] if summonerUUID then diff --git a/EEex/copy/EEex_scripts/EEex_Marshal_Codec.lua b/EEex/copy/EEex_scripts/EEex_Marshal_Codec.lua new file mode 100644 index 00000000..1057d6c8 --- /dev/null +++ b/EEex/copy/EEex_scripts/EEex_Marshal_Codec.lua @@ -0,0 +1,440 @@ + +-- IMPORTANT: These enum values are part of the shared EEex marshal table schema. +-- Do not modify existing values without implementing migration code for every channel. +EEex_Marshal_Private_TableFieldType = { + ["TABLE_END"] = 0, + ["TABLE_START"] = 1, + ["STRING"] = 2, + ["INT8"] = 3, + ["INTU8"] = 4, + ["INT16"] = 5, + ["INTU16"] = 6, + ["INT32"] = 7, + ["INTU32"] = 8, + ["INT64"] = 9, + ["INTU64"] = 10, + ["BOOLEAN_FALSE"] = 11, + ["BOOLEAN_TRUE"] = 12, +} + +function EEex_Marshal_Private_NewTableBundle() + -- A bundle is a top-level sequence of named table payloads. Each handler name lets an + -- extension channel ignore unknown entries without corrupting data owned by other handlers. + return { + ["entries"] = {}, + ["entryCount"] = 0, + ["tableToMeta"] = {}, + ["memorySize"] = 0, + } +end + +function EEex_Marshal_Private_ResetTableBundle(bundle) + bundle["entries"] = {} + bundle["entryCount"] = 0 + bundle["tableToMeta"] = {} + bundle["memorySize"] = 0 +end + +function EEex_Marshal_Private_AssertTableString(value, context) + if value:find("\0", 1, true) then + EEex_Error((context or "Marshal").." strings cannot contain NUL bytes") + end +end + +function EEex_Marshal_Private_AddTableBundleEntry(bundle, handlerName, toExport, context) + if type(handlerName) ~= "string" or handlerName == "" then + EEex_Error((context or "Marshal").." handler name must be a non-empty string") + end + EEex_Marshal_Private_AssertTableString(handlerName, context) + if type(toExport) ~= "table" then + EEex_Error((context or "Marshal").." handler must export table or nil") + end + if bundle["tableToMeta"][toExport] then + EEex_Error((context or "Marshal").." exported the same table more than once") + end + bundle["entryCount"] = bundle["entryCount"] + 1 + bundle["entries"][bundle["entryCount"]] = toExport + bundle["tableToMeta"][toExport] = { + ["handlerName"] = handlerName, + } +end + +function EEex_Marshal_Private_DetermineTableNumberInfo(number, context) + -- Use the smallest integer encoding that can round-trip the value. The enum values are fixed + -- because older saves may already contain any of these field tags. + if number ~= number or number % 1 ~= 0 then + EEex_Error((context or "Marshal").." number must be an integer") + end + if number >= 0 then + if number <= 0xFF then + return EEex_Marshal_Private_TableFieldType.INTU8, EEex_WriteU8, 1 + elseif number <= 0xFFFF then + return EEex_Marshal_Private_TableFieldType.INTU16, EEex_WriteU16, 2 + elseif number <= 0xFFFFFFFF then + return EEex_Marshal_Private_TableFieldType.INTU32, EEex_WriteU32, 4 + elseif number <= 0xFFFFFFFFFFFFFFFF then + return EEex_Marshal_Private_TableFieldType.INTU64, EEex_WriteU64, 8 + else + EEex_Error((context or "Marshal").." number is outside the supported range") + end + else + if number >= -0x100 then + return EEex_Marshal_Private_TableFieldType.INT8, EEex_Write8, 1 + elseif number >= -0x10000 then + return EEex_Marshal_Private_TableFieldType.INT16, EEex_Write16, 2 + elseif number >= -0x100000000 then + return EEex_Marshal_Private_TableFieldType.INT32, EEex_Write32, 4 + elseif number >= -0x10000000000000000 then + return EEex_Marshal_Private_TableFieldType.INT64, EEex_Write64, 8 + else + EEex_Error((context or "Marshal").." number is outside the supported range") + end + end +end + +function EEex_Marshal_Private_CalculateTableBundleSize(bundle, context) + + -- The writer below is iterative to avoid Lua recursion limits. Keep the sizing walk structurally + -- identical so payload allocation exactly matches what WriteTableBundle will emit. + local accumulator = 0 + local lengthTypeSwitch = { + ["boolean"] = function(v) + return 0 + end, + ["number"] = function(v) + local _, _, writeAdvance = EEex_Marshal_Private_DetermineTableNumberInfo(v, context) + return writeAdvance + end, + ["string"] = function(v) + EEex_Marshal_Private_AssertTableString(v, context) + return #v + 1 + end, + } + + local processStack = {{bundle["entries"], nil}} -- toProcessT, iterK + local activeTables = { + [bundle["entries"]] = true, + } + local stackTop = 1 + + while true do + + ::continue:: + local toProcess = processStack[stackTop] + local toProcessT = toProcess[1] + + while true do + + local k, v = next(toProcessT, toProcess[2]) + if k == nil then + break + end + local kType = type(k) + if kType ~= "boolean" and kType ~= "number" and kType ~= "string" then + EEex_Error((context or "Marshal").." only supports boolean, number, and string keys") + end + + toProcess[2] = k + + if stackTop == 1 then + local meta = bundle["tableToMeta"][v] + if not meta then + EEex_Error((context or "Marshal").." top-level table is missing handler metadata") + end + local handlerName = meta["handlerName"] + accumulator = accumulator + #handlerName + 1 + if activeTables[v] then + EEex_Error((context or "Marshal").." table cycle detected") + end + activeTables[v] = true + stackTop = stackTop + 1 + processStack[stackTop] = {v, nil} + goto continue + else + local vType = type(v) + if vType ~= "boolean" and vType ~= "number" and vType ~= "string" and vType ~= "table" then + EEex_Error((context or "Marshal").." only supports boolean, number, string, and table values") + end + if vType == "table" then + -- KEY_FIELD_TYPE + KEY_LENGTH + TABLE_START + accumulator = accumulator + 1 + lengthTypeSwitch[kType](k) + 1 + if activeTables[v] then + EEex_Error((context or "Marshal").." table cycle detected") + end + activeTables[v] = true + stackTop = stackTop + 1 + processStack[stackTop] = {v, nil} + goto continue + end + -- KEY_FIELD_TYPE + KEY_LENGTH + VALUE_FIELD_TYPE + VALUE_LENGTH + accumulator = accumulator + 1 + lengthTypeSwitch[kType](k) + 1 + lengthTypeSwitch[vType](v) + end + end + + accumulator = accumulator + 1 -- TABLE_END + + activeTables[toProcessT] = nil + processStack[stackTop] = nil + stackTop = stackTop - 1 + + if stackTop == 0 then + break + end + end + + return accumulator +end + +function EEex_Marshal_Private_WriteTableBundle(memoryPtr, bundle, context) + + -- Layout: + -- handlerName\0, encoded table fields..., TABLE_END + -- handlerName\0, encoded table fields..., TABLE_END + -- TABLE_END + -- The final TABLE_END is read as an empty handler string. + local writeNumber = function(number) + local typeByte, writeFunc, writeAdvance = EEex_Marshal_Private_DetermineTableNumberInfo(number, context) + EEex_Write8(memoryPtr, typeByte) + memoryPtr = memoryPtr + 1 + writeFunc(memoryPtr, number) + memoryPtr = memoryPtr + writeAdvance + end + + local writeTypeSwitch = { + ["boolean"] = function(v) + EEex_Write8(memoryPtr, v + and EEex_Marshal_Private_TableFieldType.BOOLEAN_TRUE + or EEex_Marshal_Private_TableFieldType.BOOLEAN_FALSE + ) + memoryPtr = memoryPtr + 1 + end, + ["number"] = writeNumber, + ["string"] = function(v) + EEex_Marshal_Private_AssertTableString(v, context) + EEex_Write8(memoryPtr, EEex_Marshal_Private_TableFieldType.STRING) + memoryPtr = memoryPtr + 1 + EEex_WriteString(memoryPtr, v) + memoryPtr = memoryPtr + #v + 1 + end, + ["table"] = function(v) + EEex_Write8(memoryPtr, EEex_Marshal_Private_TableFieldType.TABLE_START) + memoryPtr = memoryPtr + 1 + end, + } + + local processStack = {{bundle["entries"], nil}} -- toProcessT, iterK + local activeTables = { + [bundle["entries"]] = true, + } + local stackTop = 1 + + while true do + + ::continue:: + local toProcess = processStack[stackTop] + local toProcessT = toProcess[1] + + while true do + + local k, v = next(toProcessT, toProcess[2]) + if k == nil then + break + end + local kType = type(k) + if kType ~= "boolean" and kType ~= "number" and kType ~= "string" then + EEex_Error((context or "Marshal").." only supports boolean, number, and string keys") + end + + toProcess[2] = k + + if stackTop == 1 then + local meta = bundle["tableToMeta"][v] + if not meta then + EEex_Error((context or "Marshal").." top-level table is missing handler metadata") + end + local handlerName = meta["handlerName"] + EEex_Marshal_Private_AssertTableString(handlerName, context) + EEex_WriteString(memoryPtr, handlerName) + memoryPtr = memoryPtr + #handlerName + 1 + else + local vType = type(v) + if vType ~= "boolean" and vType ~= "number" and vType ~= "string" and vType ~= "table" then + EEex_Error((context or "Marshal").." only supports boolean, number, string, and table values") + end + writeTypeSwitch[kType](k) + writeTypeSwitch[vType](v) + end + + if type(v) == "table" then + if activeTables[v] then + EEex_Error((context or "Marshal").." table cycle detected") + end + activeTables[v] = true + stackTop = stackTop + 1 + processStack[stackTop] = {v, nil} + goto continue + end + end + + EEex_Write8(memoryPtr, EEex_Marshal_Private_TableFieldType.TABLE_END) + memoryPtr = memoryPtr + 1 + + activeTables[toProcessT] = nil + processStack[stackTop] = nil + stackTop = stackTop - 1 + + if stackTop == 0 then + break + end + end +end + +function EEex_Marshal_Private_ReadTableBundle(memory, handlerReader, size) + + -- When size is provided, keep reads inside the extension payload. The sprite extra-effect + -- channel predates bounded reads and still calls this without size, preserving that behavior. + local baseMemory = memory + local endMemory = size and (memory + size) or nil + + local assertAvailable = function(byteCount, what) + if endMemory and memory + byteCount > endMemory then + EEex_Error("Marshal payload ended while reading "..what) + end + end + + local readByte = function(what) + assertAvailable(1, what) + local read = EEex_ReadU8(memory) + memory = memory + 1 + return read + end + + local readString = function(what) + if not endMemory then + local read = EEex_ReadString(memory) + memory = memory + #read + 1 + return read + end + + local cursor = memory + while cursor < endMemory do + if EEex_ReadU8(cursor) == 0 then + local read = EEex_ReadString(memory) + memory = cursor + 1 + return read + end + cursor = cursor + 1 + end + + EEex_Error("Marshal payload ended while reading "..what) + end + + while true do + + local toFill = {} + local handlerStr = readString("handler name") + + -- The top-level list writes TABLE_END('\0') to signal that all + -- marshalled data has ended, which reads as an empty string. + if handlerStr == "" then + break + end + + local fieldReadSwitch = { + [EEex_Marshal_Private_TableFieldType.STRING] = function() + return readString("string field") + end, + [EEex_Marshal_Private_TableFieldType.INT8] = function() + assertAvailable(1, "int8 field") + local read = EEex_Read8(memory) + memory = memory + 1 + return read + end, + [EEex_Marshal_Private_TableFieldType.INTU8] = function() + return readByte("uint8 field") + end, + [EEex_Marshal_Private_TableFieldType.INT16] = function() + assertAvailable(2, "int16 field") + local read = EEex_Read16(memory) + memory = memory + 2 + return read + end, + [EEex_Marshal_Private_TableFieldType.INTU16] = function() + assertAvailable(2, "uint16 field") + local read = EEex_ReadU16(memory) + memory = memory + 2 + return read + end, + [EEex_Marshal_Private_TableFieldType.INT32] = function() + assertAvailable(4, "int32 field") + local read = EEex_Read32(memory) + memory = memory + 4 + return read + end, + [EEex_Marshal_Private_TableFieldType.INTU32] = function() + assertAvailable(4, "uint32 field") + local read = EEex_ReadU32(memory) + memory = memory + 4 + return read + end, + [EEex_Marshal_Private_TableFieldType.INT64] = function() + assertAvailable(8, "int64 field") + local read = EEex_Read64(memory) + memory = memory + 8 + return read + end, + [EEex_Marshal_Private_TableFieldType.INTU64] = function() + assertAvailable(8, "uint64 field") + local read = EEex_ReadU64(memory) + memory = memory + 8 + return read + end, + [EEex_Marshal_Private_TableFieldType.BOOLEAN_FALSE] = function() + return false + end, + [EEex_Marshal_Private_TableFieldType.BOOLEAN_TRUE] = function() + return true + end, + } + + local tableStack = {} + local tableStackTop = 0 + + while true do + + local keyFieldType = readByte("key field type") + + if keyFieldType == EEex_Marshal_Private_TableFieldType.TABLE_END then + if tableStackTop == 0 then + break + end + toFill = tableStack[tableStackTop] + tableStackTop = tableStackTop - 1 + else + local keyReader = fieldReadSwitch[keyFieldType] + if not keyReader then + EEex_Error("Unknown marshal key field type "..keyFieldType) + end + local key = keyReader() + local valueFieldType = readByte("value field type") + if valueFieldType == EEex_Marshal_Private_TableFieldType.TABLE_START then + local subTable = {} + toFill[key] = subTable + tableStackTop = tableStackTop + 1 + tableStack[tableStackTop] = toFill + toFill = subTable + else + local valueReader = fieldReadSwitch[valueFieldType] + if not valueReader then + EEex_Error("Unknown marshal value field type "..valueFieldType) + end + toFill[key] = valueReader() + end + end + end + + handlerReader(handlerStr, toFill) + end + + return memory - baseMemory +end diff --git a/EEex/copy/EEex_scripts/EEex_Marshal_Patch.lua b/EEex/copy/EEex_scripts/EEex_Marshal_Patch.lua new file mode 100644 index 00000000..dfb3eddc --- /dev/null +++ b/EEex/copy/EEex_scripts/EEex_Marshal_Patch.lua @@ -0,0 +1,190 @@ + +(function() + + EEex_DisableCodeProtection() + + local EEex_Marshal_Patch_Private_VolatileRegisterIgnores = { + {"hook_integrity_watchdog_ignore_registers", { + EEex_HookIntegrityWatchdogRegister.RAX, + EEex_HookIntegrityWatchdogRegister.RCX, + EEex_HookIntegrityWatchdogRegister.RDX, + EEex_HookIntegrityWatchdogRegister.R8, + EEex_HookIntegrityWatchdogRegister.R9, + EEex_HookIntegrityWatchdogRegister.R10, + EEex_HookIntegrityWatchdogRegister.R11, + }}, + } + + local EEex_Marshal_Patch_Private_EntryStackVolatileRegisterIgnores = EEex_FlattenTable({ + {{"stack_mod", 8}}, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + }) + + -- rcx = CGameArea*, rdx = unsigned char**, r8 = unsigned int*, r9b = version. + -- Start a per-area UDAux collection before vanilla container marshal callbacks fire. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameArea::Marshal(unsigned char**,unsigned int*,unsigned char)-FirstInstruction"), 0, 8, 8, + EEex_Marshal_Patch_Private_EntryStackVolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(40) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], rdx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)], r8 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)], r9 + + call #L(EEex::UDAux_Hook_OnBeforeAreaMarshal) + + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- The vanilla blob has been allocated and sized; append the X-UDA1.0 extension now. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameArea::Marshal(unsigned char**,unsigned int*,unsigned char)-AfterMarshal"), 0, 8, 8, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(8) + call #L(EEex::UDAux_Hook_OnAfterAreaMarshal) + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- Container marshal order is the stable save ordinal used by the area UDAux payload. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameContainer::Marshal(SAreaFileWrapper*)-FirstInstruction"), 0, 5, 5, + EEex_Marshal_Patch_Private_EntryStackVolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(40) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], rdx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)], r8 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)], r9 + + call #L(EEex::UDAux_Hook_OnAreaContainerMarshal) + + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- Parse X-UDA1.0 before vanilla unmarshal constructs CGameContainer instances. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameArea::Unmarshal(unsigned char*,unsigned int,unsigned char)-FirstInstruction"), 0, 8, 8, + EEex_Marshal_Patch_Private_EntryStackVolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(40) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], rdx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)], r8 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)], r9 + + call #L(EEex::UDAux_Hook_OnBeforeAreaUnmarshal) + + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- r12d holds the CGameArea::Unmarshal result at this epilogue path. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameArea::Unmarshal(unsigned char*,unsigned int,unsigned char)-AfterUnmarshal"), 0, 8, 8, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(8) + mov ecx, r12d + call #L(EEex::UDAux_Hook_OnAfterAreaUnmarshal) + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- r13 is the newly constructed CGameContainer; its item list has been populated. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameContainer::CGameContainer(CGameArea*,CAreaFileContainer*,...)-AfterConstruct"), 0, 7, 7, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(8) + mov rcx, r13 + call #L(EEex::UDAux_Hook_OnAfterAreaContainerConstruct) + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- rcx = CStore*. Delete store/item aux before the destructor drains m_lInventory. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CStore::~CStore()-FirstInstruction"), 0, 5, 5, + EEex_Marshal_Patch_Private_EntryStackVolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(8) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + + call #L(EEex::UDAux_Hook_OnBeforeStoreInventoryClear) + + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- At this call: rcx = CRes*, rdx = marshalled STO buffer, r8d = size, rsi = CStore*. + -- The hook may replace rdx/r8d with a buffer containing the X-UDS1.0 payload. + EEex_HookBeforeCallWithLabels(EEex_Label("Hook-CStore::Marshal()-dimmServiceFromMemory"), + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(24) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], r9 + mov dword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-20)], r8d + + mov rcx, rsi + lea r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-20)] + call #L(EEex::UDAux_Hook_OnStoreMarshalData) + + mov rdx, rax + mov r8d, dword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-20)] + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- rbx = CStore*. Existing runtime CStore data is about to be replaced. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CStore::SetResRef(CResRef const&)-BeforeInventoryClear"), 0, 7, 7, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(8) + mov rcx, rbx + call #L(EEex::UDAux_Hook_OnBeforeStoreInventoryClear) + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- rbx = CStore*, rdi = demanded raw STO data, [rsp+20h] = CResStore* helper. + -- Import only records aux data here; runtime item pointers do not exist yet. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CStore::SetResRef(CResRef const&)-BeforeHeaderRead"), 0, 10, 10, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + mov r8, qword ptr ss:[rsp+20h] + #MAKE_SHADOW_SPACE(8) + mov rcx, rbx + mov rdx, rdi + call #L(EEex::UDAux_Hook_OnBeforeStoreLoad) + #DESTROY_SHADOW_SPACE + ]]} + ) + + -- rbx still names the CStore after inventory/drink/spell arrays are rebuilt. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CStore::SetResRef(CResRef const&)-AfterInventoryPopulate"), 0, 6, 6, + EEex_Marshal_Patch_Private_VolatileRegisterIgnores, + {[[ + #MAKE_SHADOW_SPACE(8) + mov rcx, rbx + call #L(EEex::UDAux_Hook_OnAfterStoreLoad) + #DESTROY_SHADOW_SPACE + ]]} + ) + + EEex_EnableCodeProtection() + +end)() diff --git a/EEex/copy/EEex_scripts/EEex_Sprite.lua b/EEex/copy/EEex_scripts/EEex_Sprite.lua index b2d8d930..0e2b9c99 100644 --- a/EEex/copy/EEex_scripts/EEex_Sprite.lua +++ b/EEex/copy/EEex_scripts/EEex_Sprite.lua @@ -1063,207 +1063,7 @@ end -- -- end --- IMPORTANT: This is part of the X-BIV1.0 schema, don't modify existing enum values without implementing migration code! -EEex_Sprite_Private_MarshalHandlerFieldType = { - ["TABLE_END"] = 0, - ["TABLE_START"] = 1, - ["STRING"] = 2, - ["INT8"] = 3, - ["INTU8"] = 4, - ["INT16"] = 5, - ["INTU16"] = 6, - ["INT32"] = 7, - ["INTU32"] = 8, - ["INT64"] = 9, - ["INTU64"] = 10, - ["BOOLEAN_FALSE"] = 11, - ["BOOLEAN_TRUE"] = 12, -} - -EEex_Sprite_Private_CurrentSpriteMarshalHandlerData = {} -EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableSize = 0 -EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableToMeta = {} -EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_MemorySize = 0 - -function EEex_Sprite_Private_DetermineSpriteMarshalHandlerNumberInfo(number) - if number >= 0 then - if number <= 0xFF then - return EEex_Sprite_Private_MarshalHandlerFieldType.INTU8, EEex_WriteU8, 1 - elseif number <= 0xFFFF then - return EEex_Sprite_Private_MarshalHandlerFieldType.INTU16, EEex_WriteU16, 2 - elseif number <= 0xFFFFFFFF then - return EEex_Sprite_Private_MarshalHandlerFieldType.INTU32, EEex_WriteU32, 4 - elseif number <= 0xFFFFFFFFFFFFFFFF then - return EEex_Sprite_Private_MarshalHandlerFieldType.INTU64, EEex_WriteU64, 8 - else - EEex_Error("Number too large to be marshalled in creature handler") - end - else - if number >= -0x100 then - return EEex_Sprite_Private_MarshalHandlerFieldType.INT8, EEex_Write8, 1 - elseif number >= -0x10000 then - return EEex_Sprite_Private_MarshalHandlerFieldType.INT16, EEex_Write16, 2 - elseif number >= -0x100000000 then - return EEex_Sprite_Private_MarshalHandlerFieldType.INT32, EEex_Write32, 4 - elseif number >= -0x10000000000000000 then - return EEex_Sprite_Private_MarshalHandlerFieldType.INT64, EEex_Write64, 8 - else - EEex_Error("Number too large to be marshalled in creature handler") - end - end -end - -function EEex_Sprite_Private_CalculateSpriteMarshalHandlerDataSize(t) - - local accumulator = 0 - local lengthTypeSwitch = { - ["boolean"] = function(v) - return 0 - end, - ["number"] = function(v) - local _, _, writeAdvance = EEex_Sprite_Private_DetermineSpriteMarshalHandlerNumberInfo(v) - return writeAdvance - end, - ["string"] = function(v) - return #v + 1 - end, - } - - local processStack = {{t, nil}} -- toProcessT, iterK - local stackTop = 1 - - while true do - - ::continue:: - local toProcess = processStack[stackTop] - local toProcessT = toProcess[1] - - while true do - - local k, v = next(toProcessT, toProcess[2]) - if k == nil then - break - end - local kType = type(k) - if kType ~= "boolean" and kType ~= "number" and kType ~= "string" then - EEex_Error("Only booleans / numbers / strings can be used as keys in creature marshal") - end - - toProcess[2] = k - - if stackTop == 1 then - local handlerName = EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableToMeta[v].handlerName - -- HANDLER_STRING_LENGTH - accumulator = accumulator + #handlerName + 1 - stackTop = stackTop + 1 - processStack[stackTop] = {v, nil} - goto continue - else - local vType = type(v) - if vType ~= "boolean" and vType ~= "number" and vType ~= "string" and vType ~= "table" then - EEex_Error("Only booleans / numbers / strings / tables can be used as values in creature marshal") - end - if vType == "table" then - -- KEY_FIELD_TYPE + KEY_LENGTH + TABLE_START - accumulator = accumulator + 1 + lengthTypeSwitch[kType](k) + 1 - stackTop = stackTop + 1 - processStack[stackTop] = {v, nil} - goto continue - end - -- KEY_FIELD_TYPE + KEY_LENGTH + VALUE_FIELD_TYPE + VALUE_LENGTH - accumulator = accumulator + 1 + lengthTypeSwitch[kType](k) + 1 + lengthTypeSwitch[vType](v) - end - end - - accumulator = accumulator + 1 -- TABLE_END - - processStack[stackTop] = nil - stackTop = stackTop - 1 - - if stackTop == 0 then - break - end - end - - return accumulator -end - -function EEex_Sprite_Private_WriteSpriteMarshalHandlerData(memoryPtr, t) - - local writeNumber = function(number) - local typeByte, writeFunc, writeAdvance = EEex_Sprite_Private_DetermineSpriteMarshalHandlerNumberInfo(number) - EEex_Write8(memoryPtr, typeByte) - memoryPtr = memoryPtr + 1 - writeFunc(memoryPtr, number) - memoryPtr = memoryPtr + writeAdvance - end - - local writeTypeSwitch = { - ["boolean"] = function(v) - EEex_Write8(memoryPtr, v - and EEex_Sprite_Private_MarshalHandlerFieldType.BOOLEAN_TRUE - or EEex_Sprite_Private_MarshalHandlerFieldType.BOOLEAN_FALSE - ) - memoryPtr = memoryPtr + 1 - end, - ["number"] = writeNumber, - ["string"] = function(v) - EEex_Write8(memoryPtr, EEex_Sprite_Private_MarshalHandlerFieldType.STRING) - memoryPtr = memoryPtr + 1 - EEex_WriteString(memoryPtr, v) - memoryPtr = memoryPtr + #v + 1 - end, - ["table"] = function(v) - EEex_Write8(memoryPtr, EEex_Sprite_Private_MarshalHandlerFieldType.TABLE_START) - memoryPtr = memoryPtr + 1 - end, - } - - local processStack = {{t, nil}} -- toProcessT, iterK - local stackTop = 1 - - while true do - - ::continue:: - local toProcess = processStack[stackTop] - local toProcessT = toProcess[1] - - while true do - - local k, v = next(toProcessT, toProcess[2]) - if k == nil then - break - end - - toProcess[2] = k - - if stackTop == 1 then - local handlerName = EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableToMeta[v].handlerName - EEex_WriteString(memoryPtr, handlerName) - memoryPtr = memoryPtr + #handlerName + 1 - else - writeTypeSwitch[type(k)](k) - writeTypeSwitch[type(v)](v) - end - - if type(v) == "table" then - stackTop = stackTop + 1 - processStack[stackTop] = {v, nil} - goto continue - end - end - - EEex_Write8(memoryPtr, EEex_Sprite_Private_MarshalHandlerFieldType.TABLE_END) - memoryPtr = memoryPtr + 1 - - processStack[stackTop] = nil - stackTop = stackTop - 1 - - if stackTop == 0 then - break - end - end -end +EEex_Sprite_Private_CurrentMarshalBundle = EEex_Marshal_Private_NewTableBundle() function EEex_Sprite_Hook_CalculateExtraEffectListMarshalSize(sprite) @@ -1280,11 +1080,7 @@ function EEex_Sprite_Hook_CalculateExtraEffectListMarshalSize(sprite) if type(toExport) ~= "table" then EEex_Error("Creature marshal handler must export table or nil") end - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableSize = EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableSize + 1 - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData[EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableSize] = toExport - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableToMeta[toExport] = { - ["handlerName"] = handlerName, - } + EEex_Marshal_Private_AddTableBundleEntry(EEex_Sprite_Private_CurrentMarshalBundle, handlerName, toExport, "Creature marshal") end for handlerName, handler in pairs(EEex_Sprite_Private_MarshalHandlers) do @@ -1302,12 +1098,12 @@ function EEex_Sprite_Hook_CalculateExtraEffectListMarshalSize(sprite) -- Round up to multiple of CGameEffectBase to match an effect boundary on the CRE. -- If only 8 bytes are needed (no data is marshalled), skip writing entirely. - local extraMarshalSize = 8 + EEex_Sprite_Private_CalculateSpriteMarshalHandlerDataSize(EEex_Sprite_Private_CurrentSpriteMarshalHandlerData) - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_MemorySize = (extraMarshalSize ~= 8 and + local extraMarshalSize = 8 + EEex_Marshal_Private_CalculateTableBundleSize(EEex_Sprite_Private_CurrentMarshalBundle, "Creature marshal") + EEex_Sprite_Private_CurrentMarshalBundle["memorySize"] = (extraMarshalSize ~= 8 and EEex_RoundUp(extraMarshalSize, CGameEffectBase.sizeof) or 0) - 8 - return EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_MemorySize + 8 + return EEex_Sprite_Private_CurrentMarshalBundle["memorySize"] + 8 end function EEex_Sprite_Hook_WriteExtraEffectListMarshal(memory) @@ -1316,118 +1112,18 @@ function EEex_Sprite_Hook_WriteExtraEffectListMarshal(memory) return end - if EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_MemorySize > 0 then + if EEex_Sprite_Private_CurrentMarshalBundle["memorySize"] > 0 then EEex_WriteLString(memory, "X-BIV1.0", 8) local marshalPtr = memory + 8 - EEex_Memset(marshalPtr, 0, EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_MemorySize) - EEex_Sprite_Private_WriteSpriteMarshalHandlerData(marshalPtr, EEex_Sprite_Private_CurrentSpriteMarshalHandlerData) + EEex_Memset(marshalPtr, 0, EEex_Sprite_Private_CurrentMarshalBundle["memorySize"]) + EEex_Marshal_Private_WriteTableBundle(marshalPtr, EEex_Sprite_Private_CurrentMarshalBundle, "Creature marshal") end - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData = {} - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableSize = 0 - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_TableToMeta = {} - EEex_Sprite_Private_CurrentSpriteMarshalHandlerData_MemorySize = 0 + EEex_Marshal_Private_ResetTableBundle(EEex_Sprite_Private_CurrentMarshalBundle) end function EEex_Sprite_LuaHook_ReadExtraEffectListUnmarshal(sprite, baseMemory) - local memory = baseMemory + 8 - - while true do - - local toFill = {} - local handlerStr = EEex_ReadString(memory) - memory = memory + #handlerStr + 1 - - -- The top level list writes TABLE_END('\0') to signal that all - -- marshalled data has ended, which reads as an empty string - if handlerStr == "" then - break - end - - local fieldReadSwitch = { - [EEex_Sprite_Private_MarshalHandlerFieldType.STRING] = function() - local read = EEex_ReadString(memory) - memory = memory + #read + 1 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INT8] = function() - local read = EEex_Read8(memory) - memory = memory + 1 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INTU8] = function() - local read = EEex_ReadU8(memory) - memory = memory + 1 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INT16] = function() - local read = EEex_Read16(memory) - memory = memory + 2 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INTU16] = function() - local read = EEex_ReadU16(memory) - memory = memory + 2 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INT32] = function() - local read = EEex_Read32(memory) - memory = memory + 4 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INTU32] = function() - local read = EEex_ReadU32(memory) - memory = memory + 4 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INT64] = function() - local read = EEex_Read64(memory) - memory = memory + 8 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.INTU64] = function() - local read = EEex_ReadU64(memory) - memory = memory + 8 - return read - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.BOOLEAN_FALSE] = function() - return false - end, - [EEex_Sprite_Private_MarshalHandlerFieldType.BOOLEAN_TRUE] = function() - return true - end, - } - - local tableStack = {} - local tableStackTop = 0 - - while true do - - local keyFieldType = EEex_Read8(memory) - memory = memory + 1 - - if keyFieldType == EEex_Sprite_Private_MarshalHandlerFieldType.TABLE_END then - if tableStackTop == 0 then - break - end - toFill = tableStack[tableStackTop] - tableStackTop = tableStackTop - 1 - else - local key = fieldReadSwitch[keyFieldType]() - local valueFieldType = EEex_Read8(memory) - memory = memory + 1 - if valueFieldType == EEex_Sprite_Private_MarshalHandlerFieldType.TABLE_START then - local subTable = {} - toFill[key] = subTable - tableStackTop = tableStackTop + 1 - tableStack[tableStackTop] = toFill - toFill = subTable - else - toFill[key] = fieldReadSwitch[valueFieldType]() - end - end - end - + local bytesRead = EEex_Marshal_Private_ReadTableBundle(baseMemory + 8, function(handlerStr, toFill) local handlers = EEex_Sprite_Private_MarshalHandlers[handlerStr] if handlers then EEex_Utility_TryIgnore(handlers.importer, sprite, toFill) @@ -1436,10 +1132,10 @@ function EEex_Sprite_LuaHook_ReadExtraEffectListUnmarshal(sprite, baseMemory) local fallbackStorage = EEex_Utility_GetOrCreateTable(EEex_GetUDAux(sprite), "EEex_Sprite_FallbackMarshalStorage") fallbackStorage[handlerStr] = toFill end - end + end) -- Return to the C++ hook how many effects were EEex binary data - return EEex_RoundUp(memory - baseMemory, CGameEffectBase.sizeof) / CGameEffectBase.sizeof + return EEex_RoundUp(8 + bytesRead, CGameEffectBase.sizeof) / CGameEffectBase.sizeof end EEex_Sprite_Private_CustomConcentrationCheckFuncName = nil diff --git a/EEex/copy/EEex_scripts/EEex_UDAux.lua b/EEex/copy/EEex_scripts/EEex_UDAux.lua new file mode 100644 index 00000000..18321af3 --- /dev/null +++ b/EEex/copy/EEex_scripts/EEex_UDAux.lua @@ -0,0 +1,386 @@ + +if EEex_UDAux_AlreadyLoaded then + return +end +EEex_UDAux_AlreadyLoaded = true + +function EEex_UDAux_Private_AssertPersistentValue(value, path, seenTables) + -- Save data must stay in the shared marshal codec's safe domain. Return a deep copy so + -- later Lua mutations cannot change the already-validated payload while native code writes it. + local valueType = type(value) + if valueType == "boolean" then + return value + elseif valueType == "string" then + if value:find("\0", 1, true) then + EEex_Error("Persistent UDAux string at "..path.." cannot contain NUL bytes") + end + return value + elseif valueType == "number" then + if value ~= value or value % 1 ~= 0 then + EEex_Error("Persistent UDAux value at "..path.." must be an integer number") + end + if value < -0x10000000000000000 or value > 0xFFFFFFFFFFFFFFFF then + EEex_Error("Persistent UDAux number at "..path.." is outside the supported marshal range") + end + return value + elseif valueType ~= "table" then + EEex_Error("Persistent UDAux value at "..path.." has unsupported type "..valueType) + end + + if seenTables[value] then + EEex_Error("Persistent UDAux table cycle detected at "..path) + end + seenTables[value] = true + + local copy = {} + for k, v in pairs(value) do + local kType = type(k) + if kType ~= "boolean" and kType ~= "number" and kType ~= "string" then + EEex_Error("Persistent UDAux key at "..path.." has unsupported type "..kType) + end + local keyPath = path.."["..tostring(k).."]" + copy[EEex_UDAux_Private_AssertPersistentValue(k, path.."", seenTables)] = + EEex_UDAux_Private_AssertPersistentValue(v, keyPath, seenTables) + end + + seenTables[value] = nil + return copy +end + +function EEex_UDAux_Private_IsLightUDPersistent(lud) + return EEex_UserDataAuxiliaryPersistent[lud] == true +end + +function EEex_UDAux_Private_IsPersistent(ud) + return EEex_UDAux_Private_IsLightUDPersistent(EEex_UDToLightUD(ud)) +end + +function EEex_UDAux_Private_Export(ud, path) + local lud = EEex_UDToLightUD(ud) + if not EEex_UDAux_Private_IsLightUDPersistent(lud) then + return nil + end + + local auxiliary = EEex_UserDataAuxiliary[lud] + if not auxiliary or next(auxiliary) == nil then + return nil + end + return EEex_UDAux_Private_AssertPersistentValue(auxiliary, path or "persistent UDAux", {}) +end + +function EEex_UDAux_Private_Import(ud, auxiliary) + local lud = EEex_UDToLightUD(ud) + EEex_UserDataAuxiliary[lud] = EEex_UDAux_Private_AssertPersistentValue(auxiliary, "persistent UDAux import", {}) + EEex_UserDataAuxiliaryPersistent[lud] = true + return EEex_UserDataAuxiliary[lud] +end + +function EEex_UDAux_Private_CopyByLightUD(srcLud, dstLud) + local srcAuxiliary = EEex_UserDataAuxiliary[srcLud] + if EEex_UserDataAuxiliaryPersistent[srcLud] and srcAuxiliary then + EEex_UserDataAuxiliary[dstLud] = EEex_UDAux_Private_AssertPersistentValue(srcAuxiliary, "persistent UDAux copy", {}) + else + EEex_UserDataAuxiliary[dstLud] = nil + end + + if EEex_UserDataAuxiliaryPersistent[srcLud] then + EEex_UserDataAuxiliaryPersistent[dstLud] = true + else + EEex_UserDataAuxiliaryPersistent[dstLud] = nil + end +end + +function EEex_UDAux_Private_DeleteByLightUD(lud) + EEex_UserDataAuxiliary[lud] = nil + EEex_UserDataAuxiliaryPersistent[lud] = nil +end + +EEex_UDAux_Private_AreaMarshalData = nil +EEex_UDAux_Private_AreaMarshalContainerIndex = 0 +EEex_UDAux_Private_AreaUnmarshalContainers = nil +EEex_UDAux_Private_AreaUnmarshalContainerIndex = 0 +EEex_UDAux_Private_StoreMarshalData = nil +EEex_UDAux_Private_StoreUnmarshalAux = nil +EEex_UDAux_Private_StoreUnmarshalItems = nil + +function EEex_UDAux_Private_ForEachContainerItem(container, func) + -- Container items are saved in linked-list order, matching the order used by the area file. + local node = container.m_lstItems.m_pNodeHead + local index = 1 + while node do + local item = node.data + if item then + func(index, item) + end + node = node.pNext + index = index + 1 + end +end + +function EEex_UDAux_Private_ForEachStoreItem(storeItemPtrs, func) + -- Native code snapshots ordinal -> pointer because m_lInventory is a native linked list. + for index, itemPtr in pairs(storeItemPtrs) do + if itemPtr ~= 0 then + func(index, EEex_PtrToUD(itemPtr, "CStoreFileItem")) + end + end +end + +function EEex_UDAux_Private_NewAreaMarshalBundle(marshalData) + local bundle = EEex_Marshal_Private_NewTableBundle() + EEex_Marshal_Private_AddTableBundleEntry(bundle, "EEex_UDAux_Area", marshalData, "Area UDAux marshal") + return bundle +end + +function EEex_UDAux_Private_NewStoreMarshalBundle(marshalData) + local bundle = EEex_Marshal_Private_NewTableBundle() + EEex_Marshal_Private_AddTableBundleEntry(bundle, "EEex_UDAux_Store", marshalData, "Store UDAux marshal") + return bundle +end + +function EEex_UDAux_Private_BeginAreaMarshal(area) + -- Area marshal state is filled by CGameContainer::Marshal callbacks as the engine walks containers. + EEex_UDAux_Private_AreaMarshalData = { + ["containers"] = {}, + } + EEex_UDAux_Private_AreaMarshalContainerIndex = 0 +end + +function EEex_UDAux_Private_OnAreaContainerMarshal(container) + local marshalData = EEex_UDAux_Private_AreaMarshalData + if not marshalData then + return + end + + EEex_UDAux_Private_AreaMarshalContainerIndex = EEex_UDAux_Private_AreaMarshalContainerIndex + 1 + local containerIndex = EEex_UDAux_Private_AreaMarshalContainerIndex + local containerData = {} + + -- Persist by ordinal, not pointer. Pointers are process-local and change after load. + local containerAux = EEex_UDAux_Private_Export(container, "CGameArea.m_lContainers["..containerIndex.."]") + if containerAux then + containerData["aux"] = containerAux + end + + local itemData = {} + EEex_UDAux_Private_ForEachContainerItem(container, function(itemIndex, item) + local itemAux = EEex_UDAux_Private_Export(item, "CGameArea.m_lContainers["..containerIndex.."].m_lstItems["..itemIndex.."]") + if itemAux then + itemData[itemIndex] = itemAux + end + end) + + if next(itemData) then + containerData["items"] = itemData + end + if next(containerData) then + marshalData["containers"][containerIndex] = containerData + end +end + +function EEex_UDAux_Private_CalculateAreaMarshalExtensionSize() + local marshalData = EEex_UDAux_Private_AreaMarshalData + if not marshalData or next(marshalData["containers"]) == nil then + return 0 + end + return EEex_Marshal_Private_CalculateTableBundleSize(EEex_UDAux_Private_NewAreaMarshalBundle(marshalData), "Area UDAux marshal") +end + +function EEex_UDAux_Private_WriteAreaMarshalExtensionPayload(memory, size) + local marshalData = EEex_UDAux_Private_AreaMarshalData + if not marshalData then + return + end + EEex_Memset(memory, 0, size) + EEex_Marshal_Private_WriteTableBundle(memory, EEex_UDAux_Private_NewAreaMarshalBundle(marshalData), "Area UDAux marshal") +end + +function EEex_UDAux_Private_EndAreaMarshal() + EEex_UDAux_Private_AreaMarshalData = nil + EEex_UDAux_Private_AreaMarshalContainerIndex = 0 +end + +function EEex_UDAux_Private_BeginAreaUnmarshal(memory, size) + -- Keep decoded data until each CGameContainer constructor callback supplies the matching runtime object. + EEex_UDAux_Private_AreaUnmarshalContainers = {} + EEex_UDAux_Private_AreaUnmarshalContainerIndex = 0 + + if memory == 0 or size == 0 then + return + end + + EEex_Marshal_Private_ReadTableBundle(memory, function(handlerStr, toFill) + if handlerStr == "EEex_UDAux_Area" and type(toFill) == "table" then + EEex_UDAux_Private_AreaUnmarshalContainers = toFill["containers"] or {} + end + end, size) +end + +function EEex_UDAux_Private_OnAreaContainerConstruct(container) + local containers = EEex_UDAux_Private_AreaUnmarshalContainers + if not containers then + return + end + + EEex_UDAux_Private_AreaUnmarshalContainerIndex = EEex_UDAux_Private_AreaUnmarshalContainerIndex + 1 + local containerData = containers[EEex_UDAux_Private_AreaUnmarshalContainerIndex] + if not containerData then + return + end + + local containerAux = containerData["aux"] + if containerAux then + EEex_UDAux_Private_Import(container, containerAux) + end + + local items = containerData["items"] + if items then + EEex_UDAux_Private_ForEachContainerItem(container, function(itemIndex, item) + local itemAux = items[itemIndex] + if itemAux then + EEex_UDAux_Private_Import(item, itemAux) + end + end) + end +end + +function EEex_UDAux_Private_EndAreaUnmarshal() + EEex_UDAux_Private_AreaUnmarshalContainers = nil + EEex_UDAux_Private_AreaUnmarshalContainerIndex = 0 +end + +function EEex_UDAux_Private_CalculateStoreMarshalExtensionSize(store, storeItemPtrs) + EEex_UDAux_Private_StoreMarshalData = nil + if storeItemPtrs == nil then + storeItemPtrs = store + store = nil + end + + local marshalData = { + ["items"] = {}, + } + + if store then + local storeAux = EEex_UDAux_Private_Export(store, "CStore") + if storeAux then + marshalData["aux"] = storeAux + end + end + + EEex_UDAux_Private_ForEachStoreItem(storeItemPtrs, function(itemIndex, item) + -- Store-owned CStoreFileItem pointers are runtime list nodes; save their aux by list ordinal. + local itemAux = EEex_UDAux_Private_Export(item, "CStore.m_lInventory["..itemIndex.."]") + if itemAux then + marshalData["items"][itemIndex] = itemAux + end + end) + + if not marshalData["aux"] and next(marshalData["items"]) == nil then + return 0 + end + + EEex_UDAux_Private_StoreMarshalData = marshalData + return EEex_Marshal_Private_CalculateTableBundleSize(EEex_UDAux_Private_NewStoreMarshalBundle(marshalData), "Store UDAux marshal") +end + +function EEex_UDAux_Private_WriteStoreMarshalExtensionPayload(memory, size) + local marshalData = EEex_UDAux_Private_StoreMarshalData + if not marshalData then + return + end + EEex_Memset(memory, 0, size) + EEex_Marshal_Private_WriteTableBundle(memory, EEex_UDAux_Private_NewStoreMarshalBundle(marshalData), "Store UDAux marshal") +end + +function EEex_UDAux_Private_EndStoreMarshal() + EEex_UDAux_Private_StoreMarshalData = nil +end + +function EEex_UDAux_Private_BeginStoreUnmarshal(memory, size) + -- Store SetResRef has not populated m_lInventory yet; keep decoded data until the after-load hook. + EEex_UDAux_Private_StoreUnmarshalAux = nil + EEex_UDAux_Private_StoreUnmarshalItems = {} + + if memory == 0 or size == 0 then + return + end + + EEex_Marshal_Private_ReadTableBundle(memory, function(handlerStr, toFill) + if handlerStr == "EEex_UDAux_Store" and type(toFill) == "table" then + EEex_UDAux_Private_StoreUnmarshalAux = toFill["aux"] + EEex_UDAux_Private_StoreUnmarshalItems = toFill["items"] or {} + end + end, size) +end + +function EEex_UDAux_Private_OnStoreLoaded(store, storeItemPtrs) + if storeItemPtrs == nil then + storeItemPtrs = store + store = nil + end + + local storeAux = EEex_UDAux_Private_StoreUnmarshalAux + if storeAux and store then + EEex_UDAux_Private_Import(store, storeAux) + end + + local items = EEex_UDAux_Private_StoreUnmarshalItems + if not items then + return + end + + EEex_UDAux_Private_ForEachStoreItem(storeItemPtrs, function(itemIndex, item) + local itemAux = items[itemIndex] + if itemAux then + EEex_UDAux_Private_Import(item, itemAux) + end + end) +end + +function EEex_UDAux_Private_EndStoreUnmarshal() + EEex_UDAux_Private_StoreUnmarshalAux = nil + EEex_UDAux_Private_StoreUnmarshalItems = nil +end + +function EEex_UDAux_Private_OnStoreInventoryClear(store, storeItemPtrs) + if storeItemPtrs == nil then + storeItemPtrs = store + store = nil + end + + -- This runs before the engine frees CStoreFileItem nodes, so deleting by pointer is still valid. + if store then + EEex_UDAux_Private_DeleteByLightUD(EEex_UDToLightUD(store)) + end + EEex_UDAux_Private_ForEachStoreItem(storeItemPtrs, function(_, item) + EEex_UDAux_Private_DeleteByLightUD(EEex_UDToLightUD(item)) + end) +end + +function EEex_UDAux_Private_ApplyPendingItemAux(sprite) + local spriteAux = EEex_TryGetUDAux(sprite) + local pendingItems = spriteAux and spriteAux["EEex_UDAux_PendingItemAux"] + if not pendingItems then + return + end + + local anyPending = false + for i = 0, 38 do + local auxiliary = pendingItems[i] + if auxiliary then + -- Creature extra marshal data can arrive before equipment pointers are rebuilt. + -- Retry from load/list-resolved callbacks until every saved slot has a CItem. + local item = sprite.m_equipment.m_items:get(i) + if item then + EEex_UDAux_Private_Import(item, auxiliary) + pendingItems[i] = nil + else + anyPending = true + end + end + end + + if not anyPending then + spriteAux["EEex_UDAux_PendingItemAux"] = nil + end +end diff --git a/EEex/loader/InfinityLoader.db b/EEex/loader/InfinityLoader.db index 0b182a93..69335230 100644 --- a/EEex/loader/InfinityLoader.db +++ b/EEex/loader/InfinityLoader.db @@ -2142,6 +2142,50 @@ Operations=ADD 26 Pattern=448B9934060000 Operations=ADD -15 +[Hook-CGameArea::Marshal(unsigned char**,unsigned int*,unsigned char)-FirstInstruction] +Pattern=4157488DAC2488FEFFFF +Operations=ADD -8 + +[Hook-CGameArea::Marshal(unsigned char**,unsigned int*,unsigned char)-AfterMarshal] +Pattern=4C8BA42470020000488B8D60010000 +Operations=ADD 0 + +[Hook-CGameArea::Unmarshal(unsigned char*,unsigned int,unsigned char)-FirstInstruction] +Pattern=405556574154415541564157488DAC24E0FEFFFF +Operations=ADD 0 + +[Hook-CGameArea::Unmarshal(unsigned char*,unsigned int,unsigned char)-AfterUnmarshal] +Pattern=488B9C2470020000418BC4 +Operations=ADD 0 + +[Hook-CGameContainer::CGameContainer(CGameArea*,CAreaFileContainer*,...)-AfterConstruct] +Pattern=498BC5488B4D704833CC +Operations=ADD 0 + +[Hook-CGameContainer::Marshal(SAreaFileWrapper*)-FirstInstruction] +Pattern=4889542410555741564157 +Operations=ADD 0 + +[Hook-CStore::~CStore()-FirstInstruction] +Pattern=4883EC2083B9B800000000 +Operations=ADD -6 + +[Hook-CStore::Marshal()-dimmServiceFromMemory] +Pattern=41B101448BC5 +Operations=ADD 9 + +[Hook-CStore::SetResRef(CResRef const&)-BeforeInventoryClear] +Pattern=4439BBB80000007426 +Operations=ADD 0 + +[Hook-CStore::SetResRef(CResRef const&)-BeforeHeaderRead] +Pattern=488B4C2420488BF8 +Operations=ADD 18 + +[Hook-CStore::SetResRef(CResRef const&)-AfterInventoryPopulate] +Pattern=4C8B7C2440488B7C2450 +Operations=ADD -6 + [Hook-CGameContainer::Render()-B3EmptyContainer] Pattern=85C00F8893000000 Operations=ADD 150