From e9b1d44eaafc0ce513961084413c2954699bf43e Mon Sep 17 00:00:00 2001 From: Starkku Date: Sun, 12 Apr 2026 15:56:54 +0300 Subject: [PATCH 1/3] Building turret animations --- CREDITS.md | 1 + docs/Fixed-or-Improved-Logics.md | 15 +++++++ docs/Whats-New.md | 1 + src/Ext/Building/Body.cpp | 70 ++++++++++++++++++++++++++++++++ src/Ext/Building/Body.h | 5 +++ src/Ext/Building/Hooks.cpp | 42 +++++++++++++++++++ src/Ext/BuildingType/Body.cpp | 11 +++++ src/Ext/BuildingType/Body.h | 9 ++++ 8 files changed, 154 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index 170bc75fae..a1a27fba38 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -292,6 +292,7 @@ This page lists all the individual contributions to the project by their author. - Fix vehicles disguised as trees incorrectly displaying veterancy insignia when they shouldn't - GapGen + SpySat desync fix - Frame CRC generation rewrite + - Building turret idle/firing/low power animations - **Morton (MortonPL)**: - `XDrawOffset` for animations - Shield passthrough & absorption diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 61cd727f28..831e1e5984 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1008,6 +1008,21 @@ In `rulesmd.ini`: ConsideredVehicle= ; boolean ``` +### Building turret animations + +- By default building `TurretAnim(Damaged)` with `TurretAnimIsVoxel=false` only displays one frame per each of the 32 facings. This can now be increased and there are additional animations available for low power state and firing weapons. + - The frames in the .shp file should be in the order: `IdleFrames`, `LowPowerIdleFrames`, `FiringFrames`, `LowPowerFiringFrames`, animations with frame count set to 0 will be skipped / ignored. + - Note that `FiringFrames` starts playing when attacking and weapon can fire, it will not stop firing of weapon until it has finished playing nor will anything prevent it from looping multiple times if weapon firing is blocked by [delayed firing](New-or-Enhanced-Logics.md#delayed-firing) for longer than there are frames for. Matching delayed firing duration with firing frame count can be used to make pre-firing animation. + +In `rulesmd.ini`: +```ini +[SOMEBUILDING] ; BuildingType +TurretAnim.IdleFrames=1 ; integer +TurretAnim.LowPowerIdleFrames=0 ; integer +TurretAnim.FiringFrames=0 ; integer +TurretAnim.LowPowerFiringFrames=0 ; integer +``` + ### Custom exit cell for infantry factory - By default `Factory=InfantryType` buildings use exit cell for the created infantry based on hardcoded settings if any of `GDIBarracks`, `NODBarracks` or `YuriBarracks` are set to true. It is now possible to define arbitrary exit cell for such building via `BarracksExitCell`. Below is a reference of the cell offsets for the hardcoded values. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 24b3b1c159..be26179bae 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -576,6 +576,7 @@ New: - Customize `HarvesterLoadRate` (by Noble_Fish) - [Toggle to prevent `ShrapnelWeapon` from targeting buildings multiple times](Fixed-or-Improved-Logics.md#shrapnel-enhancements) (by Starkku) - [Laser drawing Z-adjust customization](Fixed-or-Improved-Logics.md#laser-z-adjust) (by Starkku) +- [Building turret idle/firing/low power animations](Fixed-or-Improved-Logics.md#building-turret-animations) (by Starkku) 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/Building/Body.cpp b/src/Ext/Building/Body.cpp index 0405c71501..752c319c73 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -455,6 +455,74 @@ void BuildingExt::KickOutClone(std::pair& info, v pClone->UnInit(); } +int BuildingExt::GetTurretFrame(BuildingClass* pThis) +{ + auto const pExt = BuildingExt::ExtMap.Find(pThis); + auto const pTypeExt = pExt->TypeExtData; + int facing = pThis->PrimaryFacing.Current().GetValue<5>(); + int shapeFacing = ObjectClass::BodyShape[facing]; + + bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); + bool isFiring = pExt->TurretAnimFiringFrame != -1; + + int idleBlockSize = 32 * pTypeExt->TurretAnim_IdleFrames; + int lowPowerIdleBlockSize = 32 * pTypeExt->TurretAnim_LowPowerIdleFrames; + int firingBlockSize = 32 * pTypeExt->TurretAnim_FiringFrames; + int offsetIdle = 0; + int offsetLowPowerIdle = offsetIdle + idleBlockSize; + int offsetFiring = offsetLowPowerIdle + lowPowerIdleBlockSize; + int offsetLowPowerFiring = offsetFiring + firingBlockSize; + + int framesPerFacing = pTypeExt->TurretAnim_IdleFrames; + int baseOffset = offsetIdle; + + if (isLowPower) + { + if (isFiring) + { + framesPerFacing = pTypeExt->TurretAnim_LowPowerFiringFrames; + baseOffset = offsetLowPowerFiring; + } + else if (pTypeExt->TurretAnim_LowPowerIdleFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_LowPowerIdleFrames; + baseOffset = offsetLowPowerIdle; + } + } + else + { + if (isFiring) + { + framesPerFacing = pTypeExt->TurretAnim_FiringFrames; + baseOffset = offsetFiring; + } + } + + int animFrame = 0; + + if (framesPerFacing > 1) + { + if (isFiring) + { + animFrame = pExt->TurretAnimFiringFrame; + pExt->TurretAnimFiringFrame++; + + if (pExt->TurretAnimFiringFrame >= framesPerFacing) + { + pExt->TurretAnimFiringFrame = -1; + pExt->TurretAnimIdleFrame = 0; // Reset idle anim frame. + } + } + else + { + animFrame = pExt->TurretAnimIdleFrame; + ++pExt->TurretAnimIdleFrame %= framesPerFacing; + } + } + + return baseOffset + (shapeFacing * framesPerFacing) + animFrame; +} + // ============================= // load / save @@ -474,6 +542,8 @@ void BuildingExt::ExtData::Serialize(T& Stm) .Process(this->CurrentLaserWeaponIndex) .Process(this->PoweredUpToLevel) .Process(this->CurrentEMPulseSW) + .Process(this->TurretAnimIdleFrame) + .Process(this->TurretAnimFiringFrame) //.Process(this->IsFiringNow) It is set and reset within a same function. ; } diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 215d284034..417810f641 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -27,6 +27,8 @@ class BuildingExt int PoweredUpToLevel; // Distinct from UpgradeLevel, and set to highest PowersUpToLevel out of applied upgrades regardless of how many are currently applied to this building. SuperClass* CurrentEMPulseSW; bool IsFiringNow; + int TurretAnimIdleFrame; + int TurretAnimFiringFrame; ExtData(BuildingClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } @@ -42,6 +44,8 @@ class BuildingExt , PoweredUpToLevel { 0 } , CurrentEMPulseSW {} , IsFiringNow { false } + , TurretAnimIdleFrame { 0 } + , TurretAnimFiringFrame { -1 } { } void DisplayIncomeString(); @@ -102,4 +106,5 @@ class BuildingExt static const std::vector GetFoundationCells(BuildingClass* pThis, CellStruct baseCoords, bool includeOccupyHeight = false); static WeaponStruct* GetLaserWeapon(BuildingClass* pThis); static void __fastcall KickOutClone(std::pair& info, void*, BuildingClass* pFactory); + static int GetTurretFrame(BuildingClass* pThis); }; diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 6f3b790408..c9bbd4cba0 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1087,3 +1087,45 @@ DEFINE_HOOK(0x45670D, BuildingClass_GetRadialIndicatorRange_Extras, 0x7) R->EAX(pThis->TechnoClass::GetTurretWeapon()); return ApplyTurretWeapon; } + +#pragma region TurretAnim + +DEFINE_HOOK(0x451242, BuildingClass_AnimationAI_TurretAnim, 0xA) +{ + enum { SkipGameCode = 0x451296 }; + + GET(BuildingClass*, pThis, ESI); + + if (auto const pAnim = pThis->Anims[(int)BuildingAnimSlot::Turret]) + { + pAnim->Animation.Value = BuildingExt::GetTurretFrame(pThis); + pAnim->Animation.Step = 0; + } + + return SkipGameCode; +} + +DEFINE_HOOK(0x44B6C7, BuildingClass_Mission_Attack_TurretAnim, 0x6) +{ + enum { SkipFiring = 0x44B6FE }; + + GET(BuildingClass*, pThis, ESI); + + if (pThis->HasTurret()) + { + if (auto const pAnim = pThis->Anims[(int)BuildingAnimSlot::Turret]) + { + auto const pExt = BuildingExt::ExtMap.Find(pThis); + auto const pTypeExt = pExt->TypeExtData; + bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); + bool firingFrames = isLowPower ? pTypeExt->TurretAnim_LowPowerFiringFrames : pTypeExt->TurretAnim_FiringFrames; + + if (firingFrames > 0 && pExt->TurretAnimFiringFrame == -1) + pExt->TurretAnimFiringFrame = 0; + } + } + + return 0; +} + +#pragma endregion diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 0df69a20b0..e9121c8c1b 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -139,6 +139,7 @@ int BuildingTypeExt::GetUpgradesAmount(BuildingTypeClass* pBuilding, HouseClass* return isUpgrade ? result : -1; } + void BuildingTypeExt::ExtData::Initialize() { } @@ -223,6 +224,12 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->UndeploysInto_Sellable.Read(exINI, pSection, "UndeploysInto.Sellable"); this->BuildingRadioLink_SyncOwner.Read(exINI, pSection, "BuildingRadioLink.SyncOwner"); + // Existing TurretAnim characteristics are read from rules so following the pattern here. + this->TurretAnim_IdleFrames.Read(exINI, pSection, "TurretAnim.IdleFrames"); + this->TurretAnim_LowPowerIdleFrames.Read(exINI, pSection, "TurretAnim.LowPowerIdleFrames"); + this->TurretAnim_FiringFrames.Read(exINI, pSection, "TurretAnim.FiringFrames"); + this->TurretAnim_LowPowerFiringFrames.Read(exINI, pSection, "TurretAnim.LowPowerFiringFrames"); + if (pThis->NumberOfDocks > 0) { std::optional empty; @@ -378,6 +385,10 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->HasPowerUpAnim) .Process(this->UndeploysInto_Sellable) .Process(this->BuildingRadioLink_SyncOwner) + .Process(this->TurretAnim_IdleFrames) + .Process(this->TurretAnim_LowPowerIdleFrames) + .Process(this->TurretAnim_FiringFrames) + .Process(this->TurretAnim_LowPowerFiringFrames) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 81fde368c3..6b7ea3d255 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -104,6 +104,11 @@ class BuildingTypeExt Nullable BuildingRadioLink_SyncOwner; + Valueable TurretAnim_IdleFrames; + Valueable TurretAnim_LowPowerIdleFrames; + Valueable TurretAnim_FiringFrames; + Valueable TurretAnim_LowPowerFiringFrames; + // Ares 0.2 Valueable CloningFacility; @@ -183,6 +188,10 @@ class BuildingTypeExt , HasPowerUpAnim {} , UndeploysInto_Sellable { false } , BuildingRadioLink_SyncOwner {} + , TurretAnim_IdleFrames { 1 } + , TurretAnim_LowPowerIdleFrames { 0 } + , TurretAnim_FiringFrames { 0 } + , TurretAnim_LowPowerFiringFrames { 0 } // Ares 0.2 , CloningFacility { false } From 9be886bb8baeebdf01879dc67eca418d59b366a8 Mon Sep 17 00:00:00 2001 From: Starkku Date: Mon, 20 Apr 2026 15:04:26 +0300 Subject: [PATCH 2/3] Add improvements - Fix issues with frame calcs by verifying frame counts - Add animation playback rate customization --- docs/Fixed-or-Improved-Logics.md | 3 ++ src/Ext/Building/Body.cpp | 48 ++++++++++++++++++++++---------- src/Ext/Building/Body.h | 2 ++ src/Ext/Building/Hooks.cpp | 3 ++ src/Ext/BuildingType/Body.cpp | 4 +++ src/Ext/BuildingType/Body.h | 4 +++ 6 files changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 831e1e5984..61c95287aa 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1013,6 +1013,7 @@ ConsideredVehicle= ; boolean - By default building `TurretAnim(Damaged)` with `TurretAnimIsVoxel=false` only displays one frame per each of the 32 facings. This can now be increased and there are additional animations available for low power state and firing weapons. - The frames in the .shp file should be in the order: `IdleFrames`, `LowPowerIdleFrames`, `FiringFrames`, `LowPowerFiringFrames`, animations with frame count set to 0 will be skipped / ignored. - Note that `FiringFrames` starts playing when attacking and weapon can fire, it will not stop firing of weapon until it has finished playing nor will anything prevent it from looping multiple times if weapon firing is blocked by [delayed firing](New-or-Enhanced-Logics.md#delayed-firing) for longer than there are frames for. Matching delayed firing duration with firing frame count can be used to make pre-firing animation. + - `TurretAnim.IdleRate` and `TurretAnim.FiringRate` can be used to customize animation frame playback rate for idle and firing frames respectively. In `rulesmd.ini`: ```ini @@ -1021,6 +1022,8 @@ TurretAnim.IdleFrames=1 ; integer TurretAnim.LowPowerIdleFrames=0 ; integer TurretAnim.FiringFrames=0 ; integer TurretAnim.LowPowerFiringFrames=0 ; integer +TurretAnim.IdleRate=1 ; integer, game frames +TurretAnim.FiringRate=1 ; integer, game frames ``` ### Custom exit cell for infantry factory diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 752c319c73..4e59e4c157 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -475,13 +475,15 @@ int BuildingExt::GetTurretFrame(BuildingClass* pThis) int framesPerFacing = pTypeExt->TurretAnim_IdleFrames; int baseOffset = offsetIdle; + bool hasFiringFrames = false; if (isLowPower) { - if (isFiring) + if (isFiring && pTypeExt->TurretAnim_LowPowerFiringFrames > 0) { framesPerFacing = pTypeExt->TurretAnim_LowPowerFiringFrames; baseOffset = offsetLowPowerFiring; + hasFiringFrames = true; } else if (pTypeExt->TurretAnim_LowPowerIdleFrames > 0) { @@ -491,32 +493,49 @@ int BuildingExt::GetTurretFrame(BuildingClass* pThis) } else { - if (isFiring) + if (isFiring && pTypeExt->TurretAnim_FiringFrames > 0) { framesPerFacing = pTypeExt->TurretAnim_FiringFrames; baseOffset = offsetFiring; + hasFiringFrames = true; } } int animFrame = 0; - if (framesPerFacing > 1) + if (isFiring && hasFiringFrames) { - if (isFiring) + animFrame = pExt->TurretAnimFiringFrame; + pExt->TurretAnimRateTick++; + + if (pExt->TurretAnimRateTick >= pTypeExt->TurretAnim_FiringRate) { - animFrame = pExt->TurretAnimFiringFrame; + pExt->TurretAnimRateTick = 0; pExt->TurretAnimFiringFrame++; + } - if (pExt->TurretAnimFiringFrame >= framesPerFacing) - { - pExt->TurretAnimFiringFrame = -1; - pExt->TurretAnimIdleFrame = 0; // Reset idle anim frame. - } + if (pExt->TurretAnimFiringFrame >= framesPerFacing) + { + pExt->TurretAnimFiringFrame = -1; + pExt->TurretAnimIdleFrame = 0; // Reset idle anim frame. + pExt->TurretAnimRateTick = 0; } - else + } + else if (framesPerFacing > 1) + { + animFrame = pExt->TurretAnimIdleFrame; + pExt->TurretAnimRateTick++; + + if (pExt->TurretAnimRateTick >= pTypeExt->TurretAnim_IdleRate) + { + pExt->TurretAnimRateTick = 0; + pExt->TurretAnimIdleFrame++; + } + + if (pExt->TurretAnimIdleFrame >= framesPerFacing) { - animFrame = pExt->TurretAnimIdleFrame; - ++pExt->TurretAnimIdleFrame %= framesPerFacing; + pExt->TurretAnimIdleFrame = 0; + pExt->TurretAnimRateTick = 0; } } @@ -544,6 +563,7 @@ void BuildingExt::ExtData::Serialize(T& Stm) .Process(this->CurrentEMPulseSW) .Process(this->TurretAnimIdleFrame) .Process(this->TurretAnimFiringFrame) + .Process(this->TurretAnimRateTick) //.Process(this->IsFiringNow) It is set and reset within a same function. ; } @@ -575,7 +595,7 @@ bool BuildingExt::SaveGlobals(PhobosStreamWriter& Stm) // ============================= // container -BuildingExt::ExtContainer::ExtContainer() : Container("BuildingClass") { } +BuildingExt::ExtContainer::ExtContainer() : Container("BuildingClass") {} BuildingExt::ExtContainer::~ExtContainer() = default; diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 417810f641..960675a9cd 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -29,6 +29,7 @@ class BuildingExt bool IsFiringNow; int TurretAnimIdleFrame; int TurretAnimFiringFrame; + int TurretAnimRateTick; ExtData(BuildingClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } @@ -46,6 +47,7 @@ class BuildingExt , IsFiringNow { false } , TurretAnimIdleFrame { 0 } , TurretAnimFiringFrame { -1 } + , TurretAnimRateTick { 0 } { } void DisplayIncomeString(); diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index c9bbd4cba0..dc105c7bfe 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1121,7 +1121,10 @@ DEFINE_HOOK(0x44B6C7, BuildingClass_Mission_Attack_TurretAnim, 0x6) bool firingFrames = isLowPower ? pTypeExt->TurretAnim_LowPowerFiringFrames : pTypeExt->TurretAnim_FiringFrames; if (firingFrames > 0 && pExt->TurretAnimFiringFrame == -1) + { pExt->TurretAnimFiringFrame = 0; + pExt->TurretAnimRateTick = 0; + } } } diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index e9121c8c1b..aa62b2871d 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -229,6 +229,8 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->TurretAnim_LowPowerIdleFrames.Read(exINI, pSection, "TurretAnim.LowPowerIdleFrames"); this->TurretAnim_FiringFrames.Read(exINI, pSection, "TurretAnim.FiringFrames"); this->TurretAnim_LowPowerFiringFrames.Read(exINI, pSection, "TurretAnim.LowPowerFiringFrames"); + this->TurretAnim_IdleRate.Read(exINI, pSection, "TurretAnim.IdleRate"); + this->TurretAnim_FiringRate.Read(exINI, pSection, "TurretAnim.FiringRate"); if (pThis->NumberOfDocks > 0) { @@ -389,6 +391,8 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->TurretAnim_LowPowerIdleFrames) .Process(this->TurretAnim_FiringFrames) .Process(this->TurretAnim_LowPowerFiringFrames) + .Process(this->TurretAnim_IdleRate) + .Process(this->TurretAnim_FiringFrames) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 6b7ea3d255..f988cca271 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -108,6 +108,8 @@ class BuildingTypeExt Valueable TurretAnim_LowPowerIdleFrames; Valueable TurretAnim_FiringFrames; Valueable TurretAnim_LowPowerFiringFrames; + Valueable TurretAnim_IdleRate; + Valueable TurretAnim_FiringRate; // Ares 0.2 Valueable CloningFacility; @@ -192,6 +194,8 @@ class BuildingTypeExt , TurretAnim_LowPowerIdleFrames { 0 } , TurretAnim_FiringFrames { 0 } , TurretAnim_LowPowerFiringFrames { 0 } + , TurretAnim_IdleRate { 1 } + , TurretAnim_FiringRate { 1 } // Ares 0.2 , CloningFacility { false } From 13df0a57aba05c30ca67d40948ab22484a46554f Mon Sep 17 00:00:00 2001 From: Starkku Date: Fri, 8 May 2026 19:38:12 +0300 Subject: [PATCH 3/3] Fix wrong variable type --- src/Ext/Building/Hooks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index dc105c7bfe..cb444d27b0 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1118,7 +1118,7 @@ DEFINE_HOOK(0x44B6C7, BuildingClass_Mission_Attack_TurretAnim, 0x6) auto const pExt = BuildingExt::ExtMap.Find(pThis); auto const pTypeExt = pExt->TypeExtData; bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); - bool firingFrames = isLowPower ? pTypeExt->TurretAnim_LowPowerFiringFrames : pTypeExt->TurretAnim_FiringFrames; + int firingFrames = isLowPower ? pTypeExt->TurretAnim_LowPowerFiringFrames : pTypeExt->TurretAnim_FiringFrames; if (firingFrames > 0 && pExt->TurretAnimFiringFrame == -1) {