From 9b82335b3eed08c4035870f0ca91230b546957f2 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:13:49 +0800 Subject: [PATCH 1/9] crusher level system --- CREDITS.md | 1 + docs/New-or-Enhanced-Logics.md | 55 +++++++++++++++++++++ docs/Whats-New.md | 1 + src/Ext/Rules/Body.cpp | 15 ++++++ src/Ext/Rules/Body.h | 16 +++++++ src/Ext/TechnoType/Body.cpp | 84 +++++++++++++++++++++++++++++++++ src/Ext/TechnoType/Body.h | 11 +++++ src/Ext/Unit/Hooks.Crushing.cpp | 28 +++++++++++ 8 files changed, 211 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index f977b2a3ed..098f22fd90 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -408,3 +408,4 @@ This page lists all the individual contributions to the project by their author. - **Damfoos** - extensive and thorough testing - **Dmitry Volkov** - extensive and thorough testing - **Rise of the East community** - extensive playtesting of in-dev features +- **Aephiex** - crusher level system \ No newline at end of file diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index b789131486..5d469c958e 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1366,6 +1366,61 @@ Convert.HumanToComputer = ; TechnoType Convert.ComputerToHuman = ; TechnoType ``` +### Crusher level and crushable level + +- A techno can now be specified with a `CrusherLevel=` and `CrushableLevel=` akin to that of successing CNC titles. This feature completely takes over the crush check and must be turned on manually by `[General]►CrusherLevelEnabled=true` before it can take any effect. +- A unit can crush something if its `CrusherLevel` is greater than the latter's `CrushableLevel`. If not set, the default value will be taken from `[General]` settings. The default values of the `[General]` settings themselves follow the convention of *[Command and Conquer 3: Tiberium Wars](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Tiberium_Wars)* and *[Kane's Wrath](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Kane%27s_Wrath)*. + - `CrusherLevel=`: + - 0 if `Crusher=no` + - 1 if `Crusher=yes` and `OmniCrusher=no` + - 3 if `Crusher=yes` and `OmniCrusher=yes` + - `CrushableLevel=`: + - 0 if `Crushable=yes` + - 1 if `Crushable=no` and `OmniCrushResistant=no` and is an Infantry + - 2 if `Crushable=no` and `OmniCrushResistant=no` and is NOT an Infantry + - 3 if `Crushable=no` and `OmniCrushResistant=yes` + - `DeployedCrushableLevel=`: + - The same value as `CrushableLevel` if it was set + - 0 if `Crushable=yes` and `DeployedCrushable=yes` + - 1 if `Crushable=yes`, `DeployedCrushable=no`, and `OmniCrushResistant=no` + - 3 if `Crushable=yes`, `DeployedCrushable=no`, and `OmniCrushResistant=yes` + - Here is a quick lookup of the default values of `CrusherLevel` and `CrushableLevel` for Yuri's Revenge units: + - Conscript: 0/0 + - Tesla Trooper: 0/1 + - Guardian G.I.: 0/0 when undeployed, 0/1 when deployed + - T-Rex: 0/3 + - IFV: 0/2 + - Rhino Tank: 1/2 + - Slave Miner: 1/3 + - Battle Fortress: 3/3 + - A few applications of the crusher level system: + - At 2/2, a vehicle can crush Tesla Troopers and deployed Guardian G.I.s, but it can't crush IFVs and is still crushable by Battle Fortresses, just like a [Scorpion Tank](https://cnc-central.fandom.com/wiki/Scorpion_tank_(Tiberium_Wars)) does with the Dozer blades upgrade. + - At 4/4, a vehicle can crush almost anything else, even Battle Fortresses, just like a [MARV](https://cnc-central.fandom.com/wiki/Mammoth_Armored_Reclamation_Vehicle) does. +- Other usage notes: + - A unit must has a locomotor that supports crushing before it can crush something. Most naval units don't, save for the amphibious transports. + - In an unmodded game, it doesn't even try to check if it can crush something if it has `Crusher=no`, meaning `OmniCrusher=yes` make no sense on a unit with `Crusher=no`. This behavior isn't changed by this feature, meaning you will still need `Crusher=yes` for a positive `CrushableLevel` to function. + - In an unmodded game, infantries can never crush anything regardless of `Crusher=yes` or locomotors. This behavior isn't changed by this feature, meaning a positive `CrusherLevel` makes no sense on an infantry type. + - If `CrushableLevel` is set, `Crushable`, `OmniCrushResistant`, and `DeployedCrushable` are redundant and ignored. Use `DeployedCrushableLevel` instead if you wish the infantry to have a different crushable level when deployed. + - If `CrushableLevel` is unset, `DeployedCrushableLevel` does not apply at all. +- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` and `OmniCrushResistant=yes` by default, meaning they can't be crushed by normal means. The crusher level system does not apply to buildings by default, and it must be turned on manually by `[General]►CrusherLevelEnabled.For1x1Buildings=true`. Note that crushing buildings may cause unexpected behavior of the game, such as crushing a Bridge Repair Hut can render the bridge irrepairable. + +In `rulesmd.ini` +```ini +[General] +CrusherLevelEnabled=false ; boolean +CrusherLevelEnabled.For1x1Buildings=false ; boolean +CrusherLevel.Defaults.Crusher=1 ; integer +CrusherLevel.Defaults.OmniCrusher=3 ; integer +CrushableLevel.Defaults.Uncrushable.Infantry=1 ; integer +CrushableLevel.Defaults.Uncrushable.Others=2 ; integer +CrushableLevel.Defaults.OmniCrushResistant=3 ; integer + +[SOMETECHNO] ; TechnoType +CrusherLevel= ; integer +CrushableLevel= ; integer +DeployedCrushableLevel= ; integer; this only works for [InfantryTypes] +``` + ## Terrain ### Destroy animation & sound diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 0b6f8c19c6..cdb4096960 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -470,6 +470,7 @@ New: - `` can now be used as owner for pre-placed objects on skirmish and multiplayer maps (by Starkku) - Allow customizing charge turret delays per burst on a weapon (by Starkku) - Unit `Speed` setting now accepts floating point values (by Starkku) +- Crusher level system (by Aephiex) Vanilla fixes: - Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index bc72f69570..a95e06fe45 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -207,6 +207,14 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->CombatLightDetailLevel.Read(exINI, GameStrings::AudioVisual, "CombatLightDetailLevel"); this->LightFlashAlphaImageDetailLevel.Read(exINI, GameStrings::AudioVisual, "LightFlashAlphaImageDetailLevel"); + this->CrusherLevelEnabled.Read(exINI, GameStrings::General, "CrusherLevelEnabled"); + this->CrusherLevelEnabled_For1x1Buildings.Read(exINI, GameStrings::General, "CrusherLevelEnabled.For1x1Buildings"); + this->CrusherLevel_Defaults_Crusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.Crusher"); + this->CrusherLevel_Defaults_OmniCrusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.OmniCrusher"); + this->CrushableLevel_Defaults_Uncrushable_Infantry.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Infantry"); + this->CrushableLevel_Defaults_Uncrushable_Others.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Others"); + this->CrushableLevel_Defaults_OmniCrushResistant.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.OmniCrushResistant"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -389,6 +397,13 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->WarheadParticleAlphaImageIsLightFlash) .Process(this->CombatLightDetailLevel) .Process(this->LightFlashAlphaImageDetailLevel) + .Process(this->CrusherLevelEnabled) + .Process(this->CrusherLevelEnabled_For1x1Buildings) + .Process(this->CrusherLevel_Defaults_Crusher) + .Process(this->CrusherLevel_Defaults_OmniCrusher) + .Process(this->CrushableLevel_Defaults_Uncrushable_Infantry) + .Process(this->CrushableLevel_Defaults_Uncrushable_Others) + .Process(this->CrushableLevel_Defaults_OmniCrushResistant) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index b2cce2d4cd..ed5165a5a7 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -165,6 +165,14 @@ class RulesExt Valueable CombatLightDetailLevel; Valueable LightFlashAlphaImageDetailLevel; + Valueable CrusherLevelEnabled; + Valueable CrusherLevelEnabled_For1x1Buildings; + Valueable CrusherLevel_Defaults_Crusher; + Valueable CrusherLevel_Defaults_OmniCrusher; + Valueable CrushableLevel_Defaults_Uncrushable_Infantry; + Valueable CrushableLevel_Defaults_Uncrushable_Others; + Valueable CrushableLevel_Defaults_OmniCrushResistant; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , InfantryGainSelfHealCap {} @@ -284,6 +292,14 @@ class RulesExt , WarheadParticleAlphaImageIsLightFlash { false } , CombatLightDetailLevel { 0 } , LightFlashAlphaImageDetailLevel { 0 } + + , CrusherLevelEnabled { false } + , CrusherLevelEnabled_For1x1Buildings { false } + , CrusherLevel_Defaults_Crusher { 1 } + , CrusherLevel_Defaults_OmniCrusher { 3 } + , CrushableLevel_Defaults_Uncrushable_Infantry { 1 } + , CrushableLevel_Defaults_Uncrushable_Others { 2 } + , CrushableLevel_Defaults_OmniCrushResistant { 3 } { } virtual ~ExtData() = default; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 0091ceb22e..24cd2b8d15 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -32,6 +32,82 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) mtx->Translate(x, y, z); } +// This function is called upon a potential crusher's perspective and returns the crusher level of it. +// This function is intended to use only on a unit with "Crusher=yes". +int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) +{ + // Returns the CrusherLevel if explictly set. + if (this->CrusherLevel.isset()) + { + return this->CrusherLevel.Get(0); + } + + // Otherwise, gets a default value for CrusherLevel. + return pCrusher->GetTechnoType()->OmniCrusher ? + RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher : + RulesExt::Global()->CrusherLevel_Defaults_Crusher; +} + +// This function is called upon a potential crushing victim's perspective and returns the crushable level of it. +// This function is intended to use only on an infantry, a unit, or an overlay. +// Passing anything else such as terrain types into this function can cause a game crash. +int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) +{ + // If this techno is infantry: + if (auto const pVictimInfantry = abstract_cast(pVictim)) + { + // Returns the CrushableLevel if explictly set. + // Respects the "DeployedCrushableLevel=" setting if the infantry is deployed. + // Unlike the unmodded game, where you cannot tell an infantry is "Crushable=no" and "DeployableCrushable=yes", + // here you may have an infantry come with a lower CrushableLevel when deployed. + if (this->CrushableLevel.isset()) + { + if (pVictimInfantry->IsDeployed()) + { + return this->DeployedCrushableLevel.Get(this->CrushableLevel.Get(0)); + } + else + { + return this->CrushableLevel.Get(0); + } + } + + // Otherwise, gets a default value for CrushableLevel. + // If the InfantryType has "Crushable=yes", and it doesn't have "DeployedCrushable=no" and is deployed, then it can always be crushed. + // Note that in base game logic, "OmniCrushResistant=yes" only prevents "OmniCrusher=yes", it does not prevent "Crusher=yes". + // There fore, "Crushable=yes" and "OmniCrushResistant=yes" can still be crushed by "Crusher=yes" and "OmniCrusher=yes". + // Plus the fact that "OmniCrusher=yes" requires "Crusher=yes" to function, + // I'm ignoring the "OmniCrushResistant=" entry if "Crushable=yes" in the first place. + auto const pVictimInfTypeClass = abstract_cast(pVictimInfantry->GetTechnoType()); + if (!pVictimInfTypeClass->Crushable || (!pVictimInfTypeClass->DeployedCrushable && pVictimInfantry->IsDeployed())) + { + return pVictimInfTypeClass->OmniCrushResistant ? + RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant : + RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + } + } + + // If this is something else: + else + { + // Returns the CrushableLevel if explictly set. + if (this->CrushableLevel.isset()) + { + return this->CrushableLevel.Get(0); + } + // Otherwise, gets a default value for CrushableLevel. + // If this is explictly set as "Crushable=yes" then regard it crushable. + if (!pVictim->GetTechnoType()->Crushable) + { + return pVictim->GetTechnoType()->OmniCrushResistant ? + RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant : + RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Others; + } + } + + return 0; +} + // Ares 0.A source const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const { @@ -414,6 +490,10 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->CrushOverlayExtraForwardTilt.Read(exINI, pSection, "CrushOverlayExtraForwardTilt"); this->CrushSlowdownMultiplier.Read(exINI, pSection, "CrushSlowdownMultiplier"); + this->CrusherLevel.Read(exINI, pSection, "CrusherLevel"); + this->CrushableLevel.Read(exINI, pSection, "CrushableLevel"); + this->DeployedCrushableLevel.Read(exINI, pSection, "DeployedCrushableLevel"); + this->DigitalDisplay_Disable.Read(exINI, pSection, "DigitalDisplay.Disable"); this->DigitalDisplayTypes.Read(exINI, pSection, "DigitalDisplayTypes"); @@ -781,6 +861,10 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->CrushOverlayExtraForwardTilt) .Process(this->CrushSlowdownMultiplier) + .Process(this->CrusherLevel) + .Process(this->CrushableLevel) + .Process(this->DeployedCrushableLevel) + .Process(this->DigitalDisplay_Disable) .Process(this->DigitalDisplayTypes) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 3335acac05..6679de8927 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -183,6 +183,10 @@ class TechnoTypeExt Valueable CrushOverlayExtraForwardTilt; Valueable CrushSlowdownMultiplier; + Nullable CrusherLevel; + Nullable CrushableLevel; + Nullable DeployedCrushableLevel; + Valueable DigitalDisplay_Disable; ValueableVector DigitalDisplayTypes; @@ -405,6 +409,10 @@ class TechnoTypeExt , CrushForwardTiltPerFrame {} , CrushOverlayExtraForwardTilt { 0.02 } + , CrusherLevel {} + , CrushableLevel {} + , DeployedCrushableLevel {} + , DigitalDisplay_Disable { false } , DigitalDisplayTypes {} @@ -465,6 +473,9 @@ class TechnoTypeExt void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0); + int GetCrusherLevel(FootClass* pCrusher); + int GetCrushableLevel(FootClass* pVictim); + // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/Unit/Hooks.Crushing.cpp b/src/Ext/Unit/Hooks.Crushing.cpp index 978a4f8bb7..3b6e935d7c 100644 --- a/src/Ext/Unit/Hooks.Crushing.cpp +++ b/src/Ext/Unit/Hooks.Crushing.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -99,3 +100,30 @@ DEFINE_HOOK(0x6A108D, ShipLocomotionClass_WhileMoving_CrushTilt, 0xD) return SkipGameCode; } + +DEFINE_HOOK(0x5F6CE0, FootClass_CanGetCrushed_Hook, 6) +{ + enum { CanCrush = 0x5F6D85, CannotCrush = 0x5F6D8C }; + + GET(FootClass* const, pCrusher, EDI); + GET(FootClass* const, pVictim, ESI); + + // An eligible crusher must be a unit with "Crusher=yes". + // An eligible victim must be either an infantry, a unit, an overlay, or a building with 1x1 foundation. + if (RulesExt::Global()->CrusherLevelEnabled && + pCrusher && pCrusher->WhatAmI() == AbstractType::Unit && pCrusher->GetTechnoType()->Crusher && + pVictim && (pVictim->WhatAmI() == AbstractType::Infantry || + pVictim->WhatAmI() == AbstractType::Unit || + pVictim->WhatAmI() == AbstractType::Overlay || + (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building && + abstract_cast(pVictim->GetTechnoType())->Foundation == Foundation::_1x1))) + { + auto pCrusherExt = TechnoTypeExt::ExtMap.Find(pCrusher->GetTechnoType()); + auto pVictimExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); + int crusherLevel = pCrusherExt->GetCrusherLevel(pCrusher); + int crushableLevel = pVictimExt->GetCrushableLevel(pVictim); + return crusherLevel > crushableLevel ? CanCrush : CannotCrush; + } + + return 0; +} From 6332ceec67fdc6dafb167f6f9ee6d372548a978a Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Wed, 1 Jan 2025 00:14:41 +0800 Subject: [PATCH 2/9] overhaul --- docs/Fixed-or-Improved-Logics.md | 2 +- docs/New-or-Enhanced-Logics.md | 35 ++++--- src/Ext/Rules/Body.cpp | 6 +- src/Ext/Rules/Body.h | 6 +- src/Ext/TechnoType/Body.cpp | 151 ++++++++++++++++++++----------- src/Ext/TechnoType/Body.h | 5 +- src/Ext/Unit/Hooks.Crushing.cpp | 30 +++--- 7 files changed, 146 insertions(+), 89 deletions(-) diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 3cf0271d9b..fd81ccb61e 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1124,7 +1124,7 @@ MinimapColor= ; integer - Red,Green,Blue ### Customizing crushing tilt and slowdown -- Vehicles with `Crusher=true` and `OmniCrusher=true` / `MovementZone=CrusherAll` were hardcoded to tilt when crushing vehicles / walls respectively. This now obeys `TiltsWhenCrushes` but can be customized individually for these two scenarios using `TiltsWhenCrusher.Vehicles` and `TiltsWhenCrusher.Overlays`, which both default to `TiltsWhenCrushes`. +- Vehicles with `Crusher=true` and `OmniCrusher=true` / `MovementZone=CrusherAll` were hardcoded to tilt when crushing vehicles / walls respectively. This now obeys `TiltsWhenCrushes` but can be customized individually for these two scenarios using `TiltsWhenCrushes.Vehicles` and `TiltsWhenCrushes.Overlays`, which both default to `TiltsWhenCrushes`. - `CrushForwardTiltPerFrame` determines how much the forward tilt is adjusted per frame when crushing overlays or vehicles. Defaults to -0.02 for vehicles using Ship locomotor crushing overlays, -0.050000001 for all other cases. - `CrushOverlayExtraForwardTilt` is additional forward tilt applied after an overlay has been crushed by the vehicle. - It is possible to customize the movement speed slowdown when `MovementZone=CrusherAll` vehicle crushes walls by setting `CrushSlowdownMultiplier`. diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 5d469c958e..3cc136be93 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1368,22 +1368,22 @@ Convert.ComputerToHuman = ; TechnoType ### Crusher level and crushable level -- A techno can now be specified with a `CrusherLevel=` and `CrushableLevel=` akin to that of successing CNC titles. This feature completely takes over the crush check and must be turned on manually by `[General]►CrusherLevelEnabled=true` before it can take any effect. -- A unit can crush something if its `CrusherLevel` is greater than the latter's `CrushableLevel`. If not set, the default value will be taken from `[General]` settings. The default values of the `[General]` settings themselves follow the convention of *[Command and Conquer 3: Tiberium Wars](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Tiberium_Wars)* and *[Kane's Wrath](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Kane%27s_Wrath)*. - - `CrusherLevel=`: +- A techno can now be specified with a `CrusherLevel=` and `CrushableLevel=` akin to that of successing CNC titles. Since this feature completely takes over the crush check, it must be turned on by `[General]►CrusherLevelEnabled=true`. +- A vehicle can crush an infantry or a vehicle if its `CrusherLevel` is greater than the latter's `CrushableLevel`. If not set, the default value will be taken from `[General]` settings. The default values of the `[General]` settings themselves follow the convention of *[Command and Conquer 3: Tiberium Wars](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Tiberium_Wars)* and *[Kane's Wrath](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Kane%27s_Wrath)*. + - `CrusherLevel=` of vehicle types: - 0 if `Crusher=no` - 1 if `Crusher=yes` and `OmniCrusher=no` - 3 if `Crusher=yes` and `OmniCrusher=yes` - `CrushableLevel=`: - 0 if `Crushable=yes` - - 1 if `Crushable=no` and `OmniCrushResistant=no` and is an Infantry - - 2 if `Crushable=no` and `OmniCrushResistant=no` and is NOT an Infantry + - 1 if `Crushable=no` and `OmniCrushResistant=no` and is an infantry + - 2 if `Crushable=no` and `OmniCrushResistant=no` and is a vehicle - 3 if `Crushable=no` and `OmniCrushResistant=yes` - - `DeployedCrushableLevel=`: - - The same value as `CrushableLevel` if it was set + - `DeployedCrushableLevel=` of infantry types: - 0 if `Crushable=yes` and `DeployedCrushable=yes` - 1 if `Crushable=yes`, `DeployedCrushable=no`, and `OmniCrushResistant=no` - 3 if `Crushable=yes`, `DeployedCrushable=no`, and `OmniCrushResistant=yes` + - The same as `CrushableLevel` in any other case - Here is a quick lookup of the default values of `CrusherLevel` and `CrushableLevel` for Yuri's Revenge units: - Conscript: 0/0 - Tesla Trooper: 0/1 @@ -1397,12 +1397,18 @@ Convert.ComputerToHuman = ; TechnoType - At 2/2, a vehicle can crush Tesla Troopers and deployed Guardian G.I.s, but it can't crush IFVs and is still crushable by Battle Fortresses, just like a [Scorpion Tank](https://cnc-central.fandom.com/wiki/Scorpion_tank_(Tiberium_Wars)) does with the Dozer blades upgrade. - At 4/4, a vehicle can crush almost anything else, even Battle Fortresses, just like a [MARV](https://cnc-central.fandom.com/wiki/Mammoth_Armored_Reclamation_Vehicle) does. - Other usage notes: - - A unit must has a locomotor that supports crushing before it can crush something. Most naval units don't, save for the amphibious transports. - - In an unmodded game, it doesn't even try to check if it can crush something if it has `Crusher=no`, meaning `OmniCrusher=yes` make no sense on a unit with `Crusher=no`. This behavior isn't changed by this feature, meaning you will still need `Crusher=yes` for a positive `CrushableLevel` to function. - - In an unmodded game, infantries can never crush anything regardless of `Crusher=yes` or locomotors. This behavior isn't changed by this feature, meaning a positive `CrusherLevel` makes no sense on an infantry type. + - A unit must has a locomotor that supports crushing before it can crush something. Most naval units don't, except for the amphibious transports. + - If a vehicle has a positive `CrusherLevel`, since it is presumably intended to be a crusher, it will be silently assigned with `Crusher=yes`, otherwise the game doesn't even try to check if it can crush something. + - Existing `Crusher=yes` entry isn't removed even if it had a `CrusherLevel` <= 0. + - If a vehicle has a `CrusherLevel` >= `[General]►CrushableLevel.Defaults.Uncrushable.Building`, since it is presumably intended to be able to crush walls, it will be silently assigned with `OmniCrusher=yes`, otherwise it won't be able to crush walls, unlike those explicitly given with `OmniCrusher=yes`. + - Existing `OmniCrusher=yes` entry isn't removed even if it had a `CrusherLevel` lower than that. + - If `CrusherLevel` is set, `OmniCrusher` is redundant and ignored. - If `CrushableLevel` is set, `Crushable`, `OmniCrushResistant`, and `DeployedCrushable` are redundant and ignored. Use `DeployedCrushableLevel` instead if you wish the infantry to have a different crushable level when deployed. - - If `CrushableLevel` is unset, `DeployedCrushableLevel` does not apply at all. -- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` and `OmniCrushResistant=yes` by default, meaning they can't be crushed by normal means. The crusher level system does not apply to buildings by default, and it must be turned on manually by `[General]►CrusherLevelEnabled.For1x1Buildings=true`. Note that crushing buildings may cause unexpected behavior of the game, such as crushing a Bridge Repair Hut can render the bridge irrepairable. + - If `CrushableLevel` is not set, `DeployedCrushableLevel` does not apply. +- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` by default and they are hard-coded to be immune to `OmniCrusher`, meaning they can't be crushed by normal means. Thus, the crusher level system does not apply to buildings by default, and it must be turned on by `[General]►CrusherLevelEnabled.For1x1Buildings=true`. If not turned on, crushing buildings fallbacks to unmodded behavior. + - Crushing a building triggers no animation or sound effect, and the building is silently removed from the map. + - Crushing a building may cause unexpected behavior of the game, such as crushing all Bridge Repair Huts can make a bridge irrepairable. +- The crusher level system does not apply to walls. Crushing walls fallbacks to unmodded behavior. In `rulesmd.ini` ```ini @@ -1412,11 +1418,12 @@ CrusherLevelEnabled.For1x1Buildings=false ; boolean CrusherLevel.Defaults.Crusher=1 ; integer CrusherLevel.Defaults.OmniCrusher=3 ; integer CrushableLevel.Defaults.Uncrushable.Infantry=1 ; integer -CrushableLevel.Defaults.Uncrushable.Others=2 ; integer +CrushableLevel.Defaults.Uncrushable.Unit=2 ; integer +CrushableLevel.Defaults.Uncrushable.Building=3 ; integer CrushableLevel.Defaults.OmniCrushResistant=3 ; integer [SOMETECHNO] ; TechnoType -CrusherLevel= ; integer +CrusherLevel= ; integer; this only works for [VehicleTypes] CrushableLevel= ; integer DeployedCrushableLevel= ; integer; this only works for [InfantryTypes] ``` diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index a95e06fe45..91d5f9abe9 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -212,7 +212,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->CrusherLevel_Defaults_Crusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.Crusher"); this->CrusherLevel_Defaults_OmniCrusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.OmniCrusher"); this->CrushableLevel_Defaults_Uncrushable_Infantry.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Infantry"); - this->CrushableLevel_Defaults_Uncrushable_Others.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Others"); + this->CrushableLevel_Defaults_Uncrushable_Unit.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Unit"); + this->CrushableLevel_Defaults_Uncrushable_Building.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Building"); this->CrushableLevel_Defaults_OmniCrushResistant.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.OmniCrushResistant"); // Section AITargetTypes @@ -402,7 +403,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->CrusherLevel_Defaults_Crusher) .Process(this->CrusherLevel_Defaults_OmniCrusher) .Process(this->CrushableLevel_Defaults_Uncrushable_Infantry) - .Process(this->CrushableLevel_Defaults_Uncrushable_Others) + .Process(this->CrushableLevel_Defaults_Uncrushable_Unit) + .Process(this->CrushableLevel_Defaults_Uncrushable_Building) .Process(this->CrushableLevel_Defaults_OmniCrushResistant) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index ed5165a5a7..530941107d 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -170,7 +170,8 @@ class RulesExt Valueable CrusherLevel_Defaults_Crusher; Valueable CrusherLevel_Defaults_OmniCrusher; Valueable CrushableLevel_Defaults_Uncrushable_Infantry; - Valueable CrushableLevel_Defaults_Uncrushable_Others; + Valueable CrushableLevel_Defaults_Uncrushable_Unit; + Valueable CrushableLevel_Defaults_Uncrushable_Building; Valueable CrushableLevel_Defaults_OmniCrushResistant; ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) @@ -298,7 +299,8 @@ class RulesExt , CrusherLevel_Defaults_Crusher { 1 } , CrusherLevel_Defaults_OmniCrusher { 3 } , CrushableLevel_Defaults_Uncrushable_Infantry { 1 } - , CrushableLevel_Defaults_Uncrushable_Others { 2 } + , CrushableLevel_Defaults_Uncrushable_Unit { 2 } + , CrushableLevel_Defaults_Uncrushable_Building { 3 } , CrushableLevel_Defaults_OmniCrushResistant { 3 } { } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 24cd2b8d15..24220c7f01 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -19,6 +19,9 @@ TechnoTypeExt::ExtContainer TechnoTypeExt::ExtMap; void TechnoTypeExt::ExtData::Initialize() { this->ShieldType = ShieldTypeClass::FindOrAllocate(NONE_STR); + + if (RulesExt::Global()->CrusherLevelEnabled) + InitCrusherLevel(); } void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) @@ -32,80 +35,120 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) mtx->Translate(x, y, z); } -// This function is called upon a potential crusher's perspective and returns the crusher level of it. -// This function is intended to use only on a unit with "Crusher=yes". -int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) +// Init the `CrusherLevel`, `CrushableLevel`, and `DeployedCrushableLevel` for a techno type. +void TechnoTypeExt::ExtData::InitCrusherLevel() { - // Returns the CrusherLevel if explictly set. - if (this->CrusherLevel.isset()) + // Crusher related. + // Only UnitType can crush something, so we don't bother to do this for anything else. + if (this->OwnerObject()->WhatAmI() == AbstractType::UnitType) { - return this->CrusherLevel.Get(0); - } - - // Otherwise, gets a default value for CrusherLevel. - return pCrusher->GetTechnoType()->OmniCrusher ? - RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher : - RulesExt::Global()->CrusherLevel_Defaults_Crusher; -} - -// This function is called upon a potential crushing victim's perspective and returns the crushable level of it. -// This function is intended to use only on an infantry, a unit, or an overlay. -// Passing anything else such as terrain types into this function can cause a game crash. -int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) -{ - // If this techno is infantry: - if (auto const pVictimInfantry = abstract_cast(pVictim)) - { - // Returns the CrushableLevel if explictly set. - // Respects the "DeployedCrushableLevel=" setting if the infantry is deployed. - // Unlike the unmodded game, where you cannot tell an infantry is "Crushable=no" and "DeployableCrushable=yes", - // here you may have an infantry come with a lower CrushableLevel when deployed. - if (this->CrushableLevel.isset()) + if (!this->CrusherLevel.isset()) { - if (pVictimInfantry->IsDeployed()) + // `CrusherLevel` is not set, we take a default value based on `Crusher` and `OmniCrusher`. + if (this->OwnerObject()->Crusher) { - return this->DeployedCrushableLevel.Get(this->CrushableLevel.Get(0)); + if (this->OwnerObject()->OmniCrusher) + this->CrusherLevel = RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher; + else + this->CrusherLevel = RulesExt::Global()->CrusherLevel_Defaults_Crusher; } else { - return this->CrushableLevel.Get(0); + this->CrusherLevel = 0; } } - - // Otherwise, gets a default value for CrushableLevel. - // If the InfantryType has "Crushable=yes", and it doesn't have "DeployedCrushable=no" and is deployed, then it can always be crushed. - // Note that in base game logic, "OmniCrushResistant=yes" only prevents "OmniCrusher=yes", it does not prevent "Crusher=yes". - // There fore, "Crushable=yes" and "OmniCrushResistant=yes" can still be crushed by "Crusher=yes" and "OmniCrusher=yes". - // Plus the fact that "OmniCrusher=yes" requires "Crusher=yes" to function, - // I'm ignoring the "OmniCrushResistant=" entry if "Crushable=yes" in the first place. - auto const pVictimInfTypeClass = abstract_cast(pVictimInfantry->GetTechnoType()); - if (!pVictimInfTypeClass->Crushable || (!pVictimInfTypeClass->DeployedCrushable && pVictimInfantry->IsDeployed())) + else { - return pVictimInfTypeClass->OmniCrushResistant ? - RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant : - RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + // `CrusherLevel` is set, we make sure `Crusher` and `OmniCrusher` are present when needed, to make .ini coding easier. + // If crusher level is greater than zero, then make the techno type itself a crusher. + // If we don't, a vehicle type that is not a crusher can never crush anything, even if it had a sky high crusher level. + // The purpose is to make `Crusher=true` unnecessary for a vehicle that is clearly intended to be a crusher. + // This doesn't remove existing `Crusher=true` if the vehicle had a zero or negative crusher level. + if (!this->OwnerObject()->Crusher && this->CrusherLevel > 0) + { + this->OwnerObject()->Crusher = true; + } + + // If crusher level is equal to or greater than the default value for `OmniCrusher`, then make the techno itself an omni crusher. + // Otherwise, it can't crush walls that resist normal crusher, if only a high crusher level is given but `OmniCrusher=true` is not. + // The purpose is to make `OmniCrusher=true` unnecessary for a vehicle that is clearly intended to be an omni crusher. + // This doesn't remove existing `OmniCrusher=true` if the vehicle's crusher level is lower than the default value for `OmniCrusher`. + if (!this->OwnerObject()->OmniCrusher && this->CrusherLevel >= RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher) + { + this->OwnerObject()->OmniCrusher = true; + } } } - // If this is something else: - else + // Crushable related. + if (!this->CrushableLevel.isset()) { - // Returns the CrushableLevel if explictly set. - if (this->CrushableLevel.isset()) + // `CrushableLevel` is not set, we take a default value based on `Crushable`, `OmniCrushResistant`, and the techno's type. + if (this->OwnerObject()->Crushable) { - return this->CrushableLevel.Get(0); + this->CrushableLevel = 0; } - // Otherwise, gets a default value for CrushableLevel. - // If this is explictly set as "Crushable=yes" then regard it crushable. - if (!pVictim->GetTechnoType()->Crushable) + else if (this->OwnerObject()->OmniCrushResistant) { - return pVictim->GetTechnoType()->OmniCrushResistant ? - RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant : - RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Others; + this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant; + } + else + { + // The techno types are infantry types, unit types, aircraft types, and building types. + // Overlay types are not techno types. + switch (this->OwnerObject()->WhatAmI()) + { + case AbstractType::InfantryType: + this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + break; + case AbstractType::UnitType: + case AbstractType::AircraftType: + this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Unit; + break; + case AbstractType::BuildingType: + this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Building; + break; + } + } + + // `CrushableLevel` is not set, we take a default value for `DeployedCrushableLevel` based on `DeployedCrushable` and `OmniCrushResistant`. + // To avoid complexity of considering all possible combos, we ignore explicit `DeployedCrushableLevel` if `CrushableLevel` is not set. + if (auto pInfantryType = static_cast(this->OwnerObject())) + { + // We only consider case 1, since in any other case, the infantry's + // default `DeployedCrushableLevel` will be equal to its default `CrushableLevel`. + // 1. `Crushable=true`, `DeployedCrushable=false`: uncrushable only when deployed; + // 2. `Crushable=false`, `DeployedCrushable=true`: uncrushable always. + if (pInfantryType->Crushable && !pInfantryType->DeployedCrushable) + { + if (pInfantryType->OmniCrushResistant) + this->DeployedCrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant; + else + this->DeployedCrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + } + else + { + this->DeployedCrushableLevel = this->CrushableLevel; + } } } +} - return 0; +// This function is called upon a potential crusher's perspective and returns the crusher level of it. +int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) const +{ + return this->CrusherLevel.Get(0); +} + +// This function is called upon a potential crushing victim's perspective and returns the crushable level of it. +int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) const +{ + if (auto pVictimInfantry = static_cast(pVictim)) + { + if (pVictimInfantry->IsDeployed()) + return this->DeployedCrushableLevel.Get(this->CrushableLevel.Get(0)); + } + return this->CrushableLevel.Get(0); } // Ares 0.A source diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 6679de8927..ba00ea1932 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -473,8 +473,9 @@ class TechnoTypeExt void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0); - int GetCrusherLevel(FootClass* pCrusher); - int GetCrushableLevel(FootClass* pVictim); + void InitCrusherLevel(); + int GetCrusherLevel(FootClass* pCrusher) const; + int GetCrushableLevel(FootClass* pVictim) const; // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/Unit/Hooks.Crushing.cpp b/src/Ext/Unit/Hooks.Crushing.cpp index 3b6e935d7c..6a14857157 100644 --- a/src/Ext/Unit/Hooks.Crushing.cpp +++ b/src/Ext/Unit/Hooks.Crushing.cpp @@ -108,21 +108,23 @@ DEFINE_HOOK(0x5F6CE0, FootClass_CanGetCrushed_Hook, 6) GET(FootClass* const, pCrusher, EDI); GET(FootClass* const, pVictim, ESI); - // An eligible crusher must be a unit with "Crusher=yes". - // An eligible victim must be either an infantry, a unit, an overlay, or a building with 1x1 foundation. - if (RulesExt::Global()->CrusherLevelEnabled && - pCrusher && pCrusher->WhatAmI() == AbstractType::Unit && pCrusher->GetTechnoType()->Crusher && - pVictim && (pVictim->WhatAmI() == AbstractType::Infantry || - pVictim->WhatAmI() == AbstractType::Unit || - pVictim->WhatAmI() == AbstractType::Overlay || - (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building && - abstract_cast(pVictim->GetTechnoType())->Foundation == Foundation::_1x1))) + if (RulesExt::Global()->CrusherLevelEnabled) { - auto pCrusherExt = TechnoTypeExt::ExtMap.Find(pCrusher->GetTechnoType()); - auto pVictimExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); - int crusherLevel = pCrusherExt->GetCrusherLevel(pCrusher); - int crushableLevel = pVictimExt->GetCrushableLevel(pVictim); - return crusherLevel > crushableLevel ? CanCrush : CannotCrush; + // An eligible crusher must be a unit with "Crusher=yes". + // An eligible victim must be either an infantry, a unit, or a building with 1x1 foundation. + // Otherwise, fallback to unmodded behavior. + if (pCrusher && pCrusher->WhatAmI() == AbstractType::Unit && pCrusher->GetTechnoType()->Crusher && + pVictim && (pVictim->WhatAmI() == AbstractType::Infantry || + pVictim->WhatAmI() == AbstractType::Unit || + (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building && + abstract_cast(pVictim->GetTechnoType())->Foundation == Foundation::_1x1))) + { + auto pCrusherExt = TechnoTypeExt::ExtMap.Find(pCrusher->GetTechnoType()); + auto pVictimExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); + int crusherLevel = pCrusherExt->GetCrusherLevel(pCrusher); + int crushableLevel = pVictimExt->GetCrushableLevel(pVictim); + return crusherLevel > crushableLevel ? CanCrush : CannotCrush; + } } return 0; From f9746adfe8269bb7073be9cde0564e1b1a973326 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Wed, 1 Jan 2025 00:25:21 +0800 Subject: [PATCH 3/9] update credits --- CREDITS.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 81eca29d79..83a9f1c975 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -384,7 +384,9 @@ This page lists all the individual contributions to the project by their author. - Skirmish AI "gather when MCV deploy" behavior dehardcode - Global value of `RepairBaseNodes` - **tyuah8** - Drive/Jumpjet/Ship/Teleport locomotor did not power on when it is un-piggybacked bugfix -- **Aephiex** - initial fix for Ares academy not working on the initial payloads of vehicles built from a war factory +- **Aephiex** + - crusher level system + - initial fix for Ares academy not working on the initial payloads of vehicles built from a war factory - **Ares developers** - YRpp and Syringe which are used, save/load, project foundation and generally useful code from Ares - unfinished RadTypes code @@ -413,5 +415,4 @@ This page lists all the individual contributions to the project by their author. - **Phobos CN Tester Group (Reedom, Mantis, Swim Wing, Takitoru, Examon, AKB, Pusheen, ZQ, Claptrap, BunkerGeneral, Big J, Skywalker, ChickEmperor, Shifty, Mikain, Tobiichi Origami, Feiron, W_S502, Ailink, AbrahamMikhail, Tide, Fnfalsc, Yumeri_Rei, Nacho, Zhuzi, Ika_Aru)** - extensive and thorough testing - **Damfoos** - extensive and thorough testing - **Dmitry Volkov** - extensive and thorough testing -- **Rise of the East community** - extensive playtesting of in-dev features -- **Aephiex** - crusher level system \ No newline at end of file +- **Rise of the East community** - extensive playtesting of in-dev features \ No newline at end of file From 4868beb5bc56b2336b6018ba89d5d778a43dca33 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Wed, 1 Jan 2025 00:51:43 +0800 Subject: [PATCH 4/9] Update New-or-Enhanced-Logics.md --- docs/New-or-Enhanced-Logics.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 8e41b5ba04..cdc6fa9a5e 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1399,9 +1399,9 @@ Convert.ComputerToHuman = ; TechnoType - Other usage notes: - A unit must has a locomotor that supports crushing before it can crush something. Most naval units don't, except for the amphibious transports. - If a vehicle has a positive `CrusherLevel`, since it is presumably intended to be a crusher, it will be silently assigned with `Crusher=yes`, otherwise the game doesn't even try to check if it can crush something. - - Existing `Crusher=yes` entry isn't removed even if it had a `CrusherLevel` <= 0. + - Existing `Crusher=yes` entry isn't removed even if it had a `CrusherLevel` <= 0, however it doesn't help this unit to crush something with `CrushableLevel=0`. - If a vehicle has a `CrusherLevel` >= `[General]►CrushableLevel.Defaults.Uncrushable.Building`, since it is presumably intended to be able to crush walls, it will be silently assigned with `OmniCrusher=yes`, otherwise it won't be able to crush walls, unlike those explicitly given with `OmniCrusher=yes`. - - Existing `OmniCrusher=yes` entry isn't removed even if it had a `CrusherLevel` lower than that. + - Existing `OmniCrusher=yes` entry isn't removed even if it had a `CrusherLevel` lower than that, however it doesn't help this unit to crush something with a `CrushableLevel` equal to or greater than its `CrusherLevel`. - If `CrusherLevel` is set, `OmniCrusher` is redundant and ignored. - If `CrushableLevel` is set, `Crushable`, `OmniCrushResistant`, and `DeployedCrushable` are redundant and ignored. Use `DeployedCrushableLevel` instead if you wish the infantry to have a different crushable level when deployed. - If `CrushableLevel` is not set, `DeployedCrushableLevel` does not apply. From 3ddd5ed559568594bff29faaf9b6e85739bf3cab Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Wed, 1 Jan 2025 01:20:49 +0800 Subject: [PATCH 5/9] Update Body.cpp --- src/Ext/TechnoType/Body.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 24220c7f01..1c2c67028e 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -143,7 +143,7 @@ int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) const // This function is called upon a potential crushing victim's perspective and returns the crushable level of it. int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) const { - if (auto pVictimInfantry = static_cast(pVictim)) + if (auto pVictimInfantry = abstract_cast(pVictim)) { if (pVictimInfantry->IsDeployed()) return this->DeployedCrushableLevel.Get(this->CrushableLevel.Get(0)); From 740f60e6857a77f16b32e95960d748f6706079f3 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Wed, 1 Jan 2025 01:46:09 +0800 Subject: [PATCH 6/9] oops, crushing walls worked differently --- docs/New-or-Enhanced-Logics.md | 14 +++++++------- src/Ext/TechnoType/Body.cpp | 29 +++-------------------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index cdc6fa9a5e..25b546d482 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1397,18 +1397,18 @@ Convert.ComputerToHuman = ; TechnoType - At 2/2, a vehicle can crush Tesla Troopers and deployed Guardian G.I.s, but it can't crush IFVs and is still crushable by Battle Fortresses, just like a [Scorpion Tank](https://cnc-central.fandom.com/wiki/Scorpion_tank_(Tiberium_Wars)) does with the Dozer blades upgrade. - At 4/4, a vehicle can crush almost anything else, even Battle Fortresses, just like a [MARV](https://cnc-central.fandom.com/wiki/Mammoth_Armored_Reclamation_Vehicle) does. - Other usage notes: - - A unit must has a locomotor that supports crushing before it can crush something. Most naval units don't, except for the amphibious transports. - - If a vehicle has a positive `CrusherLevel`, since it is presumably intended to be a crusher, it will be silently assigned with `Crusher=yes`, otherwise the game doesn't even try to check if it can crush something. - - Existing `Crusher=yes` entry isn't removed even if it had a `CrusherLevel` <= 0, however it doesn't help this unit to crush something with `CrushableLevel=0`. - - If a vehicle has a `CrusherLevel` >= `[General]►CrushableLevel.Defaults.Uncrushable.Building`, since it is presumably intended to be able to crush walls, it will be silently assigned with `OmniCrusher=yes`, otherwise it won't be able to crush walls, unlike those explicitly given with `OmniCrusher=yes`. - - Existing `OmniCrusher=yes` entry isn't removed even if it had a `CrusherLevel` lower than that, however it doesn't help this unit to crush something with a `CrushableLevel` equal to or greater than its `CrusherLevel`. + - A vehicle must has a locomotor that supports crushing before it can crush something. Most naval units don't, except for the amphibious transports. + - A vehicle must have `Crusher=yes` before it can crush something, meaning a positive `CrusherLevel` requires `Crusher=yes` to function. + - Infantry types can never crush anything even if it had `Crusher=yes` and a tank's locomotor and movement zone. - If `CrusherLevel` is set, `OmniCrusher` is redundant and ignored. - If `CrushableLevel` is set, `Crushable`, `OmniCrushResistant`, and `DeployedCrushable` are redundant and ignored. Use `DeployedCrushableLevel` instead if you wish the infantry to have a different crushable level when deployed. - If `CrushableLevel` is not set, `DeployedCrushableLevel` does not apply. -- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` by default and they are hard-coded to be immune to `OmniCrusher`, meaning they can't be crushed by normal means. Thus, the crusher level system does not apply to buildings by default, and it must be turned on by `[General]►CrusherLevelEnabled.For1x1Buildings=true`. If not turned on, crushing buildings fallbacks to unmodded behavior. +- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` by default and they are hard-coded to be immune to `OmniCrusher`, meaning they can't be crushed by normal means. Thus, the crusher level system does not apply to buildings by default, and it must be turned on by `[General]►CrusherLevelEnabled.For1x1Buildings=true`. If not turned on, crushing buildings follow unmodded behavior. - Crushing a building triggers no animation or sound effect, and the building is silently removed from the map. - Crushing a building may cause unexpected behavior of the game, such as crushing all Bridge Repair Huts can make a bridge irrepairable. -- The crusher level system does not apply to walls. Crushing walls fallbacks to unmodded behavior. +- The crusher level system does not apply to walls. Crushing walls follow unmodded behavior. + - `Crusher=yes`, `MovementZone=(Destroyer|AmphibiousDestroyer)` can crush overlays with `Wall=yes`, `Crushable=yes` (Sandbags). + - `Crusher=yes`, `MovementZone=CrusherAll` can crush overlays with `Wall=yes`, `Crushable=no` (Fortress Walls). In `rulesmd.ini` ```ini diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 1c2c67028e..40d20d67ba 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -39,8 +39,8 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) void TechnoTypeExt::ExtData::InitCrusherLevel() { // Crusher related. - // Only UnitType can crush something, so we don't bother to do this for anything else. - if (this->OwnerObject()->WhatAmI() == AbstractType::UnitType) + // Only UnitType with "Crusher=yes" can crush something, so we don't bother to do this for anything else. + if (this->OwnerObject()->WhatAmI() == AbstractType::UnitType && this->OwnerObject()->Crusher) { if (!this->CrusherLevel.isset()) { @@ -57,27 +57,6 @@ void TechnoTypeExt::ExtData::InitCrusherLevel() this->CrusherLevel = 0; } } - else - { - // `CrusherLevel` is set, we make sure `Crusher` and `OmniCrusher` are present when needed, to make .ini coding easier. - // If crusher level is greater than zero, then make the techno type itself a crusher. - // If we don't, a vehicle type that is not a crusher can never crush anything, even if it had a sky high crusher level. - // The purpose is to make `Crusher=true` unnecessary for a vehicle that is clearly intended to be a crusher. - // This doesn't remove existing `Crusher=true` if the vehicle had a zero or negative crusher level. - if (!this->OwnerObject()->Crusher && this->CrusherLevel > 0) - { - this->OwnerObject()->Crusher = true; - } - - // If crusher level is equal to or greater than the default value for `OmniCrusher`, then make the techno itself an omni crusher. - // Otherwise, it can't crush walls that resist normal crusher, if only a high crusher level is given but `OmniCrusher=true` is not. - // The purpose is to make `OmniCrusher=true` unnecessary for a vehicle that is clearly intended to be an omni crusher. - // This doesn't remove existing `OmniCrusher=true` if the vehicle's crusher level is lower than the default value for `OmniCrusher`. - if (!this->OwnerObject()->OmniCrusher && this->CrusherLevel >= RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher) - { - this->OwnerObject()->OmniCrusher = true; - } - } } // Crushable related. @@ -112,7 +91,7 @@ void TechnoTypeExt::ExtData::InitCrusherLevel() } // `CrushableLevel` is not set, we take a default value for `DeployedCrushableLevel` based on `DeployedCrushable` and `OmniCrushResistant`. - // To avoid complexity of considering all possible combos, we ignore explicit `DeployedCrushableLevel` if `CrushableLevel` is not set. + // To avoid complexity of all possible combos, we ignore explicit `DeployedCrushableLevel` if `CrushableLevel` is not set. if (auto pInfantryType = static_cast(this->OwnerObject())) { // We only consider case 1, since in any other case, the infantry's @@ -134,13 +113,11 @@ void TechnoTypeExt::ExtData::InitCrusherLevel() } } -// This function is called upon a potential crusher's perspective and returns the crusher level of it. int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) const { return this->CrusherLevel.Get(0); } -// This function is called upon a potential crushing victim's perspective and returns the crushable level of it. int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) const { if (auto pVictimInfantry = abstract_cast(pVictim)) From e343827bc0446707599d9a68b2e4368aff7ebdde Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Wed, 1 Jan 2025 18:10:08 +0800 Subject: [PATCH 7/9] Update Whats-New.md --- docs/Whats-New.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 2815650ed9..74a68ac798 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -15,7 +15,7 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] - Translucent RLE SHPs will now be drawn using a more precise and performant algorithm that has no green tint and banding. Can be disabled with `rulesmd.ini->[General]->FixTransparencyBlitters=no`. - Iron Curtain status is now preserved by default when converting between TechnoTypes via `DeploysInto`/`UndeploysInto`. This behavior can be turned off per-TechnoType and global basis using `[SOMETECHNOTYPE]/[CombatDamage]->IronCurtain.KeptOnDeploy=no`. - The obsolete `[General]` -> `WarpIn` has been enabled for the default anim type when technos are warping in. If you want to restore the vanilla behavior, use the same anim type as `WarpOut`. -- Vehicles with `Crusher=true` + `OmniCrusher=true` / `MovementZone=CrusherAll` were hardcoded to tilt when crushing vehicles / walls respectively. This now obeys `TiltsWhenCrushes` but can be customized individually for these two scenarios using `TiltsWhenCrusher.Vehicles` and `TiltsWhenCrusher.Overlays`, which both default to `TiltsWhenCrushes`. +- Vehicles with `Crusher=true` + `OmniCrusher=true` / `MovementZone=CrusherAll` were hardcoded to tilt when crushing vehicles / walls respectively. This now obeys `TiltsWhenCrushes` but can be customized individually for these two scenarios using `TiltsWhenCrushes.Vehicles` and `TiltsWhenCrushes.Overlays`, which both default to `TiltsWhenCrushes`. ### From older Phobos versions From 53d95fa91f55d55cb18cbc7a9364dc3b89b01fb6 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Sun, 12 Jan 2025 21:50:54 +0800 Subject: [PATCH 8/9] minor revamp --- src/Ext/Techno/Body.cpp | 16 ++++++++++++++++ src/Ext/Techno/Body.h | 3 +++ src/Ext/TechnoType/Body.cpp | 15 --------------- src/Ext/TechnoType/Body.h | 2 -- src/Ext/Unit/Hooks.Crushing.cpp | 14 +++++++------- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 0d5c6ccf02..1739954aee 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -466,6 +466,22 @@ int TechnoExt::ExtData::GetAttachedEffectCumulativeCount(AttachEffectTypeClass* return foundCount; } +int TechnoExt::ExtData::GetCrusherLevel() const +{ + return this->TypeExtData->CrusherLevel.Get(0); +} + +int TechnoExt::ExtData::GetCrushableLevel() const +{ + if (auto pVictimInfantry = abstract_cast(this->OwnerObject())) + { + // The YR function to check if this infantry is deployed. + if (reinterpret_cast(0x5228D0)(pVictimInfantry)) + return this->TypeExtData->DeployedCrushableLevel.Get(this->TypeExtData->CrushableLevel.Get(0)); + } + return this->TypeExtData->CrushableLevel.Get(0); +} + // ============================= // load / save diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index dd958380bd..478a8223cf 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -116,6 +116,9 @@ class TechnoExt bool HasAttachedEffects(std::vector attachEffectTypes, bool requireAll, bool ignoreSameSource, TechnoClass* pInvoker, AbstractClass* pSource, std::vector const* minCounts, std::vector const* maxCounts) const; int GetAttachedEffectCumulativeCount(AttachEffectTypeClass* pAttachEffectType, bool ignoreSameSource = false, TechnoClass* pInvoker = nullptr, AbstractClass* pSource = nullptr) const; + int GetCrusherLevel() const; + int GetCrushableLevel() const; + virtual ~ExtData() override; virtual void InvalidatePointer(void* ptr, bool bRemoved) override { } virtual void LoadFromStream(PhobosStreamReader& Stm) override; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 40d20d67ba..fb21cc877f 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -113,21 +113,6 @@ void TechnoTypeExt::ExtData::InitCrusherLevel() } } -int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) const -{ - return this->CrusherLevel.Get(0); -} - -int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) const -{ - if (auto pVictimInfantry = abstract_cast(pVictim)) - { - if (pVictimInfantry->IsDeployed()) - return this->DeployedCrushableLevel.Get(this->CrushableLevel.Get(0)); - } - return this->CrushableLevel.Get(0); -} - // Ares 0.A source const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const { diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index ba00ea1932..ccd7b84520 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -474,8 +474,6 @@ class TechnoTypeExt void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0); void InitCrusherLevel(); - int GetCrusherLevel(FootClass* pCrusher) const; - int GetCrushableLevel(FootClass* pVictim) const; // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/Unit/Hooks.Crushing.cpp b/src/Ext/Unit/Hooks.Crushing.cpp index 6a14857157..d59de48e2f 100644 --- a/src/Ext/Unit/Hooks.Crushing.cpp +++ b/src/Ext/Unit/Hooks.Crushing.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -111,18 +112,17 @@ DEFINE_HOOK(0x5F6CE0, FootClass_CanGetCrushed_Hook, 6) if (RulesExt::Global()->CrusherLevelEnabled) { // An eligible crusher must be a unit with "Crusher=yes". - // An eligible victim must be either an infantry, a unit, or a building with 1x1 foundation. + // An eligible victim must be either an infantry, a unit, or a building (if crusher level is enabled for buildings). // Otherwise, fallback to unmodded behavior. if (pCrusher && pCrusher->WhatAmI() == AbstractType::Unit && pCrusher->GetTechnoType()->Crusher && pVictim && (pVictim->WhatAmI() == AbstractType::Infantry || pVictim->WhatAmI() == AbstractType::Unit || - (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building && - abstract_cast(pVictim->GetTechnoType())->Foundation == Foundation::_1x1))) + (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building))) { - auto pCrusherExt = TechnoTypeExt::ExtMap.Find(pCrusher->GetTechnoType()); - auto pVictimExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); - int crusherLevel = pCrusherExt->GetCrusherLevel(pCrusher); - int crushableLevel = pVictimExt->GetCrushableLevel(pVictim); + auto const pCrusherExt = TechnoExt::ExtMap.Find(pCrusher); + auto const pVictimExt = TechnoExt::ExtMap.Find(pVictim); + auto const crusherLevel = pCrusherExt->GetCrusherLevel(); + auto const crushableLevel = pVictimExt->GetCrushableLevel(); return crusherLevel > crushableLevel ? CanCrush : CannotCrush; } } From 0112dadd1cbd8b78607c55ed5086595b53b43336 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Mon, 13 Jan 2025 00:24:54 +0800 Subject: [PATCH 9/9] lazy load --- docs/New-or-Enhanced-Logics.md | 5 +- src/Ext/Rules/Body.cpp | 4 +- src/Ext/Rules/Body.h | 4 +- src/Ext/Techno/Body.cpp | 77 +++++++++++++++++++++++++++++-- src/Ext/TechnoType/Body.cpp | 81 --------------------------------- src/Ext/TechnoType/Body.h | 2 - src/Ext/Unit/Hooks.Crushing.cpp | 2 +- 7 files changed, 82 insertions(+), 93 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 25b546d482..95a95af688 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1403,9 +1403,10 @@ Convert.ComputerToHuman = ; TechnoType - If `CrusherLevel` is set, `OmniCrusher` is redundant and ignored. - If `CrushableLevel` is set, `Crushable`, `OmniCrushResistant`, and `DeployedCrushable` are redundant and ignored. Use `DeployedCrushableLevel` instead if you wish the infantry to have a different crushable level when deployed. - If `CrushableLevel` is not set, `DeployedCrushableLevel` does not apply. -- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` by default and they are hard-coded to be immune to `OmniCrusher`, meaning they can't be crushed by normal means. Thus, the crusher level system does not apply to buildings by default, and it must be turned on by `[General]►CrusherLevelEnabled.For1x1Buildings=true`. If not turned on, crushing buildings follow unmodded behavior. +- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` by default and they are hard-coded to be immune to `OmniCrusher`, meaning they can't be crushed by normal means. Thus, the crusher level system does not apply to buildings by default, and it must be turned on by `[General]►CrusherLevelEnabled.ForBuildings=true`. If not turned on, crushing buildings follow unmodded behavior. - Crushing a building triggers no animation or sound effect, and the building is silently removed from the map. - Crushing a building may cause unexpected behavior of the game, such as crushing all Bridge Repair Huts can make a bridge irrepairable. + - Even if this is turned on, crushing a building requires its central cell to be accessable. Some foundation sizes do not have a central cell. - The crusher level system does not apply to walls. Crushing walls follow unmodded behavior. - `Crusher=yes`, `MovementZone=(Destroyer|AmphibiousDestroyer)` can crush overlays with `Wall=yes`, `Crushable=yes` (Sandbags). - `Crusher=yes`, `MovementZone=CrusherAll` can crush overlays with `Wall=yes`, `Crushable=no` (Fortress Walls). @@ -1414,7 +1415,7 @@ In `rulesmd.ini` ```ini [General] CrusherLevelEnabled=false ; boolean -CrusherLevelEnabled.For1x1Buildings=false ; boolean +CrusherLevelEnabled.ForBuildings=false ; boolean CrusherLevel.Defaults.Crusher=1 ; integer CrusherLevel.Defaults.OmniCrusher=3 ; integer CrushableLevel.Defaults.Uncrushable.Infantry=1 ; integer diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index f92002f224..12fee345ab 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -210,7 +210,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->LightFlashAlphaImageDetailLevel.Read(exINI, GameStrings::AudioVisual, "LightFlashAlphaImageDetailLevel"); this->CrusherLevelEnabled.Read(exINI, GameStrings::General, "CrusherLevelEnabled"); - this->CrusherLevelEnabled_For1x1Buildings.Read(exINI, GameStrings::General, "CrusherLevelEnabled.For1x1Buildings"); + this->CrusherLevelEnabled_ForBuildings.Read(exINI, GameStrings::General, "CrusherLevelEnabled.ForBuildings"); this->CrusherLevel_Defaults_Crusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.Crusher"); this->CrusherLevel_Defaults_OmniCrusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.OmniCrusher"); this->CrushableLevel_Defaults_Uncrushable_Infantry.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Infantry"); @@ -402,7 +402,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->CombatLightDetailLevel) .Process(this->LightFlashAlphaImageDetailLevel) .Process(this->CrusherLevelEnabled) - .Process(this->CrusherLevelEnabled_For1x1Buildings) + .Process(this->CrusherLevelEnabled_ForBuildings) .Process(this->CrusherLevel_Defaults_Crusher) .Process(this->CrusherLevel_Defaults_OmniCrusher) .Process(this->CrushableLevel_Defaults_Uncrushable_Infantry) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index a901891f8a..04ac2bb95d 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -168,7 +168,7 @@ class RulesExt Valueable LightFlashAlphaImageDetailLevel; Valueable CrusherLevelEnabled; - Valueable CrusherLevelEnabled_For1x1Buildings; + Valueable CrusherLevelEnabled_ForBuildings; Valueable CrusherLevel_Defaults_Crusher; Valueable CrusherLevel_Defaults_OmniCrusher; Valueable CrushableLevel_Defaults_Uncrushable_Infantry; @@ -299,7 +299,7 @@ class RulesExt , LightFlashAlphaImageDetailLevel { 0 } , CrusherLevelEnabled { false } - , CrusherLevelEnabled_For1x1Buildings { false } + , CrusherLevelEnabled_ForBuildings { false } , CrusherLevel_Defaults_Crusher { 1 } , CrusherLevel_Defaults_OmniCrusher { 3 } , CrushableLevel_Defaults_Uncrushable_Infantry { 1 } diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 1739954aee..29f3eb70fa 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -468,15 +468,86 @@ int TechnoExt::ExtData::GetAttachedEffectCumulativeCount(AttachEffectTypeClass* int TechnoExt::ExtData::GetCrusherLevel() const { - return this->TypeExtData->CrusherLevel.Get(0); + auto const pTypeExt = this->TypeExtData; + if (!pTypeExt->CrusherLevel.isset()) + { + auto const pType = pTypeExt->OwnerObject(); + // `CrusherLevel` is not set, we take a default value based on `Crusher` and `OmniCrusher`. + if (pType->Crusher) + { + if (pType->OmniCrusher) + pTypeExt->CrusherLevel = RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher; + else + pTypeExt->CrusherLevel = RulesExt::Global()->CrusherLevel_Defaults_Crusher; + } + else + { + pTypeExt->CrusherLevel = 0; + } + } + + return pTypeExt->CrusherLevel.Get(); } int TechnoExt::ExtData::GetCrushableLevel() const { + auto const pTypeExt = this->TypeExtData; + if (!pTypeExt->CrushableLevel.isset()) + { + auto const pType = pTypeExt->OwnerObject(); + // `CrushableLevel` is not set, we take a default value based on `Crushable`, `OmniCrushResistant`, and the techno's type. + if (pType->Crushable) + { + pTypeExt->CrushableLevel = 0; + } + else if (pType->OmniCrushResistant) + { + pTypeExt->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant; + } + else + { + // The techno types are infantry types, unit types, aircraft types, and building types. + // Overlay types are not techno types. + switch (pType->WhatAmI()) + { + case AbstractType::InfantryType: + pTypeExt->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + break; + case AbstractType::UnitType: + case AbstractType::AircraftType: + pTypeExt->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Unit; + break; + case AbstractType::BuildingType: + pTypeExt->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Building; + break; + } + } + + // `CrushableLevel` is not set, we take a default value for `DeployedCrushableLevel` based on `DeployedCrushable` and `OmniCrushResistant`. + // To avoid complexity of all possible combos, we ignore explicit `DeployedCrushableLevel` if `CrushableLevel` is not set. + if (auto pInfantryType = abstract_cast(pType)) + { + // We only consider case 1, since in any other case, the infantry's + // default `DeployedCrushableLevel` will be equal to its default `CrushableLevel`. + // 1. `Crushable=true`, `DeployedCrushable=false`: uncrushable only when deployed; + // 2. `Crushable=false`, `DeployedCrushable=true`: uncrushable always. + if (pInfantryType->Crushable && !pInfantryType->DeployedCrushable) + { + if (pInfantryType->OmniCrushResistant) + pTypeExt->DeployedCrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant; + else + pTypeExt->DeployedCrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + } + else + { + pTypeExt->DeployedCrushableLevel = pTypeExt->CrushableLevel; + } + } + } + if (auto pVictimInfantry = abstract_cast(this->OwnerObject())) { - // The YR function to check if this infantry is deployed. - if (reinterpret_cast(0x5228D0)(pVictimInfantry)) + if (pVictimInfantry->IsDeployed()) return this->TypeExtData->DeployedCrushableLevel.Get(this->TypeExtData->CrushableLevel.Get(0)); } return this->TypeExtData->CrushableLevel.Get(0); diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index fb21cc877f..847541cbb3 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -19,9 +19,6 @@ TechnoTypeExt::ExtContainer TechnoTypeExt::ExtMap; void TechnoTypeExt::ExtData::Initialize() { this->ShieldType = ShieldTypeClass::FindOrAllocate(NONE_STR); - - if (RulesExt::Global()->CrusherLevelEnabled) - InitCrusherLevel(); } void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) @@ -35,84 +32,6 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) mtx->Translate(x, y, z); } -// Init the `CrusherLevel`, `CrushableLevel`, and `DeployedCrushableLevel` for a techno type. -void TechnoTypeExt::ExtData::InitCrusherLevel() -{ - // Crusher related. - // Only UnitType with "Crusher=yes" can crush something, so we don't bother to do this for anything else. - if (this->OwnerObject()->WhatAmI() == AbstractType::UnitType && this->OwnerObject()->Crusher) - { - if (!this->CrusherLevel.isset()) - { - // `CrusherLevel` is not set, we take a default value based on `Crusher` and `OmniCrusher`. - if (this->OwnerObject()->Crusher) - { - if (this->OwnerObject()->OmniCrusher) - this->CrusherLevel = RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher; - else - this->CrusherLevel = RulesExt::Global()->CrusherLevel_Defaults_Crusher; - } - else - { - this->CrusherLevel = 0; - } - } - } - - // Crushable related. - if (!this->CrushableLevel.isset()) - { - // `CrushableLevel` is not set, we take a default value based on `Crushable`, `OmniCrushResistant`, and the techno's type. - if (this->OwnerObject()->Crushable) - { - this->CrushableLevel = 0; - } - else if (this->OwnerObject()->OmniCrushResistant) - { - this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant; - } - else - { - // The techno types are infantry types, unit types, aircraft types, and building types. - // Overlay types are not techno types. - switch (this->OwnerObject()->WhatAmI()) - { - case AbstractType::InfantryType: - this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; - break; - case AbstractType::UnitType: - case AbstractType::AircraftType: - this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Unit; - break; - case AbstractType::BuildingType: - this->CrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Building; - break; - } - } - - // `CrushableLevel` is not set, we take a default value for `DeployedCrushableLevel` based on `DeployedCrushable` and `OmniCrushResistant`. - // To avoid complexity of all possible combos, we ignore explicit `DeployedCrushableLevel` if `CrushableLevel` is not set. - if (auto pInfantryType = static_cast(this->OwnerObject())) - { - // We only consider case 1, since in any other case, the infantry's - // default `DeployedCrushableLevel` will be equal to its default `CrushableLevel`. - // 1. `Crushable=true`, `DeployedCrushable=false`: uncrushable only when deployed; - // 2. `Crushable=false`, `DeployedCrushable=true`: uncrushable always. - if (pInfantryType->Crushable && !pInfantryType->DeployedCrushable) - { - if (pInfantryType->OmniCrushResistant) - this->DeployedCrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant; - else - this->DeployedCrushableLevel = RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; - } - else - { - this->DeployedCrushableLevel = this->CrushableLevel; - } - } - } -} - // Ares 0.A source const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const { diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index ccd7b84520..c842e51bd9 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -473,8 +473,6 @@ class TechnoTypeExt void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0); - void InitCrusherLevel(); - // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/Unit/Hooks.Crushing.cpp b/src/Ext/Unit/Hooks.Crushing.cpp index d59de48e2f..dc6ef0be93 100644 --- a/src/Ext/Unit/Hooks.Crushing.cpp +++ b/src/Ext/Unit/Hooks.Crushing.cpp @@ -117,7 +117,7 @@ DEFINE_HOOK(0x5F6CE0, FootClass_CanGetCrushed_Hook, 6) if (pCrusher && pCrusher->WhatAmI() == AbstractType::Unit && pCrusher->GetTechnoType()->Crusher && pVictim && (pVictim->WhatAmI() == AbstractType::Infantry || pVictim->WhatAmI() == AbstractType::Unit || - (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building))) + (RulesExt::Global()->CrusherLevelEnabled_ForBuildings && pVictim->WhatAmI() == AbstractType::Building))) { auto const pCrusherExt = TechnoExt::ExtMap.Find(pCrusher); auto const pVictimExt = TechnoExt::ExtMap.Find(pVictim);