diff --git a/CREDITS.md b/CREDITS.md index 735404cebd..2f1f7b87c0 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -383,6 +383,7 @@ This page lists all the individual contributions to the project by their author. - Separate the AirstrikeClass pointer between the attacker/aircraft and the target to avoid erroneous overwriting issues - Fix the bug that buildings will always be tinted as airstrike owner - Fix the bug that 'AllowAirstrike=no' cannot completely prevent air strikes from being launched against it + - Crush level system - **Apollo** - Translucent SHP drawing patches - **ststl**: - Customizable `ShowTimer` priority of superweapons diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index c5d62e0401..da9348ddfa 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -584,6 +584,39 @@ ProneSpeed.NoCrawls=1.5 ; floating point value, multiplier ProneSpeed= ; floating point value, multiplier, by default, use the corresponding global value according to Crawls ``` +## Unit + +### Crush level system + +- It's possible to customize crush level and crushable level for now. Rolling is only allowed when the CrushLevel of the compactor is greater than the CrushableLevel of the crushed object. + +In `rulesmd.ini` +```ini +[General] +CrusherLevel=5 ; integer +CrushableLevel=5 ; integer +OmniCrusherLevel=10 ; integer +OmniCrushResistantLevel=10 ; integer + +[WallModel] +WallCrushableLevel=10 ; integer + +[SOMEUNIT] ; Crusher +CrushLevel= ; integer +CrushLevel.Veteran= ; integer +CrushLevel.Elite= ; integer + +[SOMETECHNO] ; infantry, unit, aircraft +CrushableLevel= ; integer +CrushableLevel.Veteran= ; integer +CrushableLevel.Elite= ; integer + +[SOMEINFANTRY] +DeployedCrushableLevel= ; integer +DeployedCrushableLevel.Veteran= ; integer +DeployedCrushableLevel.Elite= ; integer +``` + ## Particle systems ### Fire particle target coordinate adjustment when firer rotates diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 5441ed6e80..7e3afbf797 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -367,6 +367,7 @@ New: - Customize airstrike targets (by NetsuNegi) - Aggressive attack move mission (by CrimRecya) - Amphibious access vehicle (by CrimRecya) +- Crush level system (by NetsuNegi) Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 1f471d3c47..e2a6e5d1b6 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -262,6 +262,11 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->DamagedSpeed.Read(exINI, GameStrings::General, "DamagedSpeed"); + this->CrusherLevel.Read(exINI, GameStrings::General, "CrusherLevel"); + this->CrushableLevel.Read(exINI, GameStrings::General, "CrushableLevel"); + this->OmniCrusherLevel.Read(exINI, GameStrings::General, "OmniCrusherLevel"); + this->OmniCrushResistantLevel.Read(exINI, GameStrings::General, "OmniCrushResistantLevel"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -482,6 +487,11 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->ProneSpeed_Crawls) .Process(this->ProneSpeed_NoCrawls) .Process(this->DamagedSpeed) + .Process(this->CrusherLevel) + .Process(this->CrushableLevel) + .Process(this->OmniCrusherLevel) + .Process(this->OmniCrushResistantLevel) + .Process(this->WallCrushableLevel) ; } @@ -665,3 +675,13 @@ DEFINE_HOOK(0x6744E4, RulesClass_ReadJumpjetControls_Extra, 0x7) // skip vanilla JumpjetControls and make it earlier load // DEFINE_JUMP(LJMP, 0x668EB5, 0x668EBD); // RulesClass_Process_SkipJumpjetControls // Really necessary? won't hurt to read again + +DEFINE_HOOK(0x66D242, RulesClass_ReadWallModel_CrushableLevel, 0x5) +{ + GET(CCINIClass*, pINI, EDI); + INI_EX exINI(pINI); + + RulesExt::Global()->WallCrushableLevel.Read(exINI, "WallModel", "WallCrushableLevel"); + + return 0; +} diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 79679a99ac..a714e79263 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -217,6 +217,12 @@ class RulesExt Valueable DamagedSpeed; + Valueable CrusherLevel; + Valueable CrushableLevel; + Valueable OmniCrusherLevel; + Valueable OmniCrushResistantLevel; + Valueable WallCrushableLevel; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , HarvesterDumpAmount { 0.0f } @@ -379,6 +385,12 @@ class RulesExt , ProneSpeed_NoCrawls { 1.5 } , DamagedSpeed { 0.75 } + + , CrusherLevel { 5 } + , CrushableLevel { 5 } + , OmniCrusherLevel { 10 } + , OmniCrushResistantLevel { 10 } + , WallCrushableLevel { 10 } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index a8588e978f..2933b4d522 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -431,6 +431,93 @@ bool TechnoExt::IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource) return false; } +int TechnoExt::GetCrushLevel(FootClass* pThis) +{ + const auto pType = pThis->GetTechnoType(); + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pType); + + switch (pThis->Veterancy.GetRemainingLevel()) + { + case Rank::Elite: + if (pTypeExt->CrushLevel.Elite >= 0) + return pTypeExt->CrushLevel.Elite; + + case Rank::Veteran: + if (pTypeExt->CrushLevel.Veteran >= 0) + return pTypeExt->CrushLevel.Veteran; + + default: + if (pTypeExt->CrushLevel.Rookie >= 0) + return pTypeExt->CrushLevel.Rookie; + } + + if (pType->OmniCrusher) + return RulesExt::Global()->OmniCrusherLevel; + + if (pType->Crusher || pThis->HasAbility(Ability::Crusher)) + return RulesExt::Global()->CrusherLevel; + + return 0; +} + +int TechnoExt::GetCrushableLevel(FootClass* pThis) +{ + const auto pType = pThis->GetTechnoType(); + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pType); + + const auto rank = pThis->Veterancy.GetRemainingLevel(); + const auto pInfantry = specific_cast(pThis); + + if (pInfantry && pInfantry->Uncrushable) + { + switch (rank) + { + case Rank::Elite: + if (pTypeExt->DeployedCrushableLevel.Elite >= 0) + return pTypeExt->DeployedCrushableLevel.Elite; + + case Rank::Veteran: + if (pTypeExt->DeployedCrushableLevel.Veteran >= 0) + return pTypeExt->DeployedCrushableLevel.Veteran; + + default: + if (pTypeExt->DeployedCrushableLevel.Rookie >= 0) + return pTypeExt->DeployedCrushableLevel.Rookie; + } + + if (pInfantry->Type->OmniCrushResistant) + return RulesExt::Global()->OmniCrushResistantLevel; + + if (!pInfantry->Type->DeployedCrushable) + return RulesExt::Global()->CrushableLevel; + } + else + { + switch (rank) + { + case Rank::Elite: + if (pTypeExt->CrushableLevel.Elite >= 0) + return pTypeExt->CrushableLevel.Elite; + + case Rank::Veteran: + if (pTypeExt->CrushableLevel.Veteran >= 0) + return pTypeExt->CrushableLevel.Veteran; + + default: + if (pTypeExt->CrushableLevel.Rookie >= 0) + return pTypeExt->CrushableLevel.Rookie; + } + + if (pType->OmniCrushResistant) + return RulesExt::Global()->OmniCrushResistantLevel; + + if (!pType->Crushable) + return RulesExt::Global()->CrushableLevel; + } + + return 0; +} + /// /// Gets whether or not techno has listed AttachEffect types active on it /// diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index c8ad3f5c4c..3009aeab4c 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -200,6 +200,8 @@ class TechnoExt static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); static bool CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue = false); static bool IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource); + static int GetCrushLevel(FootClass* pThis); + static int GetCrushableLevel(FootClass* pThis); static int GetTintColor(TechnoClass* pThis, bool invulnerability, bool airstrike, bool berserk); static int GetCustomTintColor(TechnoClass* pThis); static int GetCustomTintIntensity(TechnoClass* pThis); diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 0da13ea0c3..e8a195e7cd 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -534,6 +534,10 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->DamagedSpeed.Read(exINI, pSection, "DamagedSpeed"); this->ProneSpeed.Read(exINI, pSection, "ProneSpeed"); + this->CrushLevel.Read(exINI, pSection, "CrushLevel.%s"); + this->CrushableLevel.Read(exINI, pSection, "CrushableLevel.%s"); + this->DeployedCrushableLevel.Read(exINI, pSection, "DeployedCrushableLevel.%s"); + this->SuppressKillWeapons.Read(exINI, pSection, "SuppressKillWeapons"); this->SuppressKillWeapons_Types.Read(exINI, pSection, "SuppressKillWeapons.Types"); @@ -1002,6 +1006,10 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->DamagedSpeed) .Process(this->ProneSpeed) + .Process(this->CrushLevel) + .Process(this->CrushableLevel) + .Process(this->DeployedCrushableLevel) + .Process(this->SuppressKillWeapons) .Process(this->SuppressKillWeapons_Types) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 7b3b6d35a1..380c28e123 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -312,6 +312,10 @@ class TechnoTypeExt Nullable Promote_VeteranAnimation; Nullable Promote_EliteAnimation; + + Promotable CrushLevel; + Promotable CrushableLevel; + Promotable DeployedCrushableLevel; Nullable RadarInvisibleToHouse; @@ -627,6 +631,10 @@ class TechnoTypeExt , ProneSpeed { } , DamagedSpeed { } + , CrushLevel { -1 } + , CrushableLevel { -1 } + , DeployedCrushableLevel { -1 } + , SuppressKillWeapons { false } , SuppressKillWeapons_Types { } diff --git a/src/Ext/Unit/Hooks.Crushing.cpp b/src/Ext/Unit/Hooks.Crushing.cpp index 978a4f8bb7..9a3d68f53f 100644 --- a/src/Ext/Unit/Hooks.Crushing.cpp +++ b/src/Ext/Unit/Hooks.Crushing.cpp @@ -1,12 +1,123 @@ #include #include #include +#include -#include +#include #include #include -DEFINE_HOOK(0x073B05B, UnitClass_PerCellProcess_TiltWhenCrushes, 0x6) +#define Hook_IsCrusher(addr, name, mode, size, reg, retn, retn2) \ +DEFINE_HOOK(addr, ##name##_IsCrusher##mode##, size) \ +{ \ + GET(FootClass*, pThis, reg); \ + return TechnoExt::GetCrushLevel(pThis) > 0 ? retn : retn2; \ +} + +Hook_IsCrusher(0x4B3504, DriveLocomotionClass_PassableCheck, , 0x8, ECX, 0x4B3516, 0x4B3525) +Hook_IsCrusher(0x4B414F, DriveLocomotionClass_PassableCheck, 2, 0x8, ECX, 0x4B4161, 0x4B4179) +Hook_IsCrusher(0x5B098E, MechLocomotionClass_ProcessMoving, , 0x8, ECX, 0x5B09A0, 0x5B09AF) +Hook_IsCrusher(0x5B1034, MechLocomotionClass_ProcessMoving, 2, 0x8, ECX, 0x5B1054, 0x5B108A) +Hook_IsCrusher(0x5B1418, MechLocomotionClass_ProcessMoving, 3, 0x8, ECX, 0x5B1438, 0x5B146E) +Hook_IsCrusher(0x6A1044, ShipLocomotionClass_ProcessMoving, , 0x8, ECX, 0x6A1064, 0x6A10B9) +Hook_IsCrusher(0x6A2B53, ShipLocomotionClass_PassableCheck, , 0x8, ECX, 0x6A2B65, 0x6A2B74) +Hook_IsCrusher(0x6A377B, ShipLocomotionClass_PassableCheck, 2, 0x8, ECX, 0x6A378D, 0x6A37A5) +Hook_IsCrusher(0x73AFEB, UnitClass_PerCellProcess, , 0x6, EBP, 0x73B002, 0x73B074) +Hook_IsCrusher(0x73FC6C, UnitClass_IsCellOccupied, , 0x6, EBX, 0x73FD37, 0x73FC91) +Hook_IsCrusher(0x741733, UnitClass_CrushCell, , 0x6, EDI, 0x741754, 0x74195E) + +#undef Hook_IsCrusher + +DEFINE_JUMP(LJMP, 0x73FB2A, 0x73FB47)// Skip Crusher check before call ObjectClass::IsCrushable() + +DEFINE_JUMP(LJMP, 0x73FE5F, 0x73FE7C)// Skip Crusher check before call ObjectClass::IsCrushable() +DEFINE_JUMP(LJMP, 0x741529, 0x741546)// Skip Crusher check before call ObjectClass::IsCrushable() + +DEFINE_HOOK(0x741603, UnitClass_ApproachTarget_OmniCrusher, 0x6) +{ + enum { IsOmniCrusher = 0x741613, NotOmniCrusher = 0x741685 }; + + GET(UnitClass*, pThis, ESI); + + return TechnoExt::GetCrushLevel(pThis) >= RulesExt::Global()->OmniCrusherLevel ? IsOmniCrusher : NotOmniCrusher; +} + +DEFINE_JUMP(LJMP, 0x7438F7, 0x743918)// Skip Crusher check before call ObjectClass::IsCrushable() + +DEFINE_HOOK(0x73B013, UnitClass_PerCellProcess_CrusherWall, 0x6) +{ + enum { CanCrush = 0x73B036, CannotCrush = 0x73B074 }; + + GET(OverlayTypeClass*, pOverlay, ESI); + + // WW's code, ArrayIndex=0 is GASAND in default case, I guess it means sand bag is crushable + if (!pOverlay->ArrayIndex || pOverlay->Crushable) + return CanCrush; + + if (!pOverlay->Wall) + return CannotCrush; + + GET(UnitClass*, pThis, EBP); + + return pThis->Type->MovementZone == MovementZone::CrusherAll || TechnoExt::GetCrushLevel(pThis) > RulesExt::Global()->WallCrushableLevel ? CanCrush : CannotCrush; +} + +DEFINE_HOOK(0x73F42E, UnitClass_IsCellOccupied_CrushWall, 0x6) +{ + enum { CanCrush = 0x73F46E, CannotCrush = 0x73F483 }; + + GET(UnitClass*, pThis, EBX); + + return pThis->Type->MovementZone == MovementZone::CrusherAll || TechnoExt::GetCrushLevel(pThis) > RulesExt::Global()->WallCrushableLevel ? CanCrush : CannotCrush; +} + +DEFINE_HOOK(0x4B19B8, DriveLocomotionClass_ProcessMoving_CrushWall, 0x8) +{ + enum { CanCrush = 0x4B19E2, CannotCrush = 0x4B1A04 }; + + GET(FootClass*, pLinkedTo, ECX); + GET(OverlayTypeClass*, pOverlay, ESI); + + return pOverlay->Wall && TechnoExt::GetCrushLevel(pLinkedTo) > RulesExt::Global()->WallCrushableLevel ? CanCrush : CannotCrush; +} + +DEFINE_HOOK(0x4B1A1B, DriveLocomotionClass_ProcessMoving_CrusherAll, 0x8) +{ + enum { CanCrush = 0x4B1A2C, CannotCrush = 0x4B1A77 }; + + GET(FootClass*, pLinkedTo, ECX); + + return pLinkedTo->GetTechnoType()->MovementZone == MovementZone::CrusherAll || TechnoExt::GetCrushLevel(pLinkedTo) > RulesExt::Global()->OmniCrusherLevel ? CanCrush : CannotCrush; +} + +DEFINE_HOOK(0x5F6CD0, ObjectClass_IsCrushable, 0x6) +{ + enum { SkipGameCode = 0x5F6D90 }; + + GET(ObjectClass*, pThis, ECX); + GET_STACK(FootClass*, pCrusher, 0x4); + bool result = false; + + if (pThis && pCrusher && pThis != pCrusher) + { + if (pThis->AbstractFlags & AbstractFlags::Techno) + { + const auto pFoot = abstract_cast(pThis); + + if (pFoot && !pCrusher->Owner->IsAlliedWith(pFoot) && !pFoot->IsIronCurtained()) + result = TechnoExt::GetCrushLevel(pCrusher) > TechnoExt::GetCrushableLevel(pFoot); + } + else + { + result = pThis->GetType()->Crushable; + } + } + + R->AL(result); + return SkipGameCode; +} + +DEFINE_HOOK(0x73B05B, UnitClass_PerCellProcess_TiltWhenCrushes, 0x6) { enum { SkipGameCode = 0x73B074 }; @@ -22,7 +133,7 @@ DEFINE_HOOK(0x073B05B, UnitClass_PerCellProcess_TiltWhenCrushes, 0x6) return SkipGameCode; } -DEFINE_HOOK(0x0741941, UnitClass_OverrunSquare_TiltWhenCrushes, 0x6) +DEFINE_HOOK(0x741941, UnitClass_OverrunSquare_TiltWhenCrushes, 0x6) { enum { SkipGameCode = 0x74195E };