diff --git a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h index edc3203..a007082 100644 --- a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h +++ b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h @@ -128,6 +128,10 @@ namespace EEex { void Sprite_Hook_OnAfterEffectListUnmarshalled(CGameSprite* pSprite); void Sprite_Hook_OnBeforeEffectListMarshalled(CGameSprite* pSprite); byte Sprite_Hook_OnGetAttackFrameType(CGameSprite* pSprite, byte numAttacks); + short Sprite_Hook_AttackOnceGetBaseAttackRoll(CGameSprite* pSprite, CGameSprite* pTarget, short attackRoll); + void Sprite_Hook_AttackOnceConsumeAmmoForRoll(CGameSprite* pSprite, CGameSprite* pTarget, CItem* pWeapon, int abilityNum); + int Sprite_Hook_AttackOnceGetDamageRoll(CGameSprite* pSprite, CGameSprite* pTarget, short damageRoll, int useDamageModifiers); + bool Sprite_Hook_AttackOnceShouldEndAction(CGameSprite* pSprite); //////////// // Action // diff --git a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h index 0b0048d..f087fff 100644 --- a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h +++ b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h @@ -4907,6 +4907,8 @@ namespace EEex long long GetMicroseconds(); void GetProjectileStartingPos(lua_State* L, CProjectile* pProjectile, CGameArea* pArea, CGameAIBase* pSourceObject, CGameObject* pTargetObject, int nTargetPosX, int nTargetPosY, int nHeight); CGameSprite* GetSpriteFromUUID(uint64_t uuid); + void StartAttackOnce(CGameSprite* pSprite, CGameObject* pTarget, int baseAttackRollMod, int overrideBaseDamageRoll, int consumeAmmo); + void ClearAttackOnce(CGameSprite* pSprite); void HookIntegrityWatchdogIgnoreRegisters(uintptr_t address, size_t instance, EEex_HookIntegrityWatchdogRegister registers); void HookIntegrityWatchdogIgnoreStackRange(uintptr_t address, size_t instance, int lowerBound, int upperBound); uiItem* InjectTemplateInstance(lua_State* L, const char* menuName, const char* templateName, int instanceId, int x, int y); diff --git a/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt b/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt index f4bc78e..9b6f67d 100644 --- a/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt +++ b/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt @@ -139,6 +139,8 @@ namespace EEex $external_implementation long long GetMicroseconds(); $external_implementation $custom_return_3 GetProjectileStartingPos(CProjectile* pProjectile, CGameArea* pArea, CGameAIBase* pSourceObject, CGameObject* pTargetObject, int nTargetPosX, int nTargetPosY, int nHeight); $external_implementation CGameSprite* GetSpriteFromUUID(uint64_t uuid); + $external_implementation void StartAttackOnce(CGameSprite* pSprite, CGameObject* pTarget, int baseAttackRollMod, int overrideBaseDamageRoll, int consumeAmmo); + $external_implementation void ClearAttackOnce(CGameSprite* pSprite); $external_implementation void HookIntegrityWatchdogIgnoreRegisters(uintptr_t address, size_t instance, EEex_HookIntegrityWatchdogRegister registers); $external_implementation void HookIntegrityWatchdogIgnoreStackRange(uintptr_t address, size_t instance, int lowerBound, int upperBound); $external_implementation $pass_lua_state uiItem* InjectTemplateInstance(primitive const char* menuName, primitive const char* templateName, int instanceId, int x, int y); diff --git a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp index 9a80717..4a73e19 100644 --- a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp +++ b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp @@ -141,11 +141,33 @@ std::unordered_map> proj // Sprite // //////////// +// Transient action-480 state. EEex_AttackOnce delegates targeting, movement, +// animation, projectiles, and interruption to vanilla Attack(), so this state +// only bridges that vanilla action until the first real Hit() roll is observed. +struct ExAttackOnceState { + bool active = false; + // Set by the first matching Hit() roll, regardless of hit or miss. + bool consumed = false; + // Prevents recursive / duplicate SetCurrAction() calls from same-swing hooks. + bool endRequested = false; + // m_specificID3 gate: spend one finite ranged stack use on the first roll. + bool consumeAmmo = false; + bool ammoConsumed = false; + // -1 means "latch the first sprite vanilla Attack() actually rolls against." + int targetId = -1; + // Applied directly to the raw d20 roll, then clamped to the engine d20 range. + int baseAttackRollMod = 0; + // <0 keeps vanilla weapon roll / feedback; 0 forces zero weapon damage; + // >0 replaces only the base weapon roll before normal mitigation. + int overrideBaseDamageRoll = 0; +}; + struct ExSpriteData { EngineVal combatRoundsOverride[5]{}; Array oldDisabledSpellTypes; int oldDisableSpells = 0; + ExAttackOnceState attackOnce; uint64_t uuid = 0; ExSpriteData() { @@ -3873,6 +3895,313 @@ void EEex::Opcode_Hook_AfterListsResolved(CGameSprite* pSprite) { // Sprite // //////////// +static ExAttackOnceState* getAttackOnceState(CGameSprite* pSprite) { + + if (pSprite == nullptr) { + return nullptr; + } + + if (auto itr = exSpriteDataMap.find(pSprite); itr != exSpriteDataMap.end()) { + return &itr->second.attackOnce; + } + + return nullptr; +} + +static ExAttackOnceState* getMatchingAttackOnceState(CGameSprite* pSprite, CGameSprite* pTarget) { + + if (pTarget == nullptr) { + return nullptr; + } + + ExAttackOnceState* const pState = getAttackOnceState(pSprite); + if (pState != nullptr && pState->active && pState->targetId == -1) { + // Script target resolution can be indirect when the action is armed. + // Latch the first concrete Hit() target so invalid/out-of-range targets + // continue to behave exactly like vanilla Attack() until a roll happens. + pState->targetId = pTarget->m_id; + return pState; + } + + if (pState != nullptr && pState->active && pState->targetId == pTarget->m_id) { + return pState; + } + + return nullptr; +} + +static bool attackOnceTargetMatches(const ExAttackOnceState& state, CGameSprite* pTarget) { + + return state.targetId == -1 || (pTarget != nullptr && state.targetId == pTarget->m_id); +} + +static ExAttackOnceState* getDamageAttackOnceState(CGameSprite* pSprite, CGameSprite* pTarget) { + + ExAttackOnceState* const pState = getAttackOnceState(pSprite); + if (pState != nullptr && pState->active && pState->consumed && attackOnceTargetMatches(*pState, pTarget)) { + return pState; + } + + return nullptr; +} + +struct ExAttackOnceDamageContext { + bool active = false; + CGameSprite* attacker = nullptr; + CGameSprite* target = nullptr; + int overrideBaseDamageRoll = 0; +}; + +static thread_local ExAttackOnceDamageContext exAttackOnceDamageContext{}; + +// Damage() is reached through Override_Damage(), after the Hit() hook may have +// already ended the Attack action. This scoped context identifies the nested +// weapon-damage construction without depending on the current script action. +struct ScopedAttackOnceDamageContext { + ExAttackOnceDamageContext previousContext; + + ScopedAttackOnceDamageContext(CGameSprite* pAttacker, CGameSprite* pTarget, ExAttackOnceState* pState) + : previousContext(exAttackOnceDamageContext) + { + if (pState != nullptr) { + exAttackOnceDamageContext.active = true; + exAttackOnceDamageContext.attacker = pAttacker; + exAttackOnceDamageContext.target = pTarget; + exAttackOnceDamageContext.overrideBaseDamageRoll = pState->overrideBaseDamageRoll; + } + else { + exAttackOnceDamageContext = {}; + } + } + + ~ScopedAttackOnceDamageContext() { + exAttackOnceDamageContext = previousContext; + } + + bool active() const { + return exAttackOnceDamageContext.active; + } + + int overrideBaseDamageRoll() const { + return exAttackOnceDamageContext.overrideBaseDamageRoll; + } +}; + +static void requestAttackOnceActionEnd(CGameSprite* pSprite, ExAttackOnceState& state) { + + if (state.endRequested) { + return; + } + + // End the current Attack action after the first real roll. The consumed + // AttackOnce state intentionally remains until the next non-NoAction so any + // same-swing Hit() callsites are still skipped by the pre-call hook. + ExAttackOnceState preservedState = state; + preservedState.endRequested = true; + + CPoint dummyPoint { -1, -1 }; + EngineVal noAction { 0, &dummyPoint, 0, -1 }; + pSprite->virtual_SetCurrAction(&*noAction); + pSprite->m_curAction.m_actionID = 0; + + // SetCurrAction has generic Lua cleanup hooks. Preserve AttackOnce until the + // next real action starts so remaining Swing() damage / Hit() callsites still + // observe the consumed state. + state = preservedState; +} + +static ushort* getAttackOnceAbilityUseCount(CItem* const pItem, const int abilityNum) { + + if (pItem == nullptr) { + return nullptr; + } + + switch (abilityNum) { + case 0: return &pItem->m_useCount1; + case 1: return &pItem->m_useCount2; + case 2: return &pItem->m_useCount3; + default: return nullptr; + } +} + +static short findAttackOnceEquipmentSlot(CGameSprite* const pSprite, CItem* const pItem) { + + if (pSprite == nullptr || pItem == nullptr) { + return -1; + } + + constexpr int EQUIPMENT_SLOT_COUNT = 39; + for (short slotNum = 0; slotNum < EQUIPMENT_SLOT_COUNT; ++slotNum) { + if (pSprite->m_equipment.m_items[slotNum] == pItem) { + return slotNum; + } + } + + return -1; +} + +static bool isAttackOnceConsumableRangedStack(Item_ability_st* const pAbility) { + + constexpr ushort ITEM_ABILITY_TYPE_RANGED = 2; + + // AttackOnce only spends finite ranged stacks + return pAbility != nullptr && + pAbility->type == ITEM_ABILITY_TYPE_RANGED && + pAbility->maxUsageCount > 0u && + (pAbility->usageFlags == 1u || pAbility->usageFlags == 2u); // skip the so-called "Item recharges" flags (see f.i. Quiver of Plenty +1) +} + +static void notifyAttackOnceQuickLists(CGameSprite* const pSprite, CItem* const pItem, const short slotNum, const short abilityNum, Item_ability_st* const pAbility) { + + if (pSprite == nullptr || pItem == nullptr || pAbility == nullptr) { + return; + } + + EngineVal abilityId{}; + // CGameSprite::CheckQuickLists() treats type 2 as an equipment item. It + // matches quick weapons/items by equipment slot and ability index, which is + // the same identity we just decremented on the CItem. + constexpr short CABILITYID_ITEM_TYPE_ITEM = 2; + abilityId->m_itemType = CABILITYID_ITEM_TYPE_ITEM; + abilityId->m_itemNum = slotNum; + abilityId->m_abilityNum = abilityNum; + abilityId->m_res.copy(&pItem->cResRef); + abilityId->m_targetType = pAbility->actionType; + abilityId->m_targetCount = pAbility->actionCount; + // CAbilityId* ab, short changeAmount, int remove, int removeSpellIfZero + pSprite->CheckQuickLists(&*abilityId, -1, 0, 0); +} + +static void consumeAttackOnceAmmoIfNeeded(CGameSprite* const pSprite, ExAttackOnceState& state, CItem* const pWeapon, const int abilityNum) { + + if (pSprite == nullptr || pWeapon == nullptr || !state.consumeAmmo || state.ammoConsumed) { + return; + } + + if (abilityNum < 0 || abilityNum > 2 || abilityNum >= pWeapon->m_nAbilities) { + return; + } + + Item_ability_st* const pAbility = pWeapon->GetAbility(abilityNum); + ushort* const pUseCount = getAttackOnceAbilityUseCount(pWeapon, abilityNum); + if (pAbility == nullptr || pUseCount == nullptr || *pUseCount == 0) { + return; + } + + // Swing() passes the resolved weapon stack into Hit(): launcher attacks use + // the ammo item here, while no-launcher ranged stacks cover thrown weapons. + if (!isAttackOnceConsumableRangedStack(pAbility)) { + return; + } + + --(*pUseCount); + // Mark only after a real decrement so a failed / empty / opt-out ability can + // still be retried by a later matching roll path without underflowing counts. + state.ammoConsumed = true; + notifyAttackOnceQuickLists(pSprite, pWeapon, findAttackOnceEquipmentSlot(pSprite, pWeapon), static_cast(abilityNum), pAbility); +} + +void EEex::StartAttackOnce(CGameSprite* pSprite, CGameObject* pTarget, int baseAttackRollMod, int overrideBaseDamageRoll, int consumeAmmo) { + + if (pSprite == nullptr) { + return; + } + + ExAttackOnceState& state = exSpriteDataMap[pSprite].attackOnce; + state = ExAttackOnceState{}; + state.active = true; + state.targetId = -1; + state.baseAttackRollMod = baseAttackRollMod; + state.overrideBaseDamageRoll = overrideBaseDamageRoll; + state.consumeAmmo = consumeAmmo != 0; + + if (pTarget != nullptr && pTarget->m_objectType == CGameObjectType::SPRITE) { + // Direct sprite targets can be stored immediately. Object selectors keep + // targetId = -1 and are latched at the first real Hit() call. + state.targetId = pTarget->m_id; + } +} + +void EEex::ClearAttackOnce(CGameSprite* pSprite) { + + if (ExAttackOnceState* const pState = getAttackOnceState(pSprite)) { + *pState = ExAttackOnceState{}; + } +} + +short EEex::Sprite_Hook_AttackOnceGetBaseAttackRoll(CGameSprite* pSprite, CGameSprite* pTarget, short attackRoll) { + + ExAttackOnceState* const pState = getMatchingAttackOnceState(pSprite, pTarget); + if (pState == nullptr) { + return attackRoll; + } + + const short modifiedAttackRoll = static_cast(min(20, max(1, static_cast(attackRoll) + pState->baseAttackRollMod))); + pState->consumed = true; + requestAttackOnceActionEnd(pSprite, *pState); + return modifiedAttackRoll; +} + +void EEex::Sprite_Hook_AttackOnceConsumeAmmoForRoll(CGameSprite* pSprite, CGameSprite* pTarget, CItem* pWeapon, int abilityNum) { + + ExAttackOnceState* const pState = getMatchingAttackOnceState(pSprite, pTarget); + if (pState == nullptr || pState->consumed) { + return; + } + + // This hook runs immediately before the engine calls Hit(), when Swing() has + // already resolved launcher ammo vs. thrown/self-consuming weapons into + // pWeapon / abilityNum but before the later roll hook marks the action consumed. + consumeAttackOnceAmmoIfNeeded(pSprite, *pState, pWeapon, abilityNum); +} + +static int packAttackOnceDamageRollResult(short damageRoll, int useDamageModifiers) { + + // The Lua hook receives both values through eax: low word = roll to put back + // in si, high word = whether r12's modifier/detail branch should stay live. + return (useDamageModifiers != 0 ? 1 << 16 : 0) | static_cast(damageRoll); +} + +int EEex::Sprite_Hook_AttackOnceGetDamageRoll(CGameSprite* pSprite, CGameSprite* pTarget, short damageRoll, int useDamageModifiers) { + + int overrideBaseDamageRoll = 0; + bool hasOverride = false; + + if ( + exAttackOnceDamageContext.active && + exAttackOnceDamageContext.attacker == pSprite && + (exAttackOnceDamageContext.target == nullptr || exAttackOnceDamageContext.target == pTarget) + ) { + overrideBaseDamageRoll = exAttackOnceDamageContext.overrideBaseDamageRoll; + hasOverride = true; + } + else if (ExAttackOnceState* const pState = getDamageAttackOnceState(pSprite, pTarget)) { + overrideBaseDamageRoll = pState->overrideBaseDamageRoll; + hasOverride = true; + } + + if (!hasOverride) { + return packAttackOnceDamageRollResult(damageRoll, useDamageModifiers); + } + + if (overrideBaseDamageRoll < 0) { + return packAttackOnceDamageRollResult(damageRoll, useDamageModifiers); + } + + // For non-negative overrides, replace the displayed / actual base roll. Keep + // the engine's modifier/detail branch enabled even for zero so feedback still + // shows vanilla non-roll contributions; final zero damage is enforced after + // Damage() returns. + const short overrideRoll = clampToType(overrideBaseDamageRoll); + return packAttackOnceDamageRollResult(overrideRoll, 1); +} + +bool EEex::Sprite_Hook_AttackOnceShouldEndAction(CGameSprite* pSprite) { + + ExAttackOnceState* const pState = getAttackOnceState(pSprite); + return pState != nullptr && pState->active && pState->consumed; +} + void EEex::Sprite_Hook_OnConstruct(CGameSprite* pSprite) { STUTTER_LOG_START(void, "EEex::Sprite_Hook_OnConstruct") @@ -4013,7 +4342,26 @@ CGameEffectDamage* CGameSprite::Override_Damage( CAIObjectType* type, short facing, short myFacing, CGameSprite* target, int lastSwing) { Item_ability_st *const pAbility = curWeaponIn->GetAbility(curAttackNum); - CGameEffectDamage *const pEffect = this->Damage(curWeaponIn, pLauncher, curAttackNum, criticalDamage, type, facing, myFacing, target, lastSwing); + ExAttackOnceState* const pAttackOnceState = getDamageAttackOnceState(this, target); + CGameEffectDamage* pEffect = nullptr; + + { + // Swing() is patched to call this wrapper, which then calls the real + // Damage(). Keep the AttackOnce damage override in a scoped context so + // the inner Damage() hook can always identify this exact weapon-damage + // construction, even after the action has already been ended. + ScopedAttackOnceDamageContext attackOnceDamageContext { this, target, pAttackOnceState }; + pEffect = this->Damage(curWeaponIn, pLauncher, curAttackNum, criticalDamage, type, facing, myFacing, target, lastSwing); + + if (pEffect != nullptr && attackOnceDamageContext.active() && attackOnceDamageContext.overrideBaseDamageRoll() == 0) { + // A zero override means "no weapon damage from this swing", so clear + // any downstream STR/proficiency/effect bonuses that Damage() added. + pEffect->m_effectAmount = 0; + pEffect->m_numDice = 0; + pEffect->m_diceSize = 0; + } + + } CItem *const pLeftHandItem = this->m_equipment.m_items[9]; const bool isLeftHand = lastSwing && pLeftHandItem != nullptr && pLeftHandItem->pRes->pHeader->itemType != 12; diff --git a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/EEexLua_generated.cpp b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/EEexLua_generated.cpp index 686f498..656298c 100644 --- a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/EEexLua_generated.cpp +++ b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/EEexLua_generated.cpp @@ -267,6 +267,18 @@ static int tolua_function_EEex_GetSpriteFromUUID(lua_State* L) return 1; } +static int tolua_function_EEex_StartAttackOnce(lua_State* L) +{ + EEex::StartAttackOnce((CGameSprite*)tolua_tousertype_dynamic(L, 1, 0, "CGameSprite"), (CGameObject*)tolua_tousertype_dynamic(L, 2, 0, "CGameObject"), tolua_function_tointeger(L, 3, "StartAttackOnce"), tolua_function_tointeger(L, 4, "StartAttackOnce"), tolua_function_tointeger(L, 5, "StartAttackOnce")); + return 0; +} + +static int tolua_function_EEex_ClearAttackOnce(lua_State* L) +{ + EEex::ClearAttackOnce((CGameSprite*)tolua_tousertype_dynamic(L, 1, 0, "CGameSprite")); + return 0; +} + static int tolua_function_EEex_HookIntegrityWatchdogIgnoreRegisters(lua_State* L) { EEex::HookIntegrityWatchdogIgnoreRegisters(tolua_function_tointeger(L, 1, "HookIntegrityWatchdogIgnoreRegisters"), tolua_function_tointeger(L, 2, "HookIntegrityWatchdogIgnoreRegisters"), (EEex_HookIntegrityWatchdogRegister)tolua_function_tointeger<__int32>(L, 3, "HookIntegrityWatchdogIgnoreRegisters")); @@ -946,6 +958,8 @@ int OpenBindingsInternal(lua_State* L) tolua_function(L, "GetMicroseconds", &tolua_function_EEex_GetMicroseconds); tolua_function(L, "GetProjectileStartingPos", &tolua_function_EEex_GetProjectileStartingPos); tolua_function(L, "GetSpriteFromUUID", &tolua_function_EEex_GetSpriteFromUUID); + tolua_function(L, "StartAttackOnce", &tolua_function_EEex_StartAttackOnce); + tolua_function(L, "ClearAttackOnce", &tolua_function_EEex_ClearAttackOnce); tolua_function(L, "HookIntegrityWatchdogIgnoreRegisters", &tolua_function_EEex_HookIntegrityWatchdogIgnoreRegisters); tolua_function(L, "HookIntegrityWatchdogIgnoreStackRange", &tolua_function_EEex_HookIntegrityWatchdogIgnoreStackRange); tolua_function(L, "InjectTemplateInstance", &tolua_function_EEex_InjectTemplateInstance); diff --git a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp index a0cb76e..a2c0f28 100644 --- a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp +++ b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp @@ -147,6 +147,10 @@ static void exportPatterns() { exportPattern(TEXT("EEex::Sprite_Hook_OnAfterEffectListUnmarshalled"), EEex::Sprite_Hook_OnAfterEffectListUnmarshalled); exportPattern(TEXT("EEex::Sprite_Hook_OnBeforeEffectListMarshalled"), EEex::Sprite_Hook_OnBeforeEffectListMarshalled); exportPattern(TEXT("EEex::Sprite_Hook_OnGetAttackFrameType"), EEex::Sprite_Hook_OnGetAttackFrameType); + exportPattern(TEXT("EEex::Sprite_Hook_AttackOnceGetBaseAttackRoll"), EEex::Sprite_Hook_AttackOnceGetBaseAttackRoll); + exportPattern(TEXT("EEex::Sprite_Hook_AttackOnceConsumeAmmoForRoll"), EEex::Sprite_Hook_AttackOnceConsumeAmmoForRoll); + exportPattern(TEXT("EEex::Sprite_Hook_AttackOnceGetDamageRoll"), EEex::Sprite_Hook_AttackOnceGetDamageRoll); + exportPattern(TEXT("EEex::Sprite_Hook_AttackOnceShouldEndAction"), EEex::Sprite_Hook_AttackOnceShouldEndAction); //////////// // Action //