From 67766626c50abcac302f98a777bff5d9587cb765 Mon Sep 17 00:00:00 2001 From: 4Luke4 Date: Sat, 4 Jul 2026 13:05:59 +0200 Subject: [PATCH] Add EEex_AttackOnce action and combat hooks --- EEex/copy/EEex_scripts/EEex_Action.lua | 14 ++ EEex/copy/EEex_scripts/EEex_Action_Patch.lua | 52 +++---- EEex/copy/EEex_scripts/EEex_Sprite.lua | 5 + EEex/copy/EEex_scripts/EEex_Sprite_Patch.lua | 140 +++++++++++++++++++ EEex/loader/InfinityLoader.db | 20 +++ EEex/patch/ACTION.IDS | 3 +- 6 files changed, 208 insertions(+), 26 deletions(-) diff --git a/EEex/copy/EEex_scripts/EEex_Action.lua b/EEex/copy/EEex_scripts/EEex_Action.lua index e754327..ca763aa 100644 --- a/EEex/copy/EEex_scripts/EEex_Action.lua +++ b/EEex/copy/EEex_scripts/EEex_Action.lua @@ -306,6 +306,20 @@ EEex_Action_Private_Switch = { [479] = function(aiBase, curAction) return EEex_Action_Private_SpellObjectOffset(aiBase, curAction, false, 337, CGameAIBase.ForceSpellPoint) end, + + -- EEex_AttackOnce + [480] = function(aiBase, curAction) + + if not aiBase:isSprite(true) then + return EEex_Action_ReturnType.ACTION_ERROR + end + + -- Store the EEex-only roll/damage/ammo state, then let vanilla Attack() + -- own target validation, movement, range, animation, and interruption. + EEex.StartAttackOnce(aiBase, aiBase:GetTargetShare(), curAction.m_specificID, curAction.m_specificID2, curAction.m_specificID3) + curAction.m_actionID = 3 + return aiBase:virtual_ExecuteAction() + end, } function EEex_Action_Hook_OnEvaluatingUnknown(aiBase) diff --git a/EEex/copy/EEex_scripts/EEex_Action_Patch.lua b/EEex/copy/EEex_scripts/EEex_Action_Patch.lua index b9d4bf7..0e02fd8 100644 --- a/EEex/copy/EEex_scripts/EEex_Action_Patch.lua +++ b/EEex/copy/EEex_scripts/EEex_Action_Patch.lua @@ -4,31 +4,33 @@ EEex_DisableCodeProtection() --[[ - +--------------------------------------------------------------------------------+ - | Implement new actions | - +--------------------------------------------------------------------------------+ - | 472 EEex_LuaAction(S:Chunk*) | - | 473 EEex_MatchObject(S:Chunk*) | - | 473 EEex_MatchObjectEx(S:Chunk*,I:Nth*,I:Range*,I:Flags*X-MATOBJ) | - | 474 EEex_SetTarget(S:Name*,O:Target*) | - | 476 EEex_SpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) | - | 476 EEex_SpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) | - | 477 EEex_SpellObjectOffsetNoDec(O:Target*,I:Spell*Spell,P:Offset*) | - | 477 EEex_SpellObjectOffsetNoDecRES(S:RES*,O:Target*,P:Offset*) | - | 478 EEex_ForceSpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) | - | 478 EEex_ForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) | - | 479 EEex_ReallyForceSpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) | - | 479 EEex_ReallyForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) | - +--------------------------------------------------------------------------------+ - | [Lua] EEex_Action_Hook_OnEvaluatingUnknown(evaluator: CGameAIBase) -> number | - | return -> Set as internal action return value: | - | -> EEex_Action_ReturnType.ACTION_STOPPED | - | -> EEex_Action_ReturnType.ACTION_ERROR | - | -> EEex_Action_ReturnType.ACTION_DONE | - | -> EEex_Action_ReturnType.ACTION_NORMAL | - | -> EEex_Action_ReturnType.ACTION_INTERRUPTABLE | - | -> EEex_Action_ReturnType.ACTION_NO_ACTION | - +--------------------------------------------------------------------------------+ + +---------------------------------------------------------------------------------+ + | Implement new actions | + +---------------------------------------------------------------------------------+ + | 472 EEex_LuaAction(S:Chunk*) | + | 473 EEex_MatchObject(S:Chunk*) | + | 473 EEex_MatchObjectEx(S:Chunk*,I:Nth*,I:Range*,I:Flags*X-MATOBJ) | + | 474 EEex_SetTarget(S:Name*,O:Target*) | + | 476 EEex_SpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) | + | 476 EEex_SpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) | + | 477 EEex_SpellObjectOffsetNoDec(O:Target*,I:Spell*Spell,P:Offset*) | + | 477 EEex_SpellObjectOffsetNoDecRES(S:RES*,O:Target*,P:Offset*) | + | 478 EEex_ForceSpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) | + | 478 EEex_ForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) | + | 479 EEex_ReallyForceSpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) | + | 479 EEex_ReallyForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) | + | 480 EEex_AttackOnce(O:Target*,I:BaseAttackRollMod*, | + | I:OverrideBaseDamageRoll*,I:ConsumeAmmo*Boolean) | + +---------------------------------------------------------------------------------+ + | [Lua] EEex_Action_Hook_OnEvaluatingUnknown(evaluator: CGameAIBase) -> number | + | return -> Set as internal action return value: | + | -> EEex_Action_ReturnType.ACTION_STOPPED | + | -> EEex_Action_ReturnType.ACTION_ERROR | + | -> EEex_Action_ReturnType.ACTION_DONE | + | -> EEex_Action_ReturnType.ACTION_NORMAL | + | -> EEex_Action_ReturnType.ACTION_INTERRUPTABLE | + | -> EEex_Action_ReturnType.ACTION_NO_ACTION | + +---------------------------------------------------------------------------------+ --]] EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameAIBase::ExecuteAction()-DefaultCase"), 0, 7, 7, { diff --git a/EEex/copy/EEex_scripts/EEex_Sprite.lua b/EEex/copy/EEex_scripts/EEex_Sprite.lua index b2d8d93..b59c1a7 100644 --- a/EEex/copy/EEex_scripts/EEex_Sprite.lua +++ b/EEex/copy/EEex_scripts/EEex_Sprite.lua @@ -1514,6 +1514,11 @@ function EEex_Sprite_Hook_OnSetCurrAction(sprite) local spriteAux = EEex_GetUDAux(sprite) spriteAux["EEex_Fix_HasSpellOrSpellPointStartedCasting"] = 0 spriteAux["EEex_Sprite_DamageEntriesSinceActionStarted"] = {} + -- AttackOnce uses NoAction to end the current Attack while its consumed + -- state still guards any remaining same-swing Hit() callsites. + if sprite.m_curAction.m_actionID ~= 0 then + EEex.ClearAttackOnce(sprite) + end end -------------------------------------------------------------------------------------------- diff --git a/EEex/copy/EEex_scripts/EEex_Sprite_Patch.lua b/EEex/copy/EEex_scripts/EEex_Sprite_Patch.lua index 8a9fd16..60f32e0 100644 --- a/EEex/copy/EEex_scripts/EEex_Sprite_Patch.lua +++ b/EEex/copy/EEex_scripts/EEex_Sprite_Patch.lua @@ -802,6 +802,146 @@ callAlterBaseWeaponDamageHook(EEex_Label("Hook-CGameSprite::Swing()-CGameSprite::Damage()-1")) callAlterBaseWeaponDamageHook(EEex_Label("Hook-CGameSprite::Swing()-CGameSprite::Damage()-2")) + --[[ + +------------------------------------------------------------------------------------------------------------------+ + | Implement EEex_AttackOnce() - vanilla Attack() that stops after the first real attack roll | + +------------------------------------------------------------------------------------------------------------------+ + | [EEex.dll] EEex::Sprite_Hook_AttackOnceGetBaseAttackRoll(sprite, target, attackRoll) -> short | + | [EEex.dll] EEex::Sprite_Hook_AttackOnceConsumeAmmoForRoll(sprite, target, weapon, abilityNum) | + | [EEex.dll] EEex::Sprite_Hook_AttackOnceShouldEndAction(sprite) -> boolean | + | [EEex.dll] EEex::Sprite_Hook_AttackOnceGetDamageRoll(sprite, target, damageRoll, useModifiers) -> int | + +------------------------------------------------------------------------------------------------------------------+ + --]] + + -- #MAKE_SHADOW_SPACE(X) already includes the mandatory 32-byte call shadow; + -- X is only the extra scratch used by #SHADOW_SPACE_BOTTOM() saves. + + -- di is the raw d20 result. Modify it before the engine copies it into r12 + -- so both the displayed Roll value and the later hit math use the same base roll. + EEex_HookBeforeRestoreWithLabels(EEex_Label("Hook-CGameSprite::Hit()-AttackOnceBaseAttackRoll"), 0, 7, 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_HookIntegrityWatchdogRegister.RDI, + }}}, + {[[ + #MAKE_SHADOW_SPACE(16) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], rdx + + mov rcx, rbx + mov rdx, r14 + movsx r8d, di + call #L(EEex::Sprite_Hook_AttackOnceGetBaseAttackRoll) + mov di, ax + + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + #DESTROY_SHADOW_SPACE + ]]} + ) + + local checkAttackOnceShouldEndAction = function(address, labelSuffix) + local assembly = [[ + #MAKE_SHADOW_SPACE(32) + 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 rcx, rbx + call #L(EEex::Sprite_Hook_AttackOnceShouldEndAction) + mov r10b, al + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + #DESTROY_SHADOW_SPACE + + test r10b, r10b + jz attack_once_consume_ammo_]] .. labelSuffix .. [[ + + xor eax, eax + jmp #L(return_skip) + + attack_once_consume_ammo_]] .. labelSuffix .. [[: #ENDL + #MAKE_SHADOW_SPACE(32) + 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 + + ; r8/r9 are already the resolved weapon stack and ability number that + ; CGameSprite::Hit() is about to use. Consume here so misses spend ammo + ; too, while the later Hit() roll hook still owns AttackOnce completion. + mov rcx, rbx + mov rdx, r15 + call #L(EEex::Sprite_Hook_AttackOnceConsumeAmmoForRoll) + + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + #DESTROY_SHADOW_SPACE + + attack_once_continue_]] .. labelSuffix .. [[: #ENDL + ]] + + EEex_HookBeforeCallWithLabels(address, { + {"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_HookIntegrityWatchdogRegister.RDI, + }}}, + {assembly} + ) + end + + checkAttackOnceShouldEndAction(EEex_Label("Hook-CGameSprite::Swing()-AttackOnceHitCall-1"), "1") + checkAttackOnceShouldEndAction(EEex_Label("Hook-CGameSprite::Swing()-AttackOnceHitCall-2"), "2") + + local hookAttackOnceWeaponDamageRoll = function(address) + + -- si is the constructed weapon damage roll at both hook sites. Replace it + -- before the common Roll formatter consumes it. The high word of eax keeps + -- the early modifier/detail branch enabled, so all non-roll feedback stays + -- vanilla even when OverrideBaseDamageRoll is 0. + EEex_HookBeforeRestoreWithLabels(address, 0, 5, 5, { + {"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_HookIntegrityWatchdogRegister.RSI, EEex_HookIntegrityWatchdogRegister.R12, + }}}, + {[[ + #MAKE_SHADOW_SPACE(32) + 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 rcx, rdi + mov rdx, r15 + movsx r8d, si + mov r9d, r12d + call #L(EEex::Sprite_Hook_AttackOnceGetDamageRoll) + mov si, ax + shr eax, 16 + mov r12d, eax + + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + #DESTROY_SHADOW_SPACE + ]]} + ) + end + + -- CGameSprite::Damage() has separate primary/no-launcher and launcher roll + -- construction paths that merge just before feedback text is formatted. + hookAttackOnceWeaponDamageRoll(EEex_Label("Hook-CGameSprite::Damage()-AttackOnceAfterPrimaryWeaponRoll")) + hookAttackOnceWeaponDamageRoll(EEex_Label("Hook-CGameSprite::Damage()-AttackOnceAfterLauncherWeaponRoll")) + EEex_EnableCodeProtection() end)() diff --git a/EEex/loader/InfinityLoader.db b/EEex/loader/InfinityLoader.db index 0b182a9..1215eec 100644 --- a/EEex/loader/InfinityLoader.db +++ b/EEex/loader/InfinityLoader.db @@ -2356,6 +2356,10 @@ Operations=ADD 10 Pattern=488D4DD08D7801 Operations=ADD 18 +[Hook-CGameSprite::Hit()-AttackOnceBaseAttackRoll] +Pattern=488D4DD08D7801 +Operations=ADD 7 + [Hook-CGameSprite::Hit()-MeleeingWithRangedPenalty] Pattern=2BF90FAFC0 Operations=ADD 9 @@ -2452,6 +2456,22 @@ Operations=ADD 55 Pattern=498D57304C8B442470 Operations=ADD 49 +[Hook-CGameSprite::Damage()-AttackOnceAfterLauncherWeaponRoll] +Pattern=75D0448B642444 +Operations=ADD 52 + +[Hook-CGameSprite::Damage()-AttackOnceAfterPrimaryWeaponRoll] +Pattern=75D0448B6C2458 +Operations=ADD 56 + +[Hook-CGameSprite::Swing()-AttackOnceHitCall-1] +Pattern=488D442460488B7C2458 +Operations=ADD 31 + +[Hook-CGameSprite::Swing()-AttackOnceHitCall-2] +Pattern=488D4424604C8B642458 +Operations=ADD 31 + [Hook-CGameSprite::Swing()-CImmunitiesWeapon::OnList()-Melee] Pattern=0F85B5070000 Operations=ADD -7 diff --git a/EEex/patch/ACTION.IDS b/EEex/patch/ACTION.IDS index 3e6b53c..3afa3c1 100644 --- a/EEex/patch/ACTION.IDS +++ b/EEex/patch/ACTION.IDS @@ -9,4 +9,5 @@ 478 EEex_ForceSpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) 478 EEex_ForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) 479 EEex_ReallyForceSpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) -479 EEex_ReallyForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) \ No newline at end of file +479 EEex_ReallyForceSpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) +480 EEex_AttackOnce(O:Target*,I:BaseAttackRollMod*,I:OverrideBaseDamageRoll*,I:ConsumeAmmo*Boolean) \ No newline at end of file