From f1dde2ae2db1b79b5d5b67687fb0ee6a7c9ccd77 Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Sat, 15 Aug 2026 23:04:16 +0800 Subject: [PATCH 1/7] initial --- CREDITS.md | 1 + docs/New-or-Enhanced-Logics.md | 21 ++++ docs/Whats-New.md | 1 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 3 + .../LC_MESSAGES/New-or-Enhanced-Logics.po | 16 +++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 7 ++ src/Ext/Building/Body.h | 2 + src/Ext/Building/Hooks.cpp | 110 +++++++++++++++++- src/Ext/BuildingType/Body.cpp | 23 ++++ src/Ext/BuildingType/Body.h | 24 ++++ src/Ext/BuildingType/Hooks.cpp | 5 +- 11 files changed, 206 insertions(+), 7 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 3cc738620b..4ee82ca973 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -741,6 +741,7 @@ This page lists all the individual contributions to the project by their author. - Disable AlphaImage during Buildup - Allow customizing the default value of `[Warhead] -> PreventScatter` via `[CombatDamage] -> Warhead.PreventScatter` - Allow `(Pre)ProductionAnim` animations to use `Powered` & `PoweredLight/Effect/Special` keys + - `RoofProductionAnim` - **Ollerus**: - Build limit group enhancement - Customizable rocker amplitude diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 161c1e238a..0392f752b4 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -741,6 +741,27 @@ PowerPlantEnhancer.Factor=1.0 ; floating point value PowerPlantEnhancer.MaxCount=-1 ; integer ``` +### Roof production anim + +- Now, you can use the `RoofProductionAnim*` series of flags to replace the `ProductionAnim*` series of flags when the produced infantry and vehicles are leaving the factory through the roof hatch. + - The criterion for exiting through the roof hatch is the same as `RoofDeployingAnim` in vanilla: the techno has `Jumpjet=yes` or `BalloonHover=yes`. + +In `artmd.ini`: +```ini +[SOMEBUILDING] ; BuildingType, with Factory=InfantryType or Factory=UnitType +RoofProductionAnim= ; AnimationType, falls back to ProductionAnim if unset +RoofProductionAnimDamaged= ; AnimationType, falls back to RoofProductionAnim if unset +RoofProductionAnimGarrisoned= ; AnimationType, falls back to RoofProductionAnim if unset +RoofProductionAnimX= ; integer, defaults to ProductionAnimX +RoofProductionAnimY= ; integer, defaults to ProductionAnimY +RoofProductionAnimYSort= ; integer, defaults to ProductionAnimYSort +RoofProductionAnimZAdjust= ; integer, defaults to ProductionAnimZAdjust +RoofProductionAnimPowered= ; boolean, defaults to ProductionAnimPowered +RoofProductionAnimPoweredLight= ; boolean, defaults to ProductionAnimPoweredLight +RoofProductionAnimPoweredEffect= ; boolean, defaults to ProductionAnimPoweredEffect +RoofProductionAnimPoweredSpecial= ; boolean, defaults to ProductionAnimPoweredSpecial +``` + ### Spy effects - Additional espionage bonuses can be toggled with `SpyEffect.Custom`. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 999282187d..b65ff5d762 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -667,6 +667,7 @@ HideShakeEffects=false ; boolean - [Reload speed adjustment on promotion](New-or-Enhanced-Logics.md#reload-speed-adjustment-on-promotion) (by Nuke) - Allowed customizing the default value of `[Warhead] -> PreventScatter` via `[CombatDamage] -> Warhead.PreventScatter` (by Noble_Fish) - Allowed `(Pre)ProductionAnim` animations to use `Powered` & `PoweredLight/Effect/Special` keys (by Noble_Fish) +- [`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish) #### 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/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 9586cc0636..362c9311d8 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -2525,6 +2525,9 @@ msgstr "" "允许 `(Pre)ProductionAnim` 动画使用 `Powered` 与 `PoweredLight/Effect/Special` " "类的标签" +msgid "`RoofProductionAnim`" +msgstr "`RoofProductionAnim`" + msgid "**Ollerus**:" msgstr "**Ollerus**:" diff --git a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po index 8a7798d813..7c69ceba8e 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po @@ -1868,6 +1868,22 @@ msgid "" "means no limitation." msgstr "`PowerPlantEnhancer.MaxCount` 决定了此类建筑中参与电厂增幅的数量上限。负值代表无上限。" +msgid "Roof production anim" +msgstr "天窗生产动画" + +msgid "" +"Now, you can use the `RoofProductionAnim*` series of flags to replace the `ProductionAnim*` series of flags when the produced infantry and vehicles are leaving the factory through the roof hatch." +msgstr "" +"现在,你可以用 `RoofProductionAnim*` 系列语句在所生产的步兵和载具从天窗离开工厂时替代 `ProductionAnim*` 系列语句使用。" + +msgid "" +"The criterion for exiting through the roof hatch is the same as " +"`RoofDeployingAnim` in vanilla: the unit has `Jumpjet=yes` or " +"`BalloonHover=yes`." +msgstr "" +"从天窗离开的判定标准与原版中 `RoofDeployingAnim` 相同:单位拥有 `Jumpjet=yes` 或 " +"`BalloonHover=yes`。" + msgid "Spy effects" msgstr "间谍效果" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index cbb86bb4f9..5880c724bc 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -2471,6 +2471,13 @@ msgstr "" "允许 `(Pre)ProductionAnim` 动画使用 `Powered` 与 `PoweredLight/Effect/Special` " "类的标签(by Noble_Fish)" +msgid "" +"[`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim) " +"(by Noble_Fish)" +msgstr "" +"[`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim)(by" +" Noble_Fish)" + msgid "Vanilla fixes:" msgstr "原版问题修复:" diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index a7a674c6c6..a756dab4f9 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -28,6 +28,7 @@ class BuildingExt final : public TechnoExt, public Detach::ListenerConstructionYard) return true; - if (pType->Factory == AbstractType::BuildingType && GeneralUtils::IsValidString(pType->BuildingAnim[(int)BuildingAnimSlot::Production].Anim)) + if (pType->Factory == AbstractType::BuildingType && GeneralUtils::IsValidString(pType->GetBuildingAnim(BuildingAnimSlot::Production).Anim)) return true; return false; } +static bool IsRoofExitTechno(TechnoTypeClass* pType) +{ + return pType->JumpJet || pType->BalloonHover; +} + +static bool IsRoofExitBuildingUnit(BuildingClass* pBuilding) +{ + auto pUnit = pBuilding->GetNthLink(); + return pUnit && IsRoofExitTechno(pUnit->GetTechnoType()); +} + +static AnimTypeClass* PickRoofProductionAnim(BuildingTypeExt* pTypeExt, bool isDamaged, bool garrisoned) +{ + if (garrisoned && pTypeExt->RoofProductionAnimGarrisoned.Get() != nullptr) + return pTypeExt->RoofProductionAnimGarrisoned.Get(); + + if (isDamaged && pTypeExt->RoofProductionAnimDamaged.Get() != nullptr) + return pTypeExt->RoofProductionAnimDamaged.Get(); + + return pTypeExt->RoofProductionAnim.Get(); +} + +static void PlayRoofProductionAnim(BuildingClass* pBuilding, BuildingTypeExt* pTypeExt, bool isDamaged, bool garrisoned) +{ + const char* animName = nullptr; + + if (auto pAnimType = PickRoofProductionAnim(pTypeExt, isDamaged, garrisoned)) + { + animName = pAnimType->get_ID(); + } + else + { + const auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + animName = garrisoned ? prodAnim.Garrisoned : (isDamaged ? prodAnim.Damaged : prodAnim.Anim); + } + + if (!GeneralUtils::IsValidString(animName)) + return; + + auto pExt = BuildingExt::Fetch(pBuilding); + pExt->IsPlayingRoofProductionAnim = true; + pBuilding->PlayAnim(animName, BuildingAnimSlot::Production, isDamaged, garrisoned, 0); + pExt->IsPlayingRoofProductionAnim = false; + + if (auto pAnim = pBuilding->GetAnim(BuildingAnimSlot::Production)) + { + const auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + + pAnim->ZAdjust = pTypeExt->RoofProductionAnimZAdjust.Get(prodAnim.ZAdjust); + pAnim->YSortAdjust = pTypeExt->RoofProductionAnimYSort.Get(prodAnim.YSort); + pAnim->Location.X = pBuilding->Location.X + pTypeExt->RoofProductionAnimX.Get(prodAnim.Position.X); + pAnim->Location.Y = pBuilding->Location.Y + pTypeExt->RoofProductionAnimY.Get(prodAnim.Position.Y); + } +} + DEFINE_HOOK(0x43CC73, BuildingClass_ReceiveMessage_ProductionAnim, 0x6) { enum { SkipGameCode = 0x43CC79 }; @@ -1052,6 +1107,44 @@ DEFINE_HOOK(0x44B7AE, BuildingClass_Mission_Repair_ProductionAnim, 0x6) return SkipGameCode; } +static bool TryPlayRoofProductionAnim(BuildingClass* pBuilding, bool isDamaged) +{ + if (!IsRoofExitBuildingUnit(pBuilding)) + return false; + + auto pTypeExt = BuildingTypeExt::Fetch(pBuilding->Type); + const bool garrisoned = pBuilding->GetOccupantCount() > 0; + + const auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + if (BuildingTypeExt::IsPoweredAnimBlocked(pBuilding, + pTypeExt->RoofProductionAnimPowered.Get(prodAnim.Powered), + pTypeExt->RoofProductionAnimPoweredLight.Get(prodAnim.PoweredLight), + pTypeExt->RoofProductionAnimPoweredEffect.Get(prodAnim.PoweredEffect), + pTypeExt->RoofProductionAnimPoweredSpecial.Get(prodAnim.PoweredSpecial))) + return true; + + PlayRoofProductionAnim(pBuilding, pTypeExt, isDamaged, garrisoned); + return true; +} + +DEFINE_HOOK(0x44DDF0, BuildingClass_Unload_RoofProductionAnim, 0x6) +{ + enum { SkipGameCode = 0x44E267 }; + + GET(BuildingClass*, pBuilding, EBP); + + return TryPlayRoofProductionAnim(pBuilding, false) ? SkipGameCode : 0; +} + +DEFINE_HOOK(0x44DDDE, BuildingClass_Unload_RoofProductionAnim_Damaged, 0x6) +{ + enum { SkipGameCode = 0x44E267 }; + + GET(BuildingClass*, pBuilding, EBP); + + return TryPlayRoofProductionAnim(pBuilding, true) ? SkipGameCode : 0; +} + DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, 0x6) { GET(BuildingClass*, pThis, ESI); @@ -1060,15 +1153,20 @@ DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, if (pType->Factory == AbstractType::InfantryType) { - bool isDamaged = false; - auto anim = pType->BuildingAnim[(int)BuildingAnimSlot::Production].Anim; + const bool isDamaged = pThis->GetHealthPercentage() <= RulesClass::Instance->ConditionYellow; - if (pThis->GetHealthPercentage() <= RulesClass::Instance->ConditionYellow) + if (IsRoofExitBuildingUnit(pThis)) { - isDamaged = true; - anim = pType->BuildingAnim[(int)BuildingAnimSlot::Production].Damaged; + pThis->DestroyNthAnim(BuildingAnimSlot::Idle); + PlayRoofProductionAnim(pThis, BuildingTypeExt::Fetch(pType), isDamaged, pThis->GetOccupantCount() > 0); + return 0; } + auto anim = pType->GetBuildingAnim(BuildingAnimSlot::Production).Anim; + + if (isDamaged) + anim = pType->GetBuildingAnim(BuildingAnimSlot::Production).Damaged; + if (GeneralUtils::IsValidString(anim)) { pThis->DestroyNthAnim(BuildingAnimSlot::Idle); diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index f371bb9636..83a11696f5 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -321,6 +321,18 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) prodAnim.PoweredEffect = pArtINI->ReadBool(pArtSection, "ProductionAnimPoweredEffect", prodAnim.PoweredEffect); prodAnim.PoweredSpecial = pArtINI->ReadBool(pArtSection, "ProductionAnimPoweredSpecial", prodAnim.PoweredSpecial); + this->RoofProductionAnim.Read(exArtINI, pArtSection, "RoofProductionAnim"); + this->RoofProductionAnimDamaged.Read(exArtINI, pArtSection, "RoofProductionAnimDamaged"); + this->RoofProductionAnimGarrisoned.Read(exArtINI, pArtSection, "RoofProductionAnimGarrisoned"); + this->RoofProductionAnimX.Read(exArtINI, pArtSection, "RoofProductionAnimX"); + this->RoofProductionAnimY.Read(exArtINI, pArtSection, "RoofProductionAnimY"); + this->RoofProductionAnimZAdjust.Read(exArtINI, pArtSection, "RoofProductionAnimZAdjust"); + this->RoofProductionAnimYSort.Read(exArtINI, pArtSection, "RoofProductionAnimYSort"); + this->RoofProductionAnimPowered.Read(exArtINI, pArtSection, "RoofProductionAnimPowered"); + this->RoofProductionAnimPoweredLight.Read(exArtINI, pArtSection, "RoofProductionAnimPoweredLight"); + this->RoofProductionAnimPoweredEffect.Read(exArtINI, pArtSection, "RoofProductionAnimPoweredEffect"); + this->RoofProductionAnimPoweredSpecial.Read(exArtINI, pArtSection, "RoofProductionAnimPoweredSpecial"); + // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); if (SuperWeaponTypeClass::Array.Count > 0) @@ -469,6 +481,17 @@ void BuildingTypeExt::Serialize(T& Stm) .Process(this->SetTabBySelecting) .Process(this->RevealToAll_Radius) .Process(this->DeployFireDelay) + .Process(this->RoofProductionAnim) + .Process(this->RoofProductionAnimDamaged) + .Process(this->RoofProductionAnimGarrisoned) + .Process(this->RoofProductionAnimX) + .Process(this->RoofProductionAnimY) + .Process(this->RoofProductionAnimZAdjust) + .Process(this->RoofProductionAnimYSort) + .Process(this->RoofProductionAnimPowered) + .Process(this->RoofProductionAnimPoweredLight) + .Process(this->RoofProductionAnimPoweredEffect) + .Process(this->RoofProductionAnimPoweredSpecial) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 26d994247c..e6dd6c92f6 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -129,6 +129,18 @@ class BuildingTypeExt final : public TechnoTypeExt Nullable DeployFireDelay; + Valueable RoofProductionAnim; + Valueable RoofProductionAnimDamaged; + Valueable RoofProductionAnimGarrisoned; + Nullable RoofProductionAnimX; + Nullable RoofProductionAnimY; + Nullable RoofProductionAnimZAdjust; + Nullable RoofProductionAnimYSort; + Nullable RoofProductionAnimPowered; + Nullable RoofProductionAnimPoweredLight; + Nullable RoofProductionAnimPoweredEffect; + Nullable RoofProductionAnimPoweredSpecial; + // Ares 0.2 Valueable CloningFacility; @@ -226,6 +238,18 @@ class BuildingTypeExt final : public TechnoTypeExt , RevealToAll_Radius {} , DeployFireDelay {} + , RoofProductionAnim { nullptr } + , RoofProductionAnimDamaged { nullptr } + , RoofProductionAnimGarrisoned { nullptr } + , RoofProductionAnimX {} + , RoofProductionAnimY {} + , RoofProductionAnimZAdjust {} + , RoofProductionAnimYSort {} + , RoofProductionAnimPowered { } + , RoofProductionAnimPoweredLight { } + , RoofProductionAnimPoweredEffect { } + , RoofProductionAnimPoweredSpecial { } + // Ares 0.2 , CloningFacility { false } diff --git a/src/Ext/BuildingType/Hooks.cpp b/src/Ext/BuildingType/Hooks.cpp index c79cb14363..7bdd5c6eee 100644 --- a/src/Ext/BuildingType/Hooks.cpp +++ b/src/Ext/BuildingType/Hooks.cpp @@ -434,7 +434,7 @@ DEFINE_HOOK(0x44E826, BuildingClass_GetPowerOutput_Enhancer, 0x6) const auto pOwner = pThis->Owner; auto [power, extraPower] = BuildingTypeExt::GetEnhancedPower(pThis->Type, R->EDI(), pOwner, pThis); - + if (pThis->UpgradeLevel) { for (const auto pUpgrade : pThis->Upgrades) @@ -533,6 +533,9 @@ static __forceinline bool AllowPoweredAnim(BuildingClass* pBuilding, BuildingAni } else if (anim == BuildingAnimSlot::Production || anim == BuildingAnimSlot::PreProduction) { + if (anim == BuildingAnimSlot::Production && BuildingExt::Fetch(pBuilding)->IsPlayingRoofProductionAnim) + return true; + auto const animData = pType->GetBuildingAnim(anim); if (BuildingTypeExt::IsPoweredAnimBlocked(pBuilding, animData.Powered, animData.PoweredLight, animData.PoweredEffect, animData.PoweredSpecial)) From 62c35f37c7616952c107f24d1a61742661185381 Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Sun, 16 Aug 2026 01:20:06 +0800 Subject: [PATCH 2/7] powered check --- 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 f6f288e053..7a257ead55 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1158,7 +1158,7 @@ DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, if (IsRoofExitBuildingUnit(pThis)) { pThis->DestroyNthAnim(BuildingAnimSlot::Idle); - PlayRoofProductionAnim(pThis, BuildingTypeExt::Fetch(pType), isDamaged, pThis->GetOccupantCount() > 0); + TryPlayRoofProductionAnim(pThis, isDamaged); return 0; } From 83d2331824808e691138cc8e458421d88378bc6f Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Sun, 16 Aug 2026 01:37:54 +0800 Subject: [PATCH 3/7] Keep roof production anim line independent from the plain one --- src/Ext/Building/Hooks.cpp | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 7a257ead55..3280f6991d 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1062,27 +1062,51 @@ static void PlayRoofProductionAnim(BuildingClass* pBuilding, BuildingTypeExt* pT } else { - const auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); - animName = garrisoned ? prodAnim.Garrisoned : (isDamaged ? prodAnim.Damaged : prodAnim.Anim); + // Fall back to the plain ProductionAnim only, so the roof line stays independent + // of the ProductionAnimDamaged/Garrisoned branches. + animName = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production).Anim; } if (!GeneralUtils::IsValidString(animName)) return; + // The roof anim needs its own placement values without affecting the plain line, + // so apply them only for the duration of this PlayAnim call. + auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + + const Point2D savedPosition = prodAnim.Position; + const int savedZAdjust = prodAnim.ZAdjust; + const int savedYSort = prodAnim.YSort; + + prodAnim.Position.X = pTypeExt->RoofProductionAnimX.Get(savedPosition.X); + prodAnim.Position.Y = pTypeExt->RoofProductionAnimY.Get(savedPosition.Y); + prodAnim.ZAdjust = pTypeExt->RoofProductionAnimZAdjust.Get(savedZAdjust); + prodAnim.YSort = pTypeExt->RoofProductionAnimYSort.Get(savedYSort); + auto pExt = BuildingExt::Fetch(pBuilding); pExt->IsPlayingRoofProductionAnim = true; pBuilding->PlayAnim(animName, BuildingAnimSlot::Production, isDamaged, garrisoned, 0); pExt->IsPlayingRoofProductionAnim = false; - if (auto pAnim = pBuilding->GetAnim(BuildingAnimSlot::Production)) - { - const auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + prodAnim.Position = savedPosition; + prodAnim.ZAdjust = savedZAdjust; + prodAnim.YSort = savedYSort; +} - pAnim->ZAdjust = pTypeExt->RoofProductionAnimZAdjust.Get(prodAnim.ZAdjust); - pAnim->YSortAdjust = pTypeExt->RoofProductionAnimYSort.Get(prodAnim.YSort); - pAnim->Location.X = pBuilding->Location.X + pTypeExt->RoofProductionAnimX.Get(prodAnim.Position.X); - pAnim->Location.Y = pBuilding->Location.Y + pTypeExt->RoofProductionAnimY.Get(prodAnim.Position.Y); - } +// the vanilla powered-hide would also apply to roof anims, whose power +// requirements are already enforced separately — skip it there so the +// plain line's powered setting cannot affect the roof line. +DEFINE_HOOK(0x451A44, BuildingClass_PlayAnim_SkipPoweredHideForRoof, 0x6) +{ + enum { SkipHide = 0x451A70 }; + + GET(BuildingClass*, pThis, ESI); + + if (BuildingExt::Fetch(pThis)->IsPlayingRoofProductionAnim) + return SkipHide; + + R->AL(pThis->IsPowerOnline()); + return 0; } DEFINE_HOOK(0x43CC73, BuildingClass_ReceiveMessage_ProductionAnim, 0x6) From b13f280ee82fda8a8d1378be87ccda10fd617d86 Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Sun, 16 Aug 2026 02:28:40 +0800 Subject: [PATCH 4/7] remove `BuildingClass_PlayAnim_SkipPoweredHideForRoof` hook --- src/Ext/Building/Hooks.cpp | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 3280f6991d..df0eb2cd08 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1070,18 +1070,29 @@ static void PlayRoofProductionAnim(BuildingClass* pBuilding, BuildingTypeExt* pT if (!GeneralUtils::IsValidString(animName)) return; - // The roof anim needs its own placement values without affecting the plain line, - // so apply them only for the duration of this PlayAnim call. + // The roof anim needs its own placement and power values without affecting + // the plain line, so apply them only for the duration of this PlayAnim + // call. Swapping the Powered family also makes the vanilla powered-hide + // inside PlayAnim (0x451A44) read the Roof*Powered values, keeping the two + // lines independent. auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); const Point2D savedPosition = prodAnim.Position; const int savedZAdjust = prodAnim.ZAdjust; const int savedYSort = prodAnim.YSort; + const bool savedPowered = prodAnim.Powered; + const bool savedPoweredLight = prodAnim.PoweredLight; + const bool savedPoweredEffect = prodAnim.PoweredEffect; + const bool savedPoweredSpecial = prodAnim.PoweredSpecial; prodAnim.Position.X = pTypeExt->RoofProductionAnimX.Get(savedPosition.X); prodAnim.Position.Y = pTypeExt->RoofProductionAnimY.Get(savedPosition.Y); prodAnim.ZAdjust = pTypeExt->RoofProductionAnimZAdjust.Get(savedZAdjust); prodAnim.YSort = pTypeExt->RoofProductionAnimYSort.Get(savedYSort); + prodAnim.Powered = pTypeExt->RoofProductionAnimPowered.Get(savedPowered); + prodAnim.PoweredLight = pTypeExt->RoofProductionAnimPoweredLight.Get(savedPoweredLight); + prodAnim.PoweredEffect = pTypeExt->RoofProductionAnimPoweredEffect.Get(savedPoweredEffect); + prodAnim.PoweredSpecial = pTypeExt->RoofProductionAnimPoweredSpecial.Get(savedPoweredSpecial); auto pExt = BuildingExt::Fetch(pBuilding); pExt->IsPlayingRoofProductionAnim = true; @@ -1091,22 +1102,10 @@ static void PlayRoofProductionAnim(BuildingClass* pBuilding, BuildingTypeExt* pT prodAnim.Position = savedPosition; prodAnim.ZAdjust = savedZAdjust; prodAnim.YSort = savedYSort; -} - -// the vanilla powered-hide would also apply to roof anims, whose power -// requirements are already enforced separately — skip it there so the -// plain line's powered setting cannot affect the roof line. -DEFINE_HOOK(0x451A44, BuildingClass_PlayAnim_SkipPoweredHideForRoof, 0x6) -{ - enum { SkipHide = 0x451A70 }; - - GET(BuildingClass*, pThis, ESI); - - if (BuildingExt::Fetch(pThis)->IsPlayingRoofProductionAnim) - return SkipHide; - - R->AL(pThis->IsPowerOnline()); - return 0; + prodAnim.Powered = savedPowered; + prodAnim.PoweredLight = savedPoweredLight; + prodAnim.PoweredEffect = savedPoweredEffect; + prodAnim.PoweredSpecial = savedPoweredSpecial; } DEFINE_HOOK(0x43CC73, BuildingClass_ReceiveMessage_ProductionAnim, 0x6) From 154e2c86d3eebfb398c1bbee2636a32878b20e1b Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Sun, 16 Aug 2026 02:34:11 +0800 Subject: [PATCH 5/7] Ensure infantry factory identification --- src/Ext/Building/Hooks.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index df0eb2cd08..9086b5c119 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1072,9 +1072,7 @@ static void PlayRoofProductionAnim(BuildingClass* pBuilding, BuildingTypeExt* pT // The roof anim needs its own placement and power values without affecting // the plain line, so apply them only for the duration of this PlayAnim - // call. Swapping the Powered family also makes the vanilla powered-hide - // inside PlayAnim (0x451A44) read the Roof*Powered values, keeping the two - // lines independent. + // call. auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); const Point2D savedPosition = prodAnim.Position; @@ -1130,9 +1128,10 @@ DEFINE_HOOK(0x44B7AE, BuildingClass_Mission_Repair_ProductionAnim, 0x6) return SkipGameCode; } -static bool TryPlayRoofProductionAnim(BuildingClass* pBuilding, bool isDamaged) +// pUnit, when given, is the unit whose exit triggered this call. +static bool TryPlayRoofProductionAnim(BuildingClass* pBuilding, bool isDamaged, TechnoClass* pUnit = nullptr) { - if (!IsRoofExitBuildingUnit(pBuilding)) + if (!(pUnit ? IsRoofExitTechno(pUnit->GetTechnoType()) : IsRoofExitBuildingUnit(pBuilding))) return false; auto pTypeExt = BuildingTypeExt::Fetch(pBuilding->Type); @@ -1171,6 +1170,8 @@ DEFINE_HOOK(0x44DDDE, BuildingClass_Unload_RoofProductionAnim_Damaged, 0x6) DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, 0x6) { GET(BuildingClass*, pThis, ESI); + // The roof check must use the unit that is actually leaving the factory. + GET(TechnoClass*, pUnit, EDI); auto const pType = pThis->Type; @@ -1178,10 +1179,10 @@ DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, { const bool isDamaged = pThis->GetHealthPercentage() <= RulesClass::Instance->ConditionYellow; - if (IsRoofExitBuildingUnit(pThis)) + if (pUnit && IsRoofExitTechno(pUnit->GetTechnoType())) { pThis->DestroyNthAnim(BuildingAnimSlot::Idle); - TryPlayRoofProductionAnim(pThis, isDamaged); + TryPlayRoofProductionAnim(pThis, isDamaged, pUnit); return 0; } From 2449c6ac58d130d35c6c3e483ac3aeffb0f1ead4 Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Sun, 16 Aug 2026 02:38:25 +0800 Subject: [PATCH 6/7] Optimize code, remove redundancy. --- src/Ext/Building/Hooks.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 9086b5c119..b8b00b0351 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1128,10 +1128,11 @@ DEFINE_HOOK(0x44B7AE, BuildingClass_Mission_Repair_ProductionAnim, 0x6) return SkipGameCode; } -// pUnit, when given, is the unit whose exit triggered this call. -static bool TryPlayRoofProductionAnim(BuildingClass* pBuilding, bool isDamaged, TechnoClass* pUnit = nullptr) +// isRoofExit tells whether the produced unit leaves through the roof hatch; +// each caller decides it from its own reliable source. +static bool TryPlayRoofProductionAnim(BuildingClass* pBuilding, bool isDamaged, bool isRoofExit) { - if (!(pUnit ? IsRoofExitTechno(pUnit->GetTechnoType()) : IsRoofExitBuildingUnit(pBuilding))) + if (!isRoofExit) return false; auto pTypeExt = BuildingTypeExt::Fetch(pBuilding->Type); @@ -1155,7 +1156,7 @@ DEFINE_HOOK(0x44DDF0, BuildingClass_Unload_RoofProductionAnim, 0x6) GET(BuildingClass*, pBuilding, EBP); - return TryPlayRoofProductionAnim(pBuilding, false) ? SkipGameCode : 0; + return TryPlayRoofProductionAnim(pBuilding, false, IsRoofExitBuildingUnit(pBuilding)) ? SkipGameCode : 0; } DEFINE_HOOK(0x44DDDE, BuildingClass_Unload_RoofProductionAnim_Damaged, 0x6) @@ -1164,7 +1165,7 @@ DEFINE_HOOK(0x44DDDE, BuildingClass_Unload_RoofProductionAnim_Damaged, 0x6) GET(BuildingClass*, pBuilding, EBP); - return TryPlayRoofProductionAnim(pBuilding, true) ? SkipGameCode : 0; + return TryPlayRoofProductionAnim(pBuilding, true, IsRoofExitBuildingUnit(pBuilding)) ? SkipGameCode : 0; } DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, 0x6) @@ -1182,7 +1183,7 @@ DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, if (pUnit && IsRoofExitTechno(pUnit->GetTechnoType())) { pThis->DestroyNthAnim(BuildingAnimSlot::Idle); - TryPlayRoofProductionAnim(pThis, isDamaged, pUnit); + TryPlayRoofProductionAnim(pThis, isDamaged, true); return 0; } From d0757c533b042cc55df8d3fbcffad767479a88e3 Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Mon, 17 Aug 2026 11:05:08 +0800 Subject: [PATCH 7/7] update docs --- docs/New-or-Enhanced-Logics.md | 2 +- docs/Whats-New.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 35927d0849..83b0323d20 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -744,7 +744,7 @@ PowerPlantEnhancer.MaxCount=-1 ; integer ### Roof production anim - Now, you can use the `RoofProductionAnim*` series of flags to replace the `ProductionAnim*` series of flags when the produced infantry and vehicles are leaving the factory through the roof hatch. - - The criterion for exiting through the roof hatch is the same as `RoofDeployingAnim` in vanilla: the techno has `Jumpjet=yes` or `BalloonHover=yes`. + - The criterion for exiting through the roof hatch is the same as `RoofDeployingAnim` in vanilla: the techno has `JumpJet=yes` or `BalloonHover=yes`. In `artmd.ini`: ```ini diff --git a/docs/Whats-New.md b/docs/Whats-New.md index a3dba53aac..257b0b3943 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -419,6 +419,7 @@ HideShakeEffects=false ; boolean :open: #### New: +- [`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish) #### Vanilla fixes: @@ -694,7 +695,6 @@ HideShakeEffects=false ; boolean - [Disable AlphaImage during Buildup](Fixed-or-Improved-Logics.md#disable-alphaimage-during-buildup) (by Noble_Fish) - [Reload speed adjustment on promotion](New-or-Enhanced-Logics.md#reload-speed-adjustment-on-promotion) (by Nuke) - Allowed `(Pre)ProductionAnim` animations to use `Powered` & `PoweredLight/Effect/Special` keys (by Noble_Fish) -- [`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish) #### Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)