Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions EEex/copy/EEex_Opcode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
99 changes: 99 additions & 0 deletions EEex/copy/EEex_Opcode_Patch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
40 changes: 36 additions & 4 deletions EEex/copy/EEex_Sprite_Patch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
]]},
})
)

--[[
Expand Down
51 changes: 45 additions & 6 deletions EEex/copy/EEex_Stats_Patch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
]]},
})),
Expand Down
8 changes: 8 additions & 0 deletions EEex/loader/InfinityLoader.db
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down