diff --git a/EEex/copy/EEex_Opcode.lua b/EEex/copy/EEex_Opcode.lua index e92b6d2..61c03ca 100644 --- a/EEex/copy/EEex_Opcode.lua +++ b/EEex/copy/EEex_Opcode.lua @@ -15,6 +15,79 @@ end -- Private Functions -- ----------------------- +local EEex_Opcode_Private_Op346ExtendedBonusesAuxKey = "EEex_Opcode_Op346_ExtendedBonuses" +local EEex_Opcode_Private_Op346VanillaSchoolCount = 12 +local EEex_Opcode_Private_Op346MaxSchool = 0xFF -- Effect source school fields are 8-bit in the engine data structures. + +local function EEex_Opcode_Private_Op346NormalizeInt16(value) + -- op346 writes 16-bit stats in the engine, so mirror its signed wraparound semantics here. + value = EEex_BAnd(value, 0xFFFF) + if value >= 0x8000 then + value = value - 0x10000 + end + return value +end + +local function EEex_Opcode_Private_Op346GetBonuses(sprite, create) + -- Extended schools have no native CDerivedStats slots, so cache their resolved bonuses in sprite aux data. + local auxiliary = create and EEex_GetUDAux(sprite) or EEex_TryGetUDAux(sprite) + if auxiliary == nil then + return nil + end + + local bonuses = auxiliary[EEex_Opcode_Private_Op346ExtendedBonusesAuxKey] + if bonuses == nil and create then + bonuses = {} + auxiliary[EEex_Opcode_Private_Op346ExtendedBonusesAuxKey] = bonuses + end + return bonuses +end + +function EEex_Opcode_Hook_ClearOp346ExtendedBonuses(sprite) + -- This cache is derived from active effects, so throw it away whenever stats are rebuilt or the sprite dies. + local auxiliary = EEex_TryGetUDAux(sprite) + if auxiliary ~= nil then + auxiliary[EEex_Opcode_Private_Op346ExtendedBonusesAuxKey] = nil + end +end + +function EEex_Opcode_Hook_OnOp346ApplyEffect(effect, sprite) + + local school = effect.m_special + if school < EEex_Opcode_Private_Op346VanillaSchoolCount or school > EEex_Opcode_Private_Op346MaxSchool then + return false + end + + -- Preserve the engine's add/set behavior, but redirect rows 12..255 into EEex-managed storage. + local bonuses = EEex_Opcode_Private_Op346GetBonuses(sprite, true) + local amount = EEex_Opcode_Private_Op346NormalizeInt16(effect.m_effectAmount) + local modType = effect.m_dWFlags + + if modType == 0 then + bonuses[school] = EEex_Opcode_Private_Op346NormalizeInt16((bonuses[school] or 0) + amount) + elseif modType == 1 then + bonuses[school] = amount + end + + return true +end + +function EEex_Opcode_Hook_GetOp346SaveVsSchoolBonus(effect, sprite) + + local school = effect.m_school + if school < EEex_Opcode_Private_Op346VanillaSchoolCount or school > EEex_Opcode_Private_Op346MaxSchool then + return 0 + end + + -- Saving throws read the incoming effect's spell school, so use that as the key into the extended cache. + local bonuses = EEex_Opcode_Private_Op346GetBonuses(sprite, false) + if bonuses == nil then + return 0 + end + + return bonuses[school] or 0 +end + function EEex_Opcode_Private_ApplyExtraMeleeEffects(sprite, targetSprite) EEex_Utility_IterateCPtrList(sprite:getActiveStats().m_cExtraMeleeEffects, function(effect) diff --git a/EEex/copy/EEex_Opcode_Patch.lua b/EEex/copy/EEex_Opcode_Patch.lua index 0167106..2236152 100644 --- a/EEex/copy/EEex_Opcode_Patch.lua +++ b/EEex/copy/EEex_Opcode_Patch.lua @@ -504,6 +504,105 @@ }) ) + --[[ + +---------------------------------------------------------------------------------------------------+ + | Opcode 0x15A (CGameEffectSaveVsSchoolMod) | + +---------------------------------------------------------------------------------------------------+ + | special -> MSCHOOL.2DA row. The engine only applies rows 0..11 directly; EEex extends support | + | to the remaining 8-bit school range and feeds the result back into saving throws. | + +---------------------------------------------------------------------------------------------------+ + | [Lua] EEex_Opcode_Hook_OnOp346ApplyEffect(effect: CGameEffect, sprite: CGameSprite) -> boolean | + | return: | + | -> false - Preserve vanilla failure path | + | -> true - Treat the effect as handled | + +---------------------------------------------------------------------------------------------------+ + | [Lua] EEex_Opcode_Hook_GetOp346SaveVsSchoolBonus(effect: CGameEffect, sprite: CGameSprite) | + | -> int | + +---------------------------------------------------------------------------------------------------+ + --]] + + local op346ApplyEffect = EEex_Label("CGameEffectSaveVsSchoolMod::ApplyEffect") + local op346CheckSaveSkipBonusJmp = EEex_Label("Hook-CGameEffect::CheckSave()-Op346SkipBonusJmp") + + -- Hook the function entry instead of the out-of-bounds branch so unhandled cases can cleanly + -- restore the original 11-byte prefix, while handled extended schools return before vanilla's 0..11 gate. + EEex_HookBeforeRestoreWithLabels(op346ApplyEffect, 0, 11, 11, { + {"stack_mod", 8}, + {"hook_integrity_watchdog_ignore_registers", { + EEex_HookIntegrityWatchdogRegister.RAX, EEex_HookIntegrityWatchdogRegister.R8, EEex_HookIntegrityWatchdogRegister.R9, + EEex_HookIntegrityWatchdogRegister.R10, EEex_HookIntegrityWatchdogRegister.R11 + }}}, + EEex_FlattenTable({ + {[[ + #MAKE_SHADOW_SPACE(64) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], rdx + ]]}, + EEex_GenLuaCall("EEex_Opcode_Hook_OnOp346ApplyEffect", { + ["args"] = { + function(rspOffset) return {"mov qword ptr ss:[rsp+#$(1)], rcx #ENDL", {rspOffset}}, "CGameEffect" end, + function(rspOffset) return {"mov qword ptr ss:[rsp+#$(1)], rdx #ENDL", {rspOffset}}, "CGameSprite" end, + }, + ["returnType"] = EEex_LuaCallReturnType.Boolean, + }), + {[[ + jmp no_error + + call_error: + xor eax, eax + + no_error: + test rax, rax + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + jz #L(return) + + mov eax, 1 + #MANUAL_HOOK_EXIT(1) + ret + ]]}, + }) + ) + -- Manually define the ignored registers for the unusual `ret` above + EEex_HookIntegrityWatchdog_IgnoreRegistersForInstance(op346ApplyEffect, 1, { + EEex_HookIntegrityWatchdogRegister.RAX, EEex_HookIntegrityWatchdogRegister.RCX, EEex_HookIntegrityWatchdogRegister.RDX, + EEex_HookIntegrityWatchdogRegister.R8, EEex_HookIntegrityWatchdogRegister.R9, EEex_HookIntegrityWatchdogRegister.R10, + EEex_HookIntegrityWatchdogRegister.R11 + }) + + -- This branch is where vanilla skips the op346 school bonus when the incoming spell school is >= 12. + -- Inject the EEex-managed bonus there so rows 12..255 participate in the normal save calculation. + EEex_HookConditionalJumpOnSuccessWithLabels(op346CheckSaveSkipBonusJmp, 7, { + {"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 + }}}, + EEex_FlattenTable({ + {[[ + #MAKE_SHADOW_SPACE(48) + ]]}, + EEex_GenLuaCall("EEex_Opcode_Hook_GetOp346SaveVsSchoolBonus", { + ["args"] = { + function(rspOffset) return {"mov qword ptr ss:[rsp+#$(1)], rbx #ENDL", {rspOffset}}, "CGameEffect" end, + function(rspOffset) return {"mov qword ptr ss:[rsp+#$(1)], rsi #ENDL", {rspOffset}}, "CGameSprite" end, + }, + ["returnType"] = EEex_LuaCallReturnType.Number, + }), + {[[ + jmp no_error + + call_error: + xor eax, eax + + no_error: + #DESTROY_SHADOW_SPACE + add edi, eax + ]]}, + }) + ) + --[[ +------------------------------------------------------------------------------------------------+ | Allow saving throw BIT23 to bypass opcode #101 | diff --git a/EEex/copy/EEex_Sprite_Patch.lua b/EEex/copy/EEex_Sprite_Patch.lua index 8a9fd16..fc8c04c 100644 --- a/EEex/copy/EEex_Sprite_Patch.lua +++ b/EEex/copy/EEex_Sprite_Patch.lua @@ -73,10 +73,42 @@ EEex_HookAfterCallWithLabels(EEex_Label("Hook-CGameSprite::Destruct()-FirstCall"), { {"hook_integrity_watchdog_ignore_registers", {EEex_HookIntegrityWatchdogRegister.RAX}}}, - {[[ - mov rcx, rbx ; pSprite - call #L(EEex::Sprite_Hook_OnDestruct) - ]]} + EEex_FlattenTable({ + {[[ + ; The op346 extended-school cache is stored in sprite aux data, but this hook runs from the + ; native destructor path, so save the volatile registers before calling back out to EEex/Lua. + #MAKE_SHADOW_SPACE(96) + 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 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-40)], r10 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-48)], r11 + + mov rcx, rbx ; pSprite + call #L(EEex::Sprite_Hook_OnDestruct) + ]]}, + -- Stats reload clears this cache when a live sprite's derived state is rebuilt. This second cleanup site + -- handles the separate lifetime case where the sprite object itself is being destroyed before another reload. + -- Clearing the aux entry here ensures the EEex-managed op346 rows 12..255 never outlive the sprite object. + EEex_GenLuaCall("EEex_Opcode_Hook_ClearOp346ExtendedBonuses", { + ["args"] = { + function(rspOffset) return {"mov qword ptr ss:[rsp+#$(1)], rbx #ENDL", {rspOffset}}, "CGameSprite" end, + }, + }), + {[[ + call_error: + ; Common exit path for both success and Lua-call failure: restore the volatile registers we saved + ; above so the engine destructor continues with the expected call-clobbered register state. + mov r11, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-48)] + mov r10, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-40)] + 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 + ]]}, + }) ) --[[ diff --git a/EEex/copy/EEex_Stats_Patch.lua b/EEex/copy/EEex_Stats_Patch.lua index cacdcbd..b5793e4 100644 --- a/EEex/copy/EEex_Stats_Patch.lua +++ b/EEex/copy/EEex_Stats_Patch.lua @@ -44,22 +44,61 @@ --]] local statsReloadTemplate = function(spriteRegStr) - return {[[ - mov rcx, #$(1) ]], {spriteRegStr}, [[ ; pSprite - call #L(EEex::Stats_Hook_OnReload) - ]]} + return EEex_FlattenTable({ + {[[ + ; Extended op346 schools are cached in sprite aux data instead of CDerivedStats, but this + ; hook runs inside native reload code, so preserve the volatile register set around the calls below. + ; This shared template now owns the shadow-space frame for every reload hook site that reuses it. + ; Keeping #MAKE_SHADOW_SPACE / #DESTROY_SHADOW_SPACE here avoids duplicating or double-freeing that + ; frame in wrapper trampolines like the special `rbx` caller below. + #MAKE_SHADOW_SPACE(96) + 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 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-40)], r10 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-48)], r11 + ]]}, + {[[ + mov rcx, #$(1) ]], {spriteRegStr}, [[ ; pSprite + call #L(EEex::Stats_Hook_OnReload) + ]]}, + -- Vanilla op346 rows 0..11 rebuild into real CDerivedStats fields during reload. EEex rows 12..255 + -- live in sprite aux storage instead, so clear that derived cache here before the rebuilt effect state + -- starts using it again. This handles the "same sprite, new stats state" lifetime case. + EEex_GenLuaCall("EEex_Opcode_Hook_ClearOp346ExtendedBonuses", { + ["args"] = { + function(rspOffset) return {"mov qword ptr ss:[rsp+#$(1)], "..spriteRegStr.." #ENDL", {rspOffset}}, "CGameSprite" end, + }, + }), + {[[ + call_error: + ; Common exit path for both success and Lua-call failure: restore the volatile registers we saved + ; above so the surrounding engine reload code resumes with its expected call-clobbered state. + mov r11, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-48)] + mov r10, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-40)] + 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 + ]]}, + }) end local callStatsReloadRbx = {"call #$(1) #ENDL", { EEex_JITNear(EEex_FlattenTable({ {[[ + ; This helper is entered via a real CALL, so the pushed return address shifts the stack by 8 bytes. + ; Only the alignment hint stays here: statsReloadTemplate() itself allocates and destroys the + ; shared shadow-space frame, so doing that again in this trampoline would double-adjust rsp. #STACK_MOD(8) ; This was called, the ret ptr broke alignment - #MAKE_SHADOW_SPACE ]]}, statsReloadTemplate("rbx"), {[[ - #DESTROY_SHADOW_SPACE + ; The shared template has already restored registers and destroyed its shadow space, so this + ; trampoline only needs to return to the original caller once the `rbx`-based reload work is done. ret ]]}, })), diff --git a/EEex/loader/InfinityLoader.db b/EEex/loader/InfinityLoader.db index 4d0aa32..5a63126 100644 --- a/EEex/loader/InfinityLoader.db +++ b/EEex/loader/InfinityLoader.db @@ -992,6 +992,14 @@ Type=LIST Pattern=4889442470410FB600 Operations=ADD -28 +; Vanilla branches here to skip op346's native school bonus when the incoming spell school is >= 12. +[Hook-CGameEffect::CheckSave()-Op346SkipBonusJmp] +Pattern=732183BEA44E000000B820110000BAC81D00000F44C24803C60FBF8C480C02000003F9 + +; Full CGameEffectSaveVsSchoolMod::ApplyEffect body used as the anchor for the function-entry op346 hook. +[CGameEffectSaveVsSchoolMod::ApplyEffect] +Pattern=448B49484C8BC14183F90C720333C0C38B492085C9741683F9017521410FB7401C664289844A2C1300008BC1C3410FB7401C4A8D0C4A6601817C2C0000B801000000C3 + [CGameEffect::Construct] Pattern=4889442428488BD9418BF1 Operations=ADD -23