diff --git a/CREDITS.md b/CREDITS.md index ded5242180..005a732780 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -158,6 +158,7 @@ This page lists all the individual contributions to the project by their author. - Warhead activation target health thresholds enhancements - Event 606: AttachEffect is attaching to a Techno - Linked superweapons + - Engineer logics on Warheads - Unit & infantry auto-conversion on ammo change - Restore the ScriptType action#24 `Play speech` from Tiberian Sun - Modify ammo on impact diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 343dae63eb..ca9f2d1387 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1990,6 +1990,7 @@ FLHKEY.BurstN= ; integer - Forward,Lateral,Height. FLHKey refers to weapon-spec - `ForceWeapon.Cloaked` forces specified weapon to be used against any cloaked targets. - `ForceWeapon.Disguised` forces specified weapon to be used against any disguised targets. - `ForceWeapon.UnderEMP` forces specified weapon to be used if the target is under EMP effect. + - `ForceWeapon.Capture` forces specified weapon to be used if the target building is capturable. - `ForceWeapon.InRange` forces specified a list of weapons to be used once the target is within their `Range`. If `ForceWeapon.InRange.TechnoOnly` set to true, it'll only be forced on TechnoTypes like other forced weapons, otherwise it'll also be forced when attacking empty grounds. The first weapon in the listed order satisfied will be selected. Can be applied to both ground and air target if `ForceAAWeapon.InRange` is not set. - `ForceAAWeapon.InRange` does the same thing but only for air target. Taking priority to `ForceWeapon.InRange`, which means that it can only be applied to ground target when they're both set. - `Force(AA)Weapon.InRange.Overrides` overrides the range when decides which weapon to use. Value from position matching the position from `Force(AA)Weapon.InRange` is used if found, or the weapon's own `Range` if not found or set to a value below 0. @@ -2012,6 +2013,7 @@ ForceWeapon.Naval.Decloaked=-1 ; integer, -1 to disable ForceWeapon.Cloaked=-1 ; integer, -1 to disable ForceWeapon.Disguised=-1 ; integer, -1 to disable ForceWeapon.UnderEMP=-1 ; integer, -1 to disable +ForceWeapon.Capture=-1 ; integer, -1 to disable ForceWeapon.InRange= ; List of integers ForceWeapon.InRange.TechnoOnly= ; boolean, default to [General] -> ForceWeapon.InRange.TechnoOnly ForceWeapon.InRange.Overrides= ; List of floating-point values @@ -3025,6 +3027,23 @@ DetonateOnAllMapObjects.RequireVerses=false ; boolean While this feature can provide better performance than a large `CellSpread` value, it still has potential to slow down the game, especially if used in conjunction with things like animations, alpha lights etc. Modder discretion and use of the filter keys (`AffectsTarget/House/Types` etc.) is advised. ``` +### Engineer logics on Warheads + +- Now any `InfantryType`, `VehicleType`, `BuildingType` or `AircraftType` can execute some operations engineers do without losing the firer in the process. +- `FakeEngineer.CanRepairBridges`, if set to true, when a building with `BridgeRepairHut=yes` linked to a bridge is affected by the Warhead then all destroyed bridge sections will be fixed. +- `FakeEngineer.CanDestroyBridges`, if set to true, when a building with `BridgeRepairHut=yes` linked to a bridge is affected by the Warhead then all the bridge will be destroyed. +- `FakeEngineer.CanCaptureBuildings`, if set to true, a building with `Capturable=true` is affected by the Warhead then the building will be captured by the house's firer. +- `FakeEngineer.BombDisarm`, if set to true, an attached bomb will be removed if the target is affected by the Warhead. + +In `rulesmd.ini`: +```ini +[SOMEWARHEAD] ; WarheadType +FakeEngineer.CanRepairBridges=false ; boolean +FakeEngineer.CanDestroyBridges=false ; boolean +FakeEngineer.CanCaptureBuildings=false ; boolean +FakeEngineer.BombDisarm=false ; boolean +``` + ### Fire weapon when Warhead kills something - `KillWeapon` will be fired at the target TechnoType's location once it's killed by this Warhead. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 05cd94ed31..72c6e4e999 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -555,6 +555,7 @@ HideShakeEffects=false ; boolean - [Units can customize the attack voice that plays when using more weapons](New-or-Enhanced-Logics.md#multi-voiceattack) (by FlyStar) - Customize squid grapple animation (by NetsuNegi) - [Auto deploy for GI-like infantry](Fixed-or-Improved-Logics.md#auto-deploy-for-gi-like-infantry) (by TaranDahl) +- Engineer logics on Warheads (by FS-21) - [Vehicles that have lost their target can customize the turret direction to align with the vehicle body](New-or-Enhanced-Logics.md#turret-response) (by FlyStar) - [Reverse engineer warhead](New-or-Enhanced-Logics.md#reverse-engineer-warhead) (by CrimRecya) - [AI base construction modification](Fixed-or-Improved-Logics.md#ai-base-construction-modification) (by CrimRecya) diff --git a/src/Ext/Infantry/Hooks.cpp b/src/Ext/Infantry/Hooks.cpp index c454ac6d56..00cc1b64de 100644 --- a/src/Ext/Infantry/Hooks.cpp +++ b/src/Ext/Infantry/Hooks.cpp @@ -209,3 +209,18 @@ DEFINE_HOOK(0x51A002, InfantryClass_UpdatePosition_InfiltrateBuilding, 0x6) return 0; } + +// Pass actual target to SelectWeapon instead of -1 during What_Action evaluation +DEFINE_HOOK(0x51E6D8, InfantryClass_WhatAction_WhatWeaponShouldIUse, 0x9) +{ + GET(InfantryClass*, pThis, EDI); + GET(AbstractClass*, pTarget, ESI); + + int weaponIdx = (pThis && pTarget) ? pThis->SelectWeapon(pTarget) : -1; + + R->EAX(weaponIdx); + + return 0x51E6E1; +} + + diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 1615942f11..460bc386ed 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include @@ -672,6 +674,175 @@ bool TechnoExt::TryToCreateCrate(CoordStruct location, Powerup selectedPowerup, return placed; } +AircraftTypeClass* TechnoExt::GetAircraftTypeExtra(AircraftClass* pAircraft) +{ + if (pAircraft->IsGreenHP()) + { + return pAircraft->Type; + } + else if (pAircraft->IsYellowHP()) + { + auto const pData = TechnoTypeExt::ExtMap.Find(pAircraft->Type); + + if (auto const imageYellow = pData->Image_ConditionYellow) + return abstract_cast(imageYellow); + } + else + { + auto const pType = pAircraft->Type; + auto const pData = TechnoTypeExt::ExtMap.Find(pType); + + if (auto const imageRed = pData->Image_ConditionRed) + return abstract_cast(imageRed); + else if (auto const imageYellow = pData->Image_ConditionYellow) + return abstract_cast(imageYellow); + } + + return pAircraft->Type; + +} + +bool TechnoExt::CanBeAffectedByFakeEngineer(TechnoClass* pThis, TechnoClass* pTarget, bool checkBridge, bool checkCapturableBuilding, bool checkAttachedBombs) +{ + if (!pThis || !pTarget) + return false; + + const auto pBuilding = abstract_cast(pTarget); + const bool isBuilding = pBuilding && pBuilding->IsAlive && pBuilding->Health > 0; + + auto checkWeaponWarhead = [&](WeaponTypeClass* pWeapon) -> bool + { + if (!pWeapon || !pWeapon->Warhead) + return false; + + const auto pWHExt = WarheadTypeExt::Fetch(pWeapon->Warhead); + if (!pWHExt->FakeEngineer_CanCaptureBuildings + && !pWHExt->FakeEngineer_CanRepairBridges + && !pWHExt->FakeEngineer_CanDestroyBridges + && !pWHExt->FakeEngineer_BombDisarm) + { + return false; + } + + if (checkAttachedBombs + && pWHExt->FakeEngineer_BombDisarm + && pTarget->AttachedBomb) + { + return true; + } + + if (checkBridge && isBuilding && pBuilding->Type->BridgeRepairHut) + { + CellStruct bridgeRepairHutCell = pBuilding->GetMapCoords(); + bool isBridgeDamaged = MapClass::Instance.IsLinkedBridgeDestroyed(bridgeRepairHutCell); + + if ((isBridgeDamaged && pWHExt->FakeEngineer_CanRepairBridges) + || (!isBridgeDamaged && pWHExt->FakeEngineer_CanDestroyBridges)) + { + return true; + } + } + + if (checkCapturableBuilding + && isBuilding + && pWHExt->FakeEngineer_CanCaptureBuildings + && (pBuilding->Type->Capturable || pBuilding->Type->NeedsEngineer) + && pBuilding->Owner != pThis->Owner + && (!pThis->Owner->IsAlliedWith(pBuilding) || pBuilding->Owner->IsNeutral())) + { + return true; + } + + return false; + }; + + // 1. Check Primary and Secondary weapons + if (const auto pPrimary = pThis->GetWeapon(0)) + { + if (checkWeaponWarhead(pPrimary->WeaponType)) + return true; + } + if (const auto pSecondary = pThis->GetWeapon(1)) + { + if (checkWeaponWarhead(pSecondary->WeaponType)) + return true; + } + + // 2. Check deploy fire weapon + int deployWeaponIdx = -1; + if (auto const pDeployWeapon = TechnoExt::GetDeployFireWeapon(pThis, pThis->GetTechnoType(), deployWeaponIdx)) + { + if (checkWeaponWarhead(pDeployWeapon)) + return true; + } + + return false; +} + +void TechnoExt::RepairOrDestroyBridgeHut(BuildingClass* pBuilding, TechnoClass* pOwner, HouseClass* pFiringHouse, bool destroyBridge) +{ + if (!pBuilding || !pBuilding->Type->BridgeRepairHut || !pBuilding->IsAlive || pBuilding->Health <= 0) + return; + + const CoordStruct targetCoords = pBuilding->GetCenterCoords(); + const CellStruct baseCell = pBuilding->GetMapCoords(); + + // Send engineer's "enter" event + auto const pTag = pBuilding->AttachedTag; + + if (pTag && pOwner) + pTag->RaiseEvent(TriggerEvent::EnteredBy, pOwner, CellStruct::Empty); + + // Check a 5x5 area for bridge tiles to determine if we should repair or destroy + bool foundWoodBridge = false; + + for (int y = -2; y <= 2; ++y) + { + for (int x = -2; x <= 2; ++x) + { + CellStruct checkCellCoords = { static_cast(baseCell.X + x), static_cast(baseCell.Y + y) }; + auto const checkCell = MapClass::Instance.GetCellAt(checkCellCoords); + + if (checkCell && (checkCell->Tile_Is_WoodBridge() || (checkCell->OverlayTypeIndex >= 74 && checkCell->OverlayTypeIndex <= 101))) + foundWoodBridge = true; + + if (foundWoodBridge) + break; + } + + if (foundWoodBridge) + break; + } + + // Destroying bridges + if (destroyBridge) + { + if (foundWoodBridge) // Destroy wood bridges + MapClass::Instance.DestroyWoodBridgeAt(baseCell); + else // Destroy concrete bridges + MapClass::Instance.DestroyConcreteBridgeAt(baseCell); + + return; + } + + auto const pFiringOwner = pOwner ? pOwner->Owner : pFiringHouse; + + // Repairing bridges + if (pFiringOwner && pFiringOwner->IsControlledByCurrentPlayer()) + { + if (RadarEventClass::Create(RadarEventType::BridgeRepaired, baseCell)) + VoxClass::PlayIndex(VoxClass::FindIndex("EVA_BridgeRepaired")); + } + + if (RulesClass::Instance->RepairBridgeSound != -1) + VocClass::PlayAt(RulesClass::Instance->RepairBridgeSound, targetCoords, nullptr); + + if (foundWoodBridge) // Repair wood bridges + MapClass::Instance.RepairWoodBridgeAt(baseCell); + else // Repair concrete bridges + MapClass::Instance.RepairConcreteBridgeAt(baseCell); +} + void TechnoExt::ResetDelayedFireTimer() { this->DelayedFireTimer.Stop(); diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 57a79aa69e..5a7ec55bf0 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -262,6 +262,11 @@ class TechnoExt : public RadioExt, public Detach::Listener static void CreateDelayedFireAnim(TechnoClass* pThis, AnimTypeClass* pAnimType, int weaponIndex, bool attach, bool center, bool removeOnNoDelay, bool onTurret, CoordStruct firingCoords); static bool HandleDelayedFireWithPauseSequence(TechnoClass* pThis, WeaponTypeClass* pWeapon, int weaponIndex, int frame, int firingFrame); static bool IsHealthInThreshold(TechnoClass* pObject, double min, double max); + static UnitTypeClass* GetUnitTypeExtra(UnitClass* pUnit); + static AircraftTypeClass* GetAircraftTypeExtra(AircraftClass* pAircraft); + static bool CanBeAffectedByFakeEngineer(TechnoClass* pThis, TechnoClass* pBuilding, bool checkBridge = false, bool checkCapturableBuilding = false, bool checkAttachedBombs = false); + static void RepairOrDestroyBridgeHut(BuildingClass* pBuilding, TechnoClass* pOwner = nullptr, HouseClass* pFiringHouse = nullptr, bool destroyBridge = false); + static bool CannotMove(UnitClass* pThis); static void ShowPromoteAnim(TechnoClass* pThis); static void ClickedApproachObject(FootClass* pThis, ObjectClass* pObject); static bool CanBeRecruitedFix(FootClass* pThis, HouseClass* pHouse); diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index 244b1a5cb9..b0f81a552e 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -92,9 +92,12 @@ DEFINE_HOOK(0x6F3428, TechnoClass_WhatWeaponShouldIUse_ForceWeapon, 0x6) { enum { UseWeaponIndex = 0x6F37AF }; - GET(TechnoClass*, pThis, ECX); + GET(TechnoClass*, pThis, ESI); GET_STACK(AbstractClass*, pTarget, STACK_OFFSET(0x18, 0x4)); + if (!pThis || !pTarget) + return 0; + auto const pTypeExt = TechnoExt::Fetch(pThis)->TypeExtData; // Force weapon @@ -137,6 +140,37 @@ DEFINE_HOOK(0x6F36DB, TechnoClass_WhatWeaponShouldIUse, 0x8) if (!pTargetTechno) return Primary; + if (TechnoExt::CanBeAffectedByFakeEngineer(pThis, pTargetTechno, true, true, true)) + { + auto const pSecWeapon = pThis->GetWeapon(1); + if (pSecWeapon && pSecWeapon->WeaponType && pSecWeapon->WeaponType->Warhead) + { + auto const pSecWHExt = WarheadTypeExt::Fetch(pSecWeapon->WeaponType->Warhead); + const bool secHasFakeEngineer = pSecWHExt->FakeEngineer_CanCaptureBuildings + || pSecWHExt->FakeEngineer_CanRepairBridges + || pSecWHExt->FakeEngineer_CanDestroyBridges + || pSecWHExt->FakeEngineer_BombDisarm; + + if (secHasFakeEngineer) + { + auto const pPriWeapon = pThis->GetWeapon(0); + if (!pPriWeapon || !pPriWeapon->WeaponType || !pPriWeapon->WeaponType->Warhead) + return Secondary; + + auto const pPriWHExt = WarheadTypeExt::Fetch(pPriWeapon->WeaponType->Warhead); + const bool priHasFakeEngineer = pPriWHExt->FakeEngineer_CanCaptureBuildings + || pPriWHExt->FakeEngineer_CanRepairBridges + || pPriWHExt->FakeEngineer_CanDestroyBridges + || pPriWHExt->FakeEngineer_BombDisarm; + + if (!priHasFakeEngineer) + return Secondary; + } + } + + return Primary; + } + const auto pTargetExt = TechnoExt::Fetch(pTargetTechno); if (const auto pShield = pTargetExt->Shield.get()) diff --git a/src/Ext/Techno/Hooks.ReceiveDamage.cpp b/src/Ext/Techno/Hooks.ReceiveDamage.cpp index c1b3e4ac38..2e9386d198 100644 --- a/src/Ext/Techno/Hooks.ReceiveDamage.cpp +++ b/src/Ext/Techno/Hooks.ReceiveDamage.cpp @@ -1,3 +1,8 @@ +#include "Body.h" + +#include +#include +#include #include #include #include @@ -81,6 +86,27 @@ DEFINE_HOOK(0x701900, TechnoClass_ReceiveDamage_Shield, 0x6) } } + auto const pBuilding = abstract_cast(pThis); + + // Capture enemy/neutral buildings + const bool canCaptureHouse = pBuilding && pBuilding->Owner != pSourceHouse + && (!pSourceHouse->IsAlliedWith(pTargetHouse) || pBuilding->Owner->IsNeutral()); + + if (canCaptureHouse && pWHExt->FakeEngineer_CanCaptureBuildings + && (pBuilding->Type->Capturable || pBuilding->Type->NeedsEngineer)) + { + // Send engineer's "enter" event + auto const pTag = pBuilding->AttachedTag; + if (args->Attacker && pTag) + pTag->RaiseEvent(TriggerEvent::EnteredBy, args->Attacker, CellStruct::Empty); + + pBuilding->SetOwningHouse(pSourceHouse, true); + } + + // Disarm bomb + if (pThis->AttachedBomb && pWHExt->FakeEngineer_BombDisarm) + pThis->AttachedBomb->Disarm(); + // Raise Combat Alert if (RulesExt::Global()->CombatAlert && damage > 1) { diff --git a/src/Ext/Techno/Hooks.TargetEvaluation.cpp b/src/Ext/Techno/Hooks.TargetEvaluation.cpp index ee259270f3..516f0004c3 100644 --- a/src/Ext/Techno/Hooks.TargetEvaluation.cpp +++ b/src/Ext/Techno/Hooks.TargetEvaluation.cpp @@ -1,8 +1,261 @@ #include "Body.h" +#include +#include +#include #include // Cursor & target acquisition stuff not directly tied to other features can go here. +#pragma region FakeEngineer + +// Skipping the next 2 small checks permits the AI to target structures, if used correctly (for example with the combination of AttackFriendlies) +DEFINE_HOOK(0x6F85C8, TechnoClass_EvaluateObject_TargetStructures, 0x7) +{ + return 0x6F866D; +} + +// Skipping the Immune check +DEFINE_HOOK(0x740402, UnitClass_WhatAction_Immune_FakeEngineer1, 0xA) +{ + enum { ForceNewValue = 0x74049F }; + + GET(TechnoClass* const, pThis, ESI); + GET(TechnoClass* const, pTarget, EDI); + + if (pThis && pTarget) + { + const auto pBuilding = abstract_cast(pTarget); + if (pBuilding) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, false, false, true); + + if (canBeAttacked) + { + if (canBeDefused) + R->EBX(Action::DisarmBomb); + else + R->EBX(Action::Attack); + + return ForceNewValue; + } + } + } + + return 0; +} + +// Skipping the C4 check +DEFINE_HOOK(0x740486, UnitClass_WhatAction_Immune_FakeEngineer2, 0xA) +{ + enum { ForceNewValue = 0x74049F }; + + GET(TechnoClass* const, pThis, ESI); + GET(TechnoClass* const, pTarget, EDI); + + if (pThis && pTarget) + { + const auto pBuilding = abstract_cast(pTarget); + if (pBuilding) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, false, false, true); + + if (canBeAttacked) + { + if (canBeDefused) + R->EBX(Action::DisarmBomb); + else + R->EBX(Action::Attack); + + return ForceNewValue; + } + } + } + + return 0; +} + +DEFINE_HOOK(0x417F63, AircraftClass_WhatAction_Immune_FakeEngineer, 0x5) +{ + enum { ForceNewValue = 0x417F68 }; + + GET(TechnoClass* const, pThis, ESI); + GET(BuildingClass* const, pBuilding, EDI); + + if (pThis && pBuilding) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, false, false, true); + + if (canBeAttacked) + { + R->EAX(canBeDefused ? Action::DisarmBomb : Action::Attack); + return ForceNewValue; + } + } + + return 0; +} + +DEFINE_HOOK(0x447527, BuildingClass_WhatAction_Immune_FakeEngineer, 0x5) +{ + enum { ForceNewValue = 0x44752C }; + + GET(TechnoClass* const, pThis, ESI); + GET(BuildingClass* const, pBuilding, EBP); + + if (pThis && pBuilding) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, false, false, true); + + if (canBeAttacked) + { + R->EAX(canBeDefused ? Action::DisarmBomb : Action::Attack); + return ForceNewValue; + } + } + + return 0; +} + +DEFINE_HOOK(0x51F179, InfantryClass_WhatAction_Immune_FakeEngineer, 0x5) +{ + enum { ForceNewValue = 0x51F17E }; + + GET(TechnoClass* const, pThis, EDI); + GET(BuildingClass* const, pBuilding, ESI); + + if (pThis && pBuilding) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pBuilding, false, false, true); + + if (canBeAttacked) + { + if (canBeDefused) + R->EBP(Action::DisarmBomb); + else + R->EBP(Action::Attack); + + return ForceNewValue; + } + } + + return 0; +} + + + +DEFINE_HOOK(0x6FC31C, TechnoClass_CanFire_ForceWeapon, 0xF) +{ + GET(AbstractClass* const, pThis, ESI); + GET(AbstractClass* const, pTarget, EBX); + REF_STACK(int, nWeaponIdx, STACK_OFFSET(0x10, 0xC)); + + const auto pFirer = abstract_cast(pThis); + const auto pVictim = abstract_cast(pTarget); + + if (!pFirer || !pVictim) + return 0; + + const auto pTypeExt = TechnoExt::ExtMap.Find(pFirer)->TypeExtData; + + // Force weapon check + int newIndex = pTypeExt->SelectForceWeapon(pFirer, pTarget); + + if (newIndex >= 0) + { + nWeaponIdx = newIndex; + } + else + { + // Multi weapon check + newIndex = pTypeExt->SelectMultiWeapon(pFirer, pTarget); + + if (newIndex >= 0) + nWeaponIdx = newIndex; + } + + return 0; +} + +DEFINE_HOOK(0x6FC705, TechnoClass_CanFire_CivilianBlock_FakeEngineer, 0x7) +{ + enum { BypassEntireBlock = 0x6FC879, ContinueVanilla = 0 }; + + WeaponTypeClass* pWeapon = R->EDI(); + if (!pWeapon) + pWeapon = *reinterpret_cast(R->ESP() + 0x10); + + if (pWeapon && pWeapon->Warhead) + { + const auto pWHExt = WarheadTypeExt::Fetch(pWeapon->Warhead); + if (pWHExt->FakeEngineer_CanCaptureBuildings + || pWHExt->FakeEngineer_CanRepairBridges + || pWHExt->FakeEngineer_CanDestroyBridges + || pWHExt->FakeEngineer_BombDisarm) + { + return BypassEntireBlock; + } + } + + return ContinueVanilla; +} + +DEFINE_HOOK(0x6FCB53, TechnoClass_CanFire_FakeEngineer, 0x9) +{ + enum { BypassToHeightAndRangeChecks = 0x6FCC5D, ContinueVanilla = 0 }; + + GET(TechnoClass* const, pFirer, ESI); + GET_STACK(AbstractClass* const, pTarget, 0x24); + GET_STACK(WeaponTypeClass* const, pWeapon, 0x10); + + auto const pActualTarget = pTarget ? pTarget : R->EBP(); + auto const pActualWeapon = pWeapon ? pWeapon : R->EBX(); + + if (pFirer && pActualTarget && pActualWeapon && pActualWeapon->Warhead) + { + const auto pWHExt = WarheadTypeExt::Fetch(pActualWeapon->Warhead); + const auto pVictim = abstract_cast(pActualTarget); + + if (pVictim && pVictim->AttachedBomb && pWHExt->FakeEngineer_BombDisarm) + return BypassToHeightAndRangeChecks; + + const auto pBuilding = abstract_cast(pActualTarget); + + if (pBuilding && pBuilding->IsAlive && pBuilding->Health > 0) + { + if (pBuilding->Type->BridgeRepairHut) + { + CellStruct bridgeRepairHutCell = pBuilding->GetMapCoords(); + bool isBridgeDamaged = MapClass::Instance.IsLinkedBridgeDestroyed(bridgeRepairHutCell); + + if ((isBridgeDamaged && pWHExt->FakeEngineer_CanRepairBridges) + || (!isBridgeDamaged && pWHExt->FakeEngineer_CanDestroyBridges)) + { + return BypassToHeightAndRangeChecks; + } + } + + const bool canCaptureHouse = pBuilding->Owner != pFirer->Owner + && (!pFirer->Owner->IsAlliedWith(pBuilding->Owner) || pBuilding->Owner->IsNeutral()); + + if (canCaptureHouse + && (pBuilding->Type->Capturable || pBuilding->Type->NeedsEngineer) + && pWHExt->FakeEngineer_CanCaptureBuildings) + { + return BypassToHeightAndRangeChecks; + } + } + } + + return ContinueVanilla; +} + +#pragma endregion + #pragma region TargetAcquisition DEFINE_HOOK(0x7098B9, TechnoClass_TargetSomethingNearby_AutoFire, 0x6) @@ -357,6 +610,20 @@ DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB418, InfantryClass__GetFireError_Wrapper) static Action __fastcall UnitClass__WhatAction_Wrapper(UnitClass* pThis, void* _, ObjectClass* pObj, bool ignoreForce) { + if (pThis && pObj) + { + if (const auto pTechnoTarget = abstract_cast(pObj)) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pTechnoTarget, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pTechnoTarget, false, false, true); + + if (canBeAttacked) + { + return canBeDefused ? Action::DisarmBomb : Action::Attack; + } + } + } + AresScheme::Prefix(pThis, pObj, -1, false); auto const result = pThis->UnitClass::MouseOverObject(pObj, ignoreForce); AresScheme::Suffix(); @@ -366,6 +633,20 @@ DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5CE4, UnitClass__WhatAction_Wrapper) static Action __fastcall InfantryClass__WhatAction_Wrapper(InfantryClass* pThis, void* _, ObjectClass* pObj, bool ignoreForce) { + if (pThis && pObj && !pThis->Type->Engineer) + { + if (const auto pTechnoTarget = abstract_cast(pObj)) + { + bool canBeAttacked = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pTechnoTarget, true, true, true); + bool canBeDefused = TechnoExt::CanBeAffectedByFakeEngineer(pThis, pTechnoTarget, false, false, true); + + if (canBeAttacked) + { + return canBeDefused ? Action::DisarmBomb : Action::Attack; + } + } + } + AresScheme::Prefix(pThis, pObj, -1, pThis->Type->Engineer); auto const result = pThis->InfantryClass::MouseOverObject(pObj, ignoreForce); AresScheme::Suffix(); diff --git a/src/Ext/Techno/WeaponHelpers.cpp b/src/Ext/Techno/WeaponHelpers.cpp index 2fdc543e8e..d4a5df6471 100644 --- a/src/Ext/Techno/WeaponHelpers.cpp +++ b/src/Ext/Techno/WeaponHelpers.cpp @@ -9,15 +9,20 @@ int TechnoExt::PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, A auto const pWeaponStructOne = pThis->GetWeapon(weaponIndexOne); auto const pWeaponStructTwo = pThis->GetWeapon(weaponIndexTwo); - if (!pWeaponStructOne && !pWeaponStructTwo) + bool hasOne = pWeaponStructOne && pWeaponStructOne->WeaponType; + bool hasTwo = pWeaponStructTwo && pWeaponStructTwo->WeaponType; + + if (!hasOne && !hasTwo) return -1; - else if (!pWeaponStructTwo) + else if (!hasTwo) return weaponIndexOne; - else if (!pWeaponStructOne) + else if (!hasOne) return weaponIndexTwo; auto const pWeaponTwo = pWeaponStructTwo->WeaponType; auto const pSecondExt = WeaponTypeExt::Fetch(pWeaponTwo); + if (!pSecondExt) + return weaponIndexOne; CellClass* pTargetCell = nullptr; @@ -48,8 +53,10 @@ int TechnoExt::PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, A } } - const bool secondIsAA = pTargetTechno && pTargetTechno->IsInAir() && pWeaponTwo->Projectile->AA; + const bool secondIsAA = pTargetTechno && pTargetTechno->IsInAir() && pWeaponTwo->Projectile && pWeaponTwo->Projectile->AA; auto const pFirstExt = WeaponTypeExt::Fetch(pWeaponStructOne->WeaponType); + if (!pFirstExt) + return weaponIndexTwo; const bool skipPrimaryPicking = pFirstExt->SkipWeaponPicking; const bool firstAllowedAE = skipPrimaryPicking || pFirstExt->HasRequiredAttachedEffects(pTargetTechno, pThis); @@ -422,8 +429,18 @@ bool TechnoExt::MultiWeaponCanFire(TechnoClass* const pThis, AbstractClass* cons if (pTechno) { + const auto pWHExt = WarheadTypeExt::Fetch(pWH); + const bool isFakeEngineer = pWHExt->FakeEngineer_CanCaptureBuildings + || pWHExt->FakeEngineer_CanRepairBridges + || pWHExt->FakeEngineer_CanDestroyBridges + || pWHExt->FakeEngineer_BombDisarm; + + bool houseAllowed = EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTechnoOwner); + if (!houseAllowed && isFakeEngineer && pTechnoOwner != pOwner && pTechnoOwner->IsNeutral()) + houseAllowed = true; + if (!EnumFunctions::IsTechnoEligible(pTechno, pWeaponExt->CanTarget) - || !EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTechnoOwner) + || !houseAllowed || !pWeaponExt->IsHealthInThreshold(pTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTechno, pThis)) { @@ -479,6 +496,18 @@ bool TechnoExt::MultiWeaponCanFire(TechnoClass* const pThis, AbstractClass* cons } } + const auto pWHExt = WarheadTypeExt::Fetch(pWH); + const bool isFakeEngineer = pWHExt->FakeEngineer_CanCaptureBuildings + || pWHExt->FakeEngineer_CanRepairBridges + || pWHExt->FakeEngineer_CanDestroyBridges + || pWHExt->FakeEngineer_BombDisarm; + + if (isFakeEngineer) + return true; + + if (pTechnoType->Immune) + return false; + if (GeneralUtils::GetWarheadVersusArmor(pWH, pTechno, pTechnoType) == 0.0) return false; } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index ae05a36241..38fa9ec63a 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -96,6 +96,17 @@ int TechnoTypeExt::SelectForceWeapon(TechnoClass* pThis, AbstractClass* pTarget) { forceWeaponIndex = this->ForceWeapon_UnderEMP; } + else if (this->ForceWeapon_Capture >= 0) + { + if (const auto pBuildingType = abstract_cast(pTargetType)) + { + const bool canCaptureHouse = pTargetTechno->Owner != pThis->Owner + && (!pThis->Owner->IsAlliedWith(pTargetTechno->Owner) || pTargetTechno->Owner->IsNeutral()); + + if ((pBuildingType->Capturable || pBuildingType->NeedsEngineer) && canCaptureHouse) + forceWeaponIndex = this->ForceWeapon_Capture; + } + } } if (forceWeaponIndex == -1 @@ -930,6 +941,7 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->ForceAAWeapon_Infantry.Read(exINI, pSection, "ForceAAWeapon.Infantry"); this->ForceAAWeapon_Units.Read(exINI, pSection, "ForceAAWeapon.Units"); this->ForceAAWeapon_Aircraft.Read(exINI, pSection, "ForceAAWeapon.Aircraft"); + this->ForceWeapon_Capture.Read(exINI, pSection, "ForceWeapon.Capture"); this->ForceWeapon_Check = ( this->ForceWeapon_Naval_Decloaked >= 0 @@ -947,6 +959,7 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) || this->ForceAAWeapon_Infantry >= 0 || this->ForceAAWeapon_Units >= 0 || this->ForceAAWeapon_Aircraft >= 0 + || this->ForceWeapon_Capture >= 0 ); this->Ammo_Shared.Read(exINI, pSection, "Ammo.Shared"); @@ -1595,6 +1608,7 @@ void TechnoTypeExt::Serialize(T& Stm) .Process(this->ForceAAWeapon_Infantry) .Process(this->ForceAAWeapon_Units) .Process(this->ForceAAWeapon_Aircraft) + .Process(this->ForceWeapon_Capture) .Process(this->Ammo_Shared) .Process(this->Ammo_Shared_Group) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index c7c2ba9ed6..f284c1fdd9 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -194,6 +194,7 @@ class TechnoTypeExt : public ObjectTypeExt Valueable ForceAAWeapon_Infantry; Valueable ForceAAWeapon_Units; Valueable ForceAAWeapon_Aircraft; + Valueable ForceWeapon_Capture; Valueable Ammo_Shared; Valueable Ammo_Shared_Group; diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index adf07d1090..7ca94d1b60 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -1,6 +1,9 @@ #include "Body.h" #include +#include +#include +#include WarheadTypeExt::ExtContainer WarheadTypeExt::ExtMap; @@ -12,6 +15,13 @@ bool WarheadTypeExt::CanTargetHouse(HouseClass* pHouse, TechnoClass* pTarget) co { const auto pOwner = pTarget->Owner; + if (this->FakeEngineer_BombDisarm && pTarget->AttachedBomb) + return true; + + const bool isFakeEngineerBuilding = (this->FakeEngineer_CanCaptureBuildings || this->FakeEngineer_CanRepairBridges || this->FakeEngineer_CanDestroyBridges); + if (isFakeEngineerBuilding && pOwner->IsNeutral() && pOwner != pHouse) + return true; + if (!this->AffectsNeutral && pOwner->IsNeutral()) return false; @@ -25,8 +35,11 @@ bool WarheadTypeExt::CanTargetHouse(HouseClass* pHouse, TechnoClass* pTarget) co if (affectsAllies && isAllies) return pOwner != pHouse; - if (this->AffectsEnemies && !isAllies) - return true; + if (this->AffectsEnemies && (!isAllies || (isFakeEngineerBuilding && pOwner->IsNeutral()))) + return pOwner != pHouse; + + if (isFakeEngineerBuilding && pOwner->IsNeutral()) + return pOwner != pHouse; return false; } @@ -490,6 +503,11 @@ void WarheadTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->Damage_Deployed.Read(exINI, pSection, "Damage.Deployed"); this->PreventScatter.Read(exINI, pSection, "PreventScatter"); + this->FakeEngineer_CanRepairBridges.Read(exINI, pSection, "FakeEngineer.CanRepairBridges"); + this->FakeEngineer_CanDestroyBridges.Read(exINI, pSection, "FakeEngineer.CanDestroyBridges"); + this->FakeEngineer_CanCaptureBuildings.Read(exINI, pSection, "FakeEngineer.CanCaptureBuildings"); + this->FakeEngineer_BombDisarm.Read(exINI, pSection, "FakeEngineer.BombDisarm"); + // List all Warheads here that respect CellSpread // Used in WarheadTypeExt::Detonate this->PossibleCellSpreadDetonate = ( @@ -821,6 +839,10 @@ void WarheadTypeExt::Serialize(T& Stm) .Process(this->Reflected) .Process(this->DamageAreaTarget) + .Process(this->FakeEngineer_CanRepairBridges) + .Process(this->FakeEngineer_CanDestroyBridges) + .Process(this->FakeEngineer_CanCaptureBuildings) + .Process(this->FakeEngineer_BombDisarm) .Process(this->Ammo) ; } diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index 7ee03e1291..09637e3e2e 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -236,6 +236,11 @@ class WarheadTypeExt final : public AbstractTypeExt Valueable ReverseEngineer; + Valueable FakeEngineer_CanRepairBridges; + Valueable FakeEngineer_CanDestroyBridges; + Valueable FakeEngineer_CanCaptureBuildings; + Valueable FakeEngineer_BombDisarm; + Valueable CanKill; Valueable UnlimboDetonate; @@ -458,6 +463,10 @@ class WarheadTypeExt final : public AbstractTypeExt , JumpjetNoWobbles {} , JumpjetDeviation {} + , FakeEngineer_CanRepairBridges { false } + , FakeEngineer_CanDestroyBridges { false } + , FakeEngineer_CanCaptureBuildings { false } + , FakeEngineer_BombDisarm { false } , Nonprovocative { false } , MergeBuildingDamage {} diff --git a/src/Ext/WarheadType/Hooks.cpp b/src/Ext/WarheadType/Hooks.cpp index e1103f05c1..071ac7d1ea 100644 --- a/src/Ext/WarheadType/Hooks.cpp +++ b/src/Ext/WarheadType/Hooks.cpp @@ -571,6 +571,53 @@ DEFINE_HOOK(0x442290, BuildingClass_ReceiveDamage_Nonprovocative1, 0x6) return pTypeExt->Nonprovocative ? SkipEvents : 0; } +DEFINE_HOOK(0x442046, BuildingClass_ReceiveDamage_Immune_FakeEngineer, 0x6) +{ + enum { BypassImmuneExit = 0x44204C }; + + GET(BuildingClass*, pThis, ESI); + GET_STACK(WarheadTypeClass*, pWarhead, STACK_OFFSET(0x9C, 0xC)); + + if (pThis && pWarhead) + { + auto const pWHExt = WarheadTypeExt::Fetch(pWarhead); + if (pWHExt->FakeEngineer_CanCaptureBuildings + || pWHExt->FakeEngineer_CanRepairBridges + || pWHExt->FakeEngineer_CanDestroyBridges + || pWHExt->FakeEngineer_BombDisarm) + { + return BypassImmuneExit; + } + } + + return 0; +} + +DEFINE_HOOK(0x4423B7, BuildingClass_ReceiveDamage_BridgeRepairHut, 0xC) +{ + GET_STACK(WarheadTypeClass*, pWarhead, STACK_OFFSET(0x9C, 0xC)); + GET_STACK(TechnoClass*, pSource, STACK_OFFSET(0x9C, 0x10)); + GET_STACK(HouseClass*, pHouse, STACK_OFFSET(0x9C, 0x18)); + GET(BuildingClass*, pThis, ESI); + + if (pThis && pWarhead) + { + auto const pWHExt = WarheadTypeExt::ExtMap.Find(pWarhead); + const bool isBridgeDestroyed = MapClass::Instance.IsLinkedBridgeDestroyed(pThis->GetMapCoords()); + + if (!isBridgeDestroyed && pWHExt->FakeEngineer_CanDestroyBridges) + { + TechnoExt::RepairOrDestroyBridgeHut(pThis, pSource, pHouse, true); + } + else if (isBridgeDestroyed && pWHExt->FakeEngineer_CanRepairBridges) + { + TechnoExt::RepairOrDestroyBridgeHut(pThis, pSource, pHouse, false); + } + } + + return 0; +} + // Suppress all events and alerts that come from attacking a building, unlike Ares' Malicious this includes all EVA notifications AND events DEFINE_HOOK(0x442956, BuildingClass_ReceiveDamage_Nonprovocative2, 0x6) {