From 65aa1bb3a0314c2cad31c37e1a00fc4205b6060f Mon Sep 17 00:00:00 2001 From: 4Luke4 Date: Sat, 7 Mar 2026 23:56:45 +0100 Subject: [PATCH] Opcode 65 Add an option to make it truly cosmetic. --- EEex/copy/EEex_Opcode.lua | 44 +++++++++++ EEex/copy/EEex_Opcode_Patch.lua | 134 ++++++++++++++++++++++++++++++++ EEex/loader/InfinityLoader.db | 10 +++ 3 files changed, 188 insertions(+) diff --git a/EEex/copy/EEex_Opcode.lua b/EEex/copy/EEex_Opcode.lua index e92b6d2..f65b727 100644 --- a/EEex/copy/EEex_Opcode.lua +++ b/EEex/copy/EEex_Opcode.lua @@ -129,6 +129,50 @@ function EEex_Opcode_LuaHook_AfterListsResolved(sprite) end end +-- Track sprites that actually executed opcode 65's BIT0 ApplyEffect() path during +-- the current ProcessEffectList() pass. This uses the engine's real execution path +-- instead of trying to reconstruct blur ownership from effect-list state after the +-- fact, which proved unreliable for both equipped effects and temporary driven +-- child effects. +local EEex_Opcode_Private_Op65BlurVisualAppliedThisPass = {} + +function EEex_Opcode_Hook_OnOp65BlurBit0AppliedThisPass(sprite) + + -- Key by sprite address rather than sprite id / resref. This marker is purely a + -- short-lived runtime fact: "opcode 65 BIT0 really did execute on this sprite + -- during the current effect-processing pass". + EEex_Opcode_Private_Op65BlurVisualAppliedThisPass[EEex_UDToPtr(sprite)] = true +end + +function EEex_Opcode_Hook_ShouldKeepOp65BlurVisual(sprite) + + -- ProcessEffectList() normally treats the sprite's blur distortion as a pure + -- reflection of STATE_BLUR. For opcode 65 BIT0, preserve that distortion only if a + -- real CGameEffectBlur::ApplyEffect() just ran for this sprite during the same + -- pass. If no such application happened, the distortion should clear normally. + local spriteAddress = EEex_UDToPtr(sprite) + if EEex_Opcode_Private_Op65BlurVisualAppliedThisPass[spriteAddress] then + EEex_Opcode_Private_Op65BlurVisualAppliedThisPass[spriteAddress] = nil + return true + end + + return false +end + +-- The blur-clear sync runs before the existing AfterListsResolved hook points in +-- CGameSprite::ProcessEffectList(). Clearing the marker here makes it strictly +-- "this pass only": the blur-sync hook can see a real opcode 65 BIT0 application +-- from the current pass, but mixed sequences (for example equipped + limited blur) +-- cannot leak a stale marker into a later removal pass and keep the distortion on. +EEex_Opcode_AddListsResolvedListener(function(sprite) + -- Sanity check + if not (EEex_GameObject_IsSprite(sprite) and sprite.m_pArea) then + return + end + -- + EEex_Opcode_Private_Op65BlurVisualAppliedThisPass[EEex_UDToPtr(sprite)] = nil +end) + --[[ +--------------------------------------------------------------------------------+ | Opcode #214 | diff --git a/EEex/copy/EEex_Opcode_Patch.lua b/EEex/copy/EEex_Opcode_Patch.lua index 0167106..91b3b76 100644 --- a/EEex/copy/EEex_Opcode_Patch.lua +++ b/EEex/copy/EEex_Opcode_Patch.lua @@ -85,6 +85,140 @@ -- Opcode Changes -- -------------------------------------- + --[[ + +----------------------------------------------------------------------------------------------------------+ + | Opcode #65 | + +----------------------------------------------------------------------------------------------------------+ + | param1 BIT0 -> Skip setting STATE_BLUR while still applying the rest of the opcode | + +----------------------------------------------------------------------------------------------------------+ + --]] + + -- EEex_HookNOPs() is the right fit here because the engine instruction we are + -- replacing is a single, isolated 5-byte "or [stateFlags], STATE_BLUR". We do not + -- need to redirect surrounding control flow; we only need to conditionally suppress + -- that one write and otherwise fall through exactly where the engine already was. + -- Keeping this hook minimal also avoids another Lua call at a fragile instruction- + -- replacement site; execution tracking for the special BIT0 path is handled by a + -- separate function-entry hook below. + EEex_HookNOPs(EEex_Label("Hook-CGameEffectBlur::ApplyEffect()-SetStateBlur"), 5, {[[ + test dword ptr ds:[rcx+0x1C], 1 ; effect->m_effectAmount BIT0 + jz #L(apply_state_blur) ; BIT0 clear -> replay the stock STATE_BLUR write below + + jmp #L(return) ; BIT0 means "apply opcode 65, but do not mark STATE_BLUR" + + apply_state_blur: + or dword ptr ds:[rdx+0x1120], 0x20000000 ; Default engine behavior when BIT0 is clear + ]]}) + + --[[ + +----------------------------------------------------------------------------------------------------------+ + | Opcode #65 | + +----------------------------------------------------------------------------------------------------------+ + | Track real opcode #65 BIT0 ApplyEffect() executions on each sprite during ProcessEffectList() | + +----------------------------------------------------------------------------------------------------------+ + | [Lua] EEex_Opcode_Hook_OnOp65BlurBit0AppliedThisPass(sprite: CGameSprite) | + +----------------------------------------------------------------------------------------------------------+ + --]] + + -- EEex_HookBeforeRestoreWithLabels() is the right fit here because this is a + -- normal function-entry hook on CGameEffectBlur::ApplyEffect(). That lets us record + -- the special BIT0 execution on the sprite from a stable call boundary instead of + -- trying to infer it later from effect-list state or from a brittle in-body patch. + -- The prologue here is "push rbx" (2 bytes) + "sub rsp, 0x20" (4 bytes), so the + -- hook must restore / skip the full 6-byte window rather than splitting the stack + -- setup instruction. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameEffectBlur::ApplyEffect()-FirstInstruction"), 0, 6, 6, { + {"stack_mod", 8}, + {"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({ + {[[ + test dword ptr ds:[rcx+0x1C], 1 ; effect->m_effectAmount BIT0 + jz #L(return) + + #MAKE_SHADOW_SPACE(48) ; Save the original effect / sprite args across the Lua helper call + 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_OnOp65BlurBit0AppliedThisPass", { + ["args"] = { + -- Pass the sprite currently being processed by CGameEffectBlur::ApplyEffect(). + -- ProcessEffectList() will consume this marker during the blur-clear sync + -- later in the same pass if STATE_BLUR was intentionally suppressed. + function(rspOffset) return { + "mov qword ptr ss:[rsp+#$(1)], rdx #ENDL", {rspOffset} + }, "CGameSprite" end, + }, + }), + {[[ + jmp no_error + + call_error: + no_error: + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]}, + }) + ) + + --[[ + +----------------------------------------------------------------------------------------------------------+ + | Opcode #65 | + +----------------------------------------------------------------------------------------------------------+ + | [Lua] EEex_Opcode_Hook_ShouldKeepOp65BlurVisual(sprite: CGameSprite) -> boolean | + +----------------------------------------------------------------------------------------------------------+ + --]] + + -- EEex_HookConditionalJumpOnFailWithLabels() is the right fit here because the + -- original site is itself a conditional branch in ProcessEffectList(): + -- "if STATE_BLUR is absent, go run the blur-clear path". We want to preserve that + -- existing branch structure and only synthesize a success case when Lua says a + -- real opcode 65 BIT0 ApplyEffect() just ran for this sprite during the current + -- pass. Using the conditional-jump hook keeps the engine's original success / fail + -- destinations intact, while the labels let this patch explicitly choose which + -- path to resume. + EEex_HookConditionalJumpOnFailWithLabels(EEex_Label("Hook-CGameSprite::ProcessEffectList()-BlurStateSyncJmp"), 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({ + {[[ + cmp byte ptr ds:[rsi+0x3F72], r12b ; If the visual blur is already off, there is nothing to preserve + je #L(jmp_fail) ; Follow the stock path and let the overwritten cmp replay normally + #MAKE_SHADOW_SPACE(40) ; One qword Lua arg + Win64 shadow space, torn down on every exit path below + ]]}, + EEex_GenLuaCall("EEex_Opcode_Hook_ShouldKeepOp65BlurVisual", { + ["args"] = { + -- Pass the current sprite so Lua can check whether opcode 65 BIT0 + -- executed on it earlier in this ProcessEffectList() pass. + function(rspOffset) return { + "mov qword ptr ss:[rsp+#$(1)], rsi #ENDL", {rspOffset} + }, "CGameSprite" end, + }, + ["returnType"] = EEex_LuaCallReturnType.Boolean, -- true => keep the distortion even without STATE_BLUR + }), + {[[ + jmp no_error ; Skip the fallback path when the Lua call completed successfully + + call_error: + xor rax, rax ; Treat Lua failure as "do not override engine behavior" + + no_error: + test rax, rax + + #DESTROY_SHADOW_SPACE ; LEA preserves flags, so the test result survives this stack restore + jz #L(jmp_fail) ; No qualifying opcode 65 effect: keep the original blur-clear path + mov eax, dword ptr ds:[rsi+0x1120] ; Rebuild the original EAX state snapshot before taking the success branch + jmp #L(jmp_success) ; Pretend the STATE_BLUR check succeeded so the visual blur is preserved + ]]}, + }) + ) + --[[ +--------------------------------------------------------------------------------------------------+ | Opcode #214 | diff --git a/EEex/loader/InfinityLoader.db b/EEex/loader/InfinityLoader.db index 4d0aa32..f9a8d75 100644 --- a/EEex/loader/InfinityLoader.db +++ b/EEex/loader/InfinityLoader.db @@ -1795,6 +1795,12 @@ Operations=ADD 3 Pattern=4C8BCF8B5320 Operations=ADD 9 +[Hook-CGameEffectBlur::ApplyEffect()-SetStateBlur] +Pattern=818A201100000000002080BA723F000000 + +[Hook-CGameEffectBlur::ApplyEffect()-FirstInstruction] +Pattern=40534883EC20F7827805000000080000488BDA7569 + [Hook-CGameEffectCopySelf::ApplyEffect()-CGameSprite::Copy()] Pattern=4533C948899C2440010000 Operations=ADD 64 @@ -1944,6 +1950,10 @@ Operations=ADD 51 Pattern=837D8C00 Operations=ADD 20 +[Hook-CGameSprite::ProcessEffectList()-BlurStateSyncJmp] +Pattern=0FBAE01D725D4438A6723F0000 +Operations=ADD 4 + [Hook-CGameSprite::ProcessEffectList()-AfterListsResolved-2] Pattern=837D8C00 Operations=ADD 29