From cd0089cb65c9ed26ae41a1d4b1fb0889fde2a894 Mon Sep 17 00:00:00 2001 From: Starkku Date: Sun, 12 Apr 2026 15:56:54 +0300 Subject: [PATCH 1/2] 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 011372db13..64161b4618 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -291,6 +291,7 @@ This page lists all the individual contributions to the project by their author. - Wall overlay unit sell exploit fix - Fix vehicles disguised as trees incorrectly displaying veterancy insignia when they shouldn't - GapGen + SpySat desync fix + - 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 91a3fc34ff..b5b019fb6c 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1002,6 +1002,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 d0e3ab9d8c..6e53828c47 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -568,6 +568,7 @@ New: - [Technos with Walk locomotor spawn wake like ship](Fixed-or-Improved-Logics.md#customizable-wake-anim) (by TaranDahl) - [Hotkey for deselect object from current selection](User-Interface.md#deselect-object) (by FrozenFog) - [Updateable firing anim](Fixed-or-Improved-Logics.md#updateable-firing-anim) (by TaranDahl) +- [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 c7413b54e5d5a4f71ab21f2dc22efd9c51428d00 Mon Sep 17 00:00:00 2001 From: Starkku Date: Mon, 20 Apr 2026 15:04:26 +0300 Subject: [PATCH 2/2] 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 b5b019fb6c..3aeff559b1 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1007,6 +1007,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 @@ -1015,6 +1016,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 }