From 9c5ccf3d2fa7fe646e7742d2a26feeb78cf1d442 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 9 Sep 2026 08:26:01 +0200 Subject: [PATCH 1/2] Flying production --- CREDITS.md | 2 + docs/New-or-Enhanced-Logics.md | 33 ++ docs/Whats-New.md | 1 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 4 + .../LC_MESSAGES/New-or-Enhanced-Logics.po | 42 +++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 4 + src/Ext/Building/Body.h | 1 + src/Ext/Building/Hooks.Production.cpp | 297 ++++++++++++++++++ src/Ext/BuildingType/Body.cpp | 10 + src/Ext/BuildingType/Body.h | 10 + src/Ext/TechnoType/Body.cpp | 29 ++ src/Ext/TechnoType/Body.h | 16 + src/Misc/Hooks.BugFixes.cpp | 10 + 13 files changed, 459 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index 2581c7f73d..7156f3b3dd 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -161,6 +161,7 @@ This page lists all the individual contributions to the project by their author. - Unit & infantry auto-conversion on ammo change - Restore the ScriptType action#24 `Play speech` from Tiberian Sun - Modify ammo on impact + - Flying production - **Starkku**: - Misc. minor bugfixes & improvements - AI script actions: @@ -962,3 +963,4 @@ This page lists all the individual contributions to the project by their author. - **Nuke** - Reload speed adjustment on promotion - **frg2089 (舰队的偶像-岛风酱!)** - Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed - **weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral houses + diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 91d2474500..974c3664f5 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2103,6 +2103,39 @@ In `artmd.ini`: FLHKEY.BurstN= ; integer - Forward,Lateral,Height. FLHKey refers to weapon-specific FLH key name and N is zero-based burst shot index. ``` +### Flying production + +- Now you can allow produced units to spawn directly in the air at customizable altitudes and locations without being constrained by traditional factory ground exit paths. + - `FlyingProduction` controls whether this unit type uses flying production. + - `FlyingProduction.SpawnHeight` determines the initial spawn altitude in leptons along the Z-axis. If resolved to 0 or negative, flying production will not be used and the unit exits normally. + - `FlyingProduction.PlayFactoryAnim` controls whether the producing factory plays its unloading door/exit animation. + - `FlyingProduction.SpawnAnim` specifies an animation to play at the aerial spawn coordinates when the unit is created. + - `FlyingProduction.SpawnAnim.AttachedToObject` determines whether `FlyingProduction.SpawnAnim` is attached to the spawned unit and follows its movement. + - `FlyingProduction.SpawnAt` accepts a list of BuildingTypes indicating where the unit should spawn, evaluated in priority order from left to right. When multiple buildings of that type exist, it prioritizes the producing factory itself if it matches, then any building marked as primary, and otherwise the closest one to the producing factory. If none of the candidate buildings exist or are alive, it falls back to the producing factory. + - `FlyingProduction.RallyPointFromSpawnBuilding` determines whether the unit targets the spawn building's rally point instead of the producing factory's rally point. + - `FlyingProduction.SpawnOffset` on `[BuildingType]` sets a 2D coordinate offset in leptons (X, Y) relative to the center of the structure for positioning the spawn point. + - `FlyingProduction.SpawnHeight` on `[BuildingType]` overrides the spawn altitude for units appearing at this structure. + - `FlyingProduction.SpawnFacing` on `[BuildingType]` specifies the facing (0-255) the unit takes upon spawning, as well as the exit direction if no rally point exists. + - `FlyingProduction.RallyPoint` on `[BuildingType]` enables rally point placement on structures that are not standard vehicle or infantry factories (e.g. helipads or towers), allowing them to establish a rally point with EVA confirmation and tactical line. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +FlyingProduction=false ; boolean +FlyingProduction.SpawnHeight= ; integer, defaults to FlightLevel or [General] -> FlightLevel +FlyingProduction.PlayFactoryAnim=false ; boolean +FlyingProduction.SpawnAnim= ; AnimType +FlyingProduction.SpawnAnim.AttachedToObject=false ; boolean +FlyingProduction.SpawnAt= ; List of BuildingTypes +FlyingProduction.RallyPointFromSpawnBuilding=false ; boolean + +[SOMEBUILDING] ; BuildingType +FlyingProduction.SpawnOffset=0,0 ; Point2D, leptons +FlyingProduction.SpawnHeight= ; integer, defaults to the spawned unit's FlyingProduction.SpawnHeight +FlyingProduction.SpawnFacing= ; DirType (0-255), defaults to the building's facing (128 / South) +FlyingProduction.RallyPoint=false ; boolean +``` + ### Forcing specific weapon against certain targets ![image](_static/images/underwater-new-attack-tag.gif) diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 20779f3d09..f31fe6687e 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -424,6 +424,7 @@ HideShakeEffects=false ; boolean :open: #### New: +- [Flying production](New-or-Enhanced-Logics.md#flying-production) (by FS-21) - [Customized transport plane for teams](AI-Scripting-and-Mapping.md#customized-transport-plane-for-teams) (by FlyStar) - [Modify ammo on impact](New-or-Enhanced-Logics.md#modify-ammo-on-impact) (by FS-21) - [Customize ivan bomb visibility](Fixed-or-Improved-Logics.md#customize-ivan-bomb-visibility) (by NetsuNegi) diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index e5eb5bc694..157c50faf3 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -477,6 +477,9 @@ msgstr "复原了《泰伯利亚之日》中的动作脚本 `24 - 播放语音`" msgid "Modify ammo on impact" msgstr "修改弹药弹头" +msgid "Flying production" +msgstr "飞行生产" + msgid "**Starkku**:" msgstr "**Starkku**:" @@ -3308,3 +3311,4 @@ msgid "" "houses" msgstr "**weiyongxuan** - 扩展 `CanTargetHouses` 以允许瞄准中立所属方" + 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 dbbc2f9f0b..293ba94700 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 @@ -7572,3 +7572,45 @@ msgid "" "the weapon to be able to fire at cells containing no TechnoTypes." msgstr "`CanTarget` 要求明确列出 `all` 或 `empty` 才能对不包含任何科技类型的单元格开火。" +msgid "Flying production" +msgstr "飞行生产" + +msgid "" +"Now you can allow produced units to spawn directly in the air at customizable altitudes and locations without being constrained by traditional factory ground exit paths." +msgstr "" +"现在你可以允许生产出的单位直接在空中指定高度和位置生成,而不再受传统工厂地面出厂路径的限制。" + +msgid "`FlyingProduction` controls whether this unit type uses flying production." +msgstr "`FlyingProduction` 控制此单位类型是否启用飞行生产逻辑。" + +msgid "`FlyingProduction.SpawnHeight` determines the initial spawn altitude in leptons along the Z-axis. If resolved to 0 or negative, flying production will not be used and the unit exits normally." +msgstr "`FlyingProduction.SpawnHeight` 决定沿 Z 轴的初始生成高度(以 lepton 为单位)。如果最终解析为 0 或负数,则不会使用飞行生产逻辑(单位将正常从地面出厂)。" + +msgid "`FlyingProduction.PlayFactoryAnim` controls whether the producing factory plays its unloading door/exit animation." +msgstr "`FlyingProduction.PlayFactoryAnim` 控制生产该单位的工厂是否播放卸载/开门动画。" + +msgid "`FlyingProduction.SpawnAnim` specifies an animation to play at the aerial spawn coordinates when the unit is created." +msgstr "`FlyingProduction.SpawnAnim` 指定单位生成时在空中生成坐标处播放的动画。" + +msgid "`FlyingProduction.SpawnAnim.AttachedToObject` determines whether `FlyingProduction.SpawnAnim` is attached to the spawned unit and follows its movement." +msgstr "`FlyingProduction.SpawnAnim.AttachedToObject` 决定 `FlyingProduction.SpawnAnim` 是否附加到生成的单位上并随其移动。" + +msgid "`FlyingProduction.SpawnAt` accepts a list of BuildingTypes indicating where the unit should spawn, evaluated in priority order from left to right. When multiple buildings of that type exist, it prioritizes the producing factory itself if it matches, then any building marked as primary, and otherwise the closest one to the producing factory. If none of the candidate buildings exist or are alive, it falls back to the producing factory." +msgstr "`FlyingProduction.SpawnAt` 接受一个 BuildingType 列表,按从左至右的优先级顺序判断单位应在哪些建筑处生成。当存在多个该类型的建筑时,若生产该单位的工厂自身匹配则直接优先使用,其次优先选择被设为主要建筑(Primary)的建筑,否则选择距生产工厂最近的建筑。如果列表中候选建筑均不存在或已损毁,则回退至生产该单位的工厂。" + +msgid "`FlyingProduction.RallyPointFromSpawnBuilding` determines whether the unit targets the spawn building's rally point instead of the producing factory's rally point." +msgstr "`FlyingProduction.RallyPointFromSpawnBuilding` 决定单位是否以实际生成建筑的集结点(Rally Point)为目标,而不是生产工厂的集结点。" + +msgid "`FlyingProduction.SpawnOffset` on `[BuildingType]` sets a 2D coordinate offset in leptons (X, Y) relative to the center of the structure for positioning the spawn point." +msgstr "`[BuildingType]` 上的 `FlyingProduction.SpawnOffset` 设置相对于建筑中心的 2D 坐标偏移(X, Y,以 lepton 为单位),用于精确定位生成点。" + +msgid "`FlyingProduction.SpawnHeight` on `[BuildingType]` overrides the spawn altitude for units appearing at this structure." +msgstr "`[BuildingType]` 上的 `FlyingProduction.SpawnHeight` 覆盖在此建筑处生成的单位的生成高度。" + +msgid "`FlyingProduction.SpawnFacing` on `[BuildingType]` specifies the facing (0-255) the unit takes upon spawning, as well as the exit direction if no rally point exists." +msgstr "`[BuildingType]` 上的 `FlyingProduction.SpawnFacing` 指定单位生成时的朝向(0-255),若无集结点则同时作为离开建筑的移动方向。" + +msgid "`FlyingProduction.RallyPoint` on `[BuildingType]` enables rally point placement on structures that are not standard vehicle or infantry factories (e.g. helipads or towers), allowing them to establish a rally point with EVA confirmation and tactical line." +msgstr "`[BuildingType]` 上的 `FlyingProduction.RallyPoint` 允许在非传统车辆或步兵工厂的建筑(如停机坪、防御塔等)上设置集结点,支持 EVA 语音确认及战术虚线显示。" + + diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 3dbbe0b5d1..b2ba870f65 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -6696,3 +6696,7 @@ msgstr "现在侧边栏拓展工具条可以超出侧边栏边界(by Belonit msgid "Lifted stupidly small limit for tooltip character amount (by Belonit)" msgstr "解除了拓展工具条字符数量小得愚蠢的上限(by Belonit)" +msgid "[Flying production](New-or-Enhanced-Logics.md#flying-production) (by FS-21)" +msgstr "[飞行生产](New-or-Enhanced-Logics.md#flying-production)(by FS-21)" + + diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index c1fcdc3b85..16cf960dc9 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -123,5 +123,6 @@ class BuildingExt final : public TechnoExt, public Detach::Listener& info, void*, BuildingClass* pFactory); static int GetTurretFrame(BuildingClass* pThis); static bool BuildingOnline(BuildingClass* pThis); + static bool TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* pProduction); }; diff --git a/src/Ext/Building/Hooks.Production.cpp b/src/Ext/Building/Hooks.Production.cpp index 8b01588194..d81f058207 100644 --- a/src/Ext/Building/Hooks.Production.cpp +++ b/src/Ext/Building/Hooks.Production.cpp @@ -1,6 +1,15 @@ #include "Body.h" +#include #include +#include +#include +#include +#include +#include +#include +#include +#include DEFINE_HOOK(0x4401BB, BuildingClass_AI_PickWithFreeDocks, 0x6) { @@ -246,3 +255,291 @@ DEFINE_HOOK(0x4449FB, BuildingClass_KickOutUnit_CloningVats, 0x8) return SkipGameCode; } + +bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* pProduction) +{ + if (!pFactory || !pProduction) + return false; + + if (pFactory->GetCurrentMission() == Mission::Construction) + return false; + + auto const pType = pProduction->GetTechnoType(); + if (!pType) + return false; + + auto const pTypeExt = TechnoTypeExt::Fetch(pType); + if (!pTypeExt || !pTypeExt->FlyingProduction) + return false; + + BuildingClass* pSpawnBuilding = nullptr; + + if (!pTypeExt->FlyingProduction_SpawnAt.empty()) + { + for (auto const pTargetType : pTypeExt->FlyingProduction_SpawnAt) + { + if (!pTargetType) + continue; + + if (pFactory->Type == pTargetType) + { + pSpawnBuilding = pFactory; + break; + } + + BuildingClass* pPrimaryCandidate = nullptr; + BuildingClass* pClosestCandidate = nullptr; + int minDistance = (std::numeric_limits::max)(); + + for (auto const pBld : pFactory->Owner->Buildings) + { + if (!pBld || !pBld->IsAlive || pBld->InLimbo || pBld->Health <= 0 || pBld->GetCurrentMission() == Mission::Selling) + continue; + + if (pBld->Type == pTargetType) + { + if (pBld->IsPrimaryFactory && !pPrimaryCandidate) + pPrimaryCandidate = pBld; + + const int dist = pFactory->DistanceFrom(pBld); + if (dist < minDistance) + { + minDistance = dist; + pClosestCandidate = pBld; + } + } + } + + if (auto const pCandidate = pPrimaryCandidate ? pPrimaryCandidate : pClosestCandidate) + { + pSpawnBuilding = pCandidate; + break; + } + } + } + + if (!pSpawnBuilding) + pSpawnBuilding = pFactory; + + auto const pSpawnBldTypeExt = BuildingTypeExt::Fetch(pSpawnBuilding->Type); + + int height = pTypeExt->FlyingProduction_SpawnHeight.Get(); + if (pSpawnBldTypeExt && pSpawnBldTypeExt->FlyingProduction_SpawnHeight.isset()) + height = pSpawnBldTypeExt->FlyingProduction_SpawnHeight.Get(); + + if (height <= 0) + return false; + + CoordStruct spawnCoords = pSpawnBuilding->GetCoords(); + if (pSpawnBldTypeExt && pSpawnBldTypeExt->FlyingProduction_SpawnOffset.isset()) + { + const auto& offset = pSpawnBldTypeExt->FlyingProduction_SpawnOffset.Get(); + spawnCoords.X += offset.X; + spawnCoords.Y += offset.Y; + } + spawnCoords.Z += height; + + DirStruct facing = pSpawnBuilding->PrimaryFacing.Current(); + if (pSpawnBldTypeExt && pSpawnBldTypeExt->FlyingProduction_SpawnFacing.isset()) + facing = DirStruct(pSpawnBldTypeExt->FlyingProduction_SpawnFacing.Get()); + + pProduction->SetOwningHouse(pFactory->Owner, true); + + bool unlimboSuccess = false; + { + struct ScenarioInitGuard + { + ScenarioInitGuard() { ++Unsorted::ScenarioInit; } + ~ScenarioInitGuard() { --Unsorted::ScenarioInit; } + } guard; + + unlimboSuccess = pProduction->Unlimbo(spawnCoords, facing.GetDir()); + } + + if (!unlimboSuccess) + return false; + + pProduction->SetLocation(spawnCoords); + pProduction->Location = spawnCoords; + pProduction->InAir = true; + pProduction->IsALoaner = false; + pProduction->UnmarkAllOccupationBits(spawnCoords); + pProduction->PrimaryFacing.SetCurrent(facing); + pProduction->PrimaryFacing.SetDesired(facing); + + if (auto const pFoot = abstract_cast(pProduction)) + { + if (auto const pJJLoco = locomotion_cast(pFoot->Locomotor)) + { + pJJLoco->CurrentHeight = height; + pJJLoco->LocomotionFacing.SetCurrent(facing); + pJJLoco->LocomotionFacing.SetDesired(facing); + pFoot->Jumpjet_OccupyCell(CellClass::Coord2Cell(spawnCoords)); + + if (height < pJJLoco->Height) + { + pJJLoco->State = JumpjetLocomotionClass::State::Ascending; + FootExt::Fetch(pFoot)->JumpjetStraightAscend = true; + } + else + { + pJJLoco->State = JumpjetLocomotionClass::State::Hovering; + } + } + else if (auto const pFlyLoco = locomotion_cast(pFoot->Locomotor)) + { + const auto pFootType = pFoot->GetTechnoType(); + const int cruiseFlightLevel = pFootType->GetFlightLevel(); + const int targetFlightLevel = cruiseFlightLevel > 0 ? cruiseFlightLevel : (RulesClass::Instance ? RulesClass::Instance->FlightLevel : height); + pFlyLoco->FlightLevel = targetFlightLevel; + pFlyLoco->CurrentSpeed = 0.0; + pFlyLoco->TargetSpeed = 0.0; + pFlyLoco->IsTakingOff = height < targetFlightLevel ? 1 : 0; + pFlyLoco->IsLanding = false; + } + } + + if (auto const pAircraft = abstract_cast(pProduction)) + pAircraft->SetHeight(height); + + if (auto const pAnimType = pTypeExt->FlyingProduction_SpawnAnim.Get()) + { + if (auto const pAnim = GameCreate(pAnimType, spawnCoords)) + { + if (pTypeExt->FlyingProduction_SpawnAnim_AttachedToObject.Get()) + pAnim->SetOwnerObject(pProduction); + } + } + + AbstractClass* pRallyTarget = nullptr; + if (pTypeExt->FlyingProduction_RallyPointFromSpawnBuilding.Get() && pSpawnBuilding->ArchiveTarget) + pRallyTarget = pSpawnBuilding->ArchiveTarget; + else + pRallyTarget = pFactory->ArchiveTarget; + + if (pRallyTarget && pRallyTarget != pSpawnBuilding) + { + pProduction->SetDestination(pRallyTarget, true); + pProduction->QueueMission(Mission::Move, true); + } + else + { + // No rally point set: give an exit nudge away from the spawn building in the facing direction + // to prevent units from remaining frozen directly over the building or blocking subsequent spawns. + const auto pBldType = pSpawnBuilding->Type; + const int nudgeDistance = std::max({ static_cast(pBldType->GetFoundationWidth()), static_cast(pBldType->GetFoundationHeight(true)), 2 }) / 2 + 1; + auto pDestCell = MapClass::Instance.GetCellAt(spawnCoords); + const auto facingType = static_cast(facing.GetValue<3>()); + + for (int i = 0; i < nudgeDistance; ++i) + { + if (auto const pNext = pDestCell->GetNeighbourCell(facingType)) + pDestCell = pNext; + } + + if (pDestCell) + { + pProduction->SetDestination(pDestCell, true); + pProduction->QueueMission(Mission::Move, true); + } + else + { + pProduction->Scatter(CoordStruct::Empty, true, false); + pProduction->QueueMission(Mission::Guard, true); + } + } + + if (pTypeExt->FlyingProduction_PlayFactoryAnim.Get()) + pFactory->QueueMission(Mission::Unload, false); + + if (auto const pOwner = pFactory->Owner) + { + auto const pHouseExt = HouseExt::Fetch(pOwner); + auto const abs = pProduction->WhatAmI(); + + if (abs == AbstractType::Unit) + { + if (auto const pUnit = abstract_cast(pProduction)) + { + if (pUnit->Type->Naval) + { + if (pHouseExt) + pHouseExt->ProducingNavalUnitTypeIndex = -1; + } + else + { + pOwner->ProducingUnitTypeIndex = -1; + } + } + } + else if (abs == AbstractType::Aircraft) + { + pOwner->ProducingAircraftTypeIndex = -1; + } + else if (abs == AbstractType::Infantry) + { + pOwner->ProducingInfantryTypeIndex = -1; + } + else if (abs == AbstractType::Building) + { + pOwner->ProducingBuildingTypeIndex = -1; + } + + if (pHouseExt) + { + if (pHouseExt->Factory_VehicleType == pFactory) + pHouseExt->Factory_VehicleType = nullptr; + if (pHouseExt->Factory_NavyType == pFactory) + pHouseExt->Factory_NavyType = nullptr; + if (pHouseExt->Factory_InfantryType == pFactory) + pHouseExt->Factory_InfantryType = nullptr; + if (pHouseExt->Factory_AircraftType == pFactory) + pHouseExt->Factory_AircraftType = nullptr; + if (pHouseExt->Factory_BuildingType == pFactory) + pHouseExt->Factory_BuildingType = nullptr; + } + + if (!pFactory->Type->Cloning) + { + auto info = std::make_pair(pType, pOwner); + for (const auto pVat : pOwner->CloningVats) + BuildingExt::KickOutClone(info, 0, pVat); + } + } + + return true; +} + +DEFINE_HOOK(0x443C60, BuildingClass_KickOutUnit_FlyingProduction, 0x6) +{ + enum { DoneSucceeded = 0x4456A2 }; + + GET(BuildingClass*, pFactory, ECX); + GET_STACK(TechnoClass*, pProduction, 0x4); + + if (BuildingExt::TrySpawnFlyingProduction(pFactory, pProduction)) + { + R->EAX(static_cast(KickOutResult::Succeeded)); + return DoneSucceeded; + } + + return 0; +} + +DEFINE_HOOK(0x455DA0, BuildingClass_IsUnitFactory_FlyingProduction, 0x6) +{ + enum { ReturnTrue = 0x455DCD }; + + GET(BuildingClass*, pThis, ECX); + + if (auto const pTypeExt = BuildingTypeExt::Fetch(pThis->Type)) + { + if (pTypeExt->FlyingProduction_RallyPoint.Get()) + return ReturnTrue; + } + + return 0; +} + + + diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 1d37508cc5..4020e0f711 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -386,6 +386,11 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) // Ares 3.0 this->UnitSell.Read(exINI, pSection, "UnitSell"); + + this->FlyingProduction_SpawnOffset.Read(exINI, pSection, "FlyingProduction.SpawnOffset"); + this->FlyingProduction_SpawnHeight.Read(exINI, pSection, "FlyingProduction.SpawnHeight"); + this->FlyingProduction_SpawnFacing.Read(exINI, pSection, "FlyingProduction.SpawnFacing"); + this->FlyingProduction_RallyPoint.Read(exINI, pSection, "FlyingProduction.RallyPoint"); } void BuildingTypeExt::CompleteInitialization() @@ -509,6 +514,11 @@ void BuildingTypeExt::Serialize(T& Stm) // Ares 3.0 .Process(this->UnitSell) + + .Process(this->FlyingProduction_SpawnOffset) + .Process(this->FlyingProduction_SpawnHeight) + .Process(this->FlyingProduction_SpawnFacing) + .Process(this->FlyingProduction_RallyPoint) ; } diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 254c604546..5e68496fef 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -154,6 +154,11 @@ class BuildingTypeExt final : public TechnoTypeExt // Ares 3.0 Nullable UnitSell; + Nullable FlyingProduction_SpawnOffset; + Nullable FlyingProduction_SpawnHeight; + Nullable FlyingProduction_SpawnFacing; + Valueable FlyingProduction_RallyPoint; + BuildingTypeExt(BuildingTypeClass* OwnerObject) : TechnoTypeExt(OwnerObject) , PowersUp_Owner { AffectedHouse::Owner } , PowersUp_Buildings {} @@ -265,6 +270,11 @@ class BuildingTypeExt final : public TechnoTypeExt // Ares 3.0 , UnitSell {} + + , FlyingProduction_SpawnOffset {} + , FlyingProduction_SpawnHeight {} + , FlyingProduction_SpawnFacing {} + , FlyingProduction_RallyPoint { false } { } // typed owner accessor (shadows the TechnoTypeClass one from the base) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index c5b99aab19..027a7443dc 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1441,6 +1441,27 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) // VoiceIFVRepair from Ares 0.2 this->VoiceIFVRepair.Read(exINI, pSection, "VoiceIFVRepair"); this->ParseVoiceWeaponAttacks(exINI, pSection, this->VoiceWeaponAttacks, this->VoiceEliteWeaponAttacks); + + this->FlyingProduction.Read(exINI, pSection, "FlyingProduction"); + this->FlyingProduction_SpawnHeight.Read(exINI, pSection, "FlyingProduction.SpawnHeight"); + this->FlyingProduction_PlayFactoryAnim.Read(exINI, pSection, "FlyingProduction.PlayFactoryAnim"); + this->FlyingProduction_SpawnAnim.Read(exINI, pSection, "FlyingProduction.SpawnAnim"); + this->FlyingProduction_SpawnAnim_AttachedToObject.Read(exINI, pSection, "FlyingProduction.SpawnAnim.AttachedToObject"); + this->FlyingProduction_SpawnAnim_AttachedToObject.Read(exINI, pSection, "FlyingProduction.SpawnAnim_AttachedToObject"); + this->FlyingProduction_SpawnAt.Read(exINI, pSection, "FlyingProduction.SpawnAt"); + this->FlyingProduction_RallyPointFromSpawnBuilding.Read(exINI, pSection, "FlyingProduction.RallyPointFromSpawnBuilding"); + + if (this->FlyingProduction.Get()) + { + if (!this->FlyingProduction_SpawnHeight.isset()) + { + const int defaultFlightLevel = RulesClass::Instance ? RulesClass::Instance->FlightLevel : 0; + this->FlyingProduction_SpawnHeight = defaultFlightLevel > 0 ? defaultFlightLevel : pThis->GetFlightLevel(); + } + + if (this->FlyingProduction_SpawnHeight.Get() <= 0) + this->FlyingProduction = false; + } } template @@ -1859,6 +1880,14 @@ void TechnoTypeExt::Serialize(T& Stm) // Ares 3.0 .Process(this->Unsellable) .Process(this->KeepAlive) + + .Process(this->FlyingProduction) + .Process(this->FlyingProduction_SpawnHeight) + .Process(this->FlyingProduction_PlayFactoryAnim) + .Process(this->FlyingProduction_SpawnAnim) + .Process(this->FlyingProduction_SpawnAnim_AttachedToObject) + .Process(this->FlyingProduction_SpawnAt) + .Process(this->FlyingProduction_RallyPointFromSpawnBuilding) ; } void TechnoTypeExt::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index fda8f6f3dc..51b1243793 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -343,6 +343,14 @@ class TechnoTypeExt : public ObjectTypeExt Nullable RadarInvisibleToHouse; + Valueable FlyingProduction; + Nullable FlyingProduction_SpawnHeight; + Valueable FlyingProduction_PlayFactoryAnim; + Nullable FlyingProduction_SpawnAnim; + Valueable FlyingProduction_SpawnAnim_AttachedToObject; + ValueableVector FlyingProduction_SpawnAt; + Valueable FlyingProduction_RallyPointFromSpawnBuilding; + struct LaserTrailDataEntry { ValueableIdx idxType; @@ -876,6 +884,14 @@ class TechnoTypeExt : public ObjectTypeExt // Ares 3.0 , Unsellable {} , KeepAlive {} + + , FlyingProduction { false } + , FlyingProduction_SpawnHeight {} + , FlyingProduction_PlayFactoryAnim { false } + , FlyingProduction_SpawnAnim {} + , FlyingProduction_SpawnAnim_AttachedToObject { false } + , FlyingProduction_SpawnAt {} + , FlyingProduction_RallyPointFromSpawnBuilding { false } { } virtual ~TechnoTypeExt() = default; diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index a1da2aedb5..f9d6bd6fb5 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -286,6 +286,16 @@ DEFINE_HOOK(0x4438B4, BuildingClass_SetRallyPoint_Naval, 0x6) } } + if (auto const pBldTypeExt = BuildingTypeExt::Fetch(pBuildingType)) + { + if (pBldTypeExt->FlyingProduction_RallyPoint.Get()) + { + R->ESI(static_cast(MovementZone::Fly)); + spdtp = SpeedType::Winged; + return NotNaval; + } + } + if (pBuildingType->Naval || pBuildingType->SpeedType == SpeedType::Float) return IsNaval; From 6e53659c32c164379a90c6d3fa16448b9dd03837 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 9 Sep 2026 09:13:16 +0200 Subject: [PATCH 2/2] Optimize primary factory lookup and clean comments in FlyingProduction --- src/Ext/Building/Hooks.Production.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Ext/Building/Hooks.Production.cpp b/src/Ext/Building/Hooks.Production.cpp index d81f058207..3b87a82ab2 100644 --- a/src/Ext/Building/Hooks.Production.cpp +++ b/src/Ext/Building/Hooks.Production.cpp @@ -258,10 +258,7 @@ DEFINE_HOOK(0x4449FB, BuildingClass_KickOutUnit_CloningVats, 0x8) bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* pProduction) { - if (!pFactory || !pProduction) - return false; - - if (pFactory->GetCurrentMission() == Mission::Construction) + if (!pFactory || !pProduction || pFactory->GetCurrentMission() == Mission::Construction) return false; auto const pType = pProduction->GetTechnoType(); @@ -272,6 +269,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* if (!pTypeExt || !pTypeExt->FlyingProduction) return false; + // Resolve which building should physically spawn the unit BuildingClass* pSpawnBuilding = nullptr; if (!pTypeExt->FlyingProduction_SpawnAt.empty()) @@ -281,6 +279,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* if (!pTargetType) continue; + // If the factory itself matches this target type, it takes immediate precedence if (pFactory->Type == pTargetType) { pSpawnBuilding = pFactory; @@ -298,8 +297,12 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* if (pBld->Type == pTargetType) { - if (pBld->IsPrimaryFactory && !pPrimaryCandidate) + if (pBld->IsPrimaryFactory) + { + // Primary factory has absolute priority; no need to evaluate remaining buildings pPrimaryCandidate = pBld; + break; + } const int dist = pFactory->DistanceFrom(pBld); if (dist < minDistance) @@ -323,6 +326,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* auto const pSpawnBldTypeExt = BuildingTypeExt::Fetch(pSpawnBuilding->Type); + // Determine spawn height, offset, and facing int height = pTypeExt->FlyingProduction_SpawnHeight.Get(); if (pSpawnBldTypeExt && pSpawnBldTypeExt->FlyingProduction_SpawnHeight.isset()) height = pSpawnBldTypeExt->FlyingProduction_SpawnHeight.Get(); @@ -343,6 +347,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* if (pSpawnBldTypeExt && pSpawnBldTypeExt->FlyingProduction_SpawnFacing.isset()) facing = DirStruct(pSpawnBldTypeExt->FlyingProduction_SpawnFacing.Get()); + // Unlimbo unit in the air safely using ScenarioInitGuard pProduction->SetOwningHouse(pFactory->Owner, true); bool unlimboSuccess = false; @@ -359,6 +364,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* if (!unlimboSuccess) return false; + // Establish aerial state and position pProduction->SetLocation(spawnCoords); pProduction->Location = spawnCoords; pProduction->InAir = true; @@ -367,6 +373,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* pProduction->PrimaryFacing.SetCurrent(facing); pProduction->PrimaryFacing.SetDesired(facing); + // Configure locomotor (Jumpjet / Aircraft) if (auto const pFoot = abstract_cast(pProduction)) { if (auto const pJJLoco = locomotion_cast(pFoot->Locomotor)) @@ -402,6 +409,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* if (auto const pAircraft = abstract_cast(pProduction)) pAircraft->SetHeight(height); + // Spawn visual effects if (auto const pAnimType = pTypeExt->FlyingProduction_SpawnAnim.Get()) { if (auto const pAnim = GameCreate(pAnimType, spawnCoords)) @@ -411,6 +419,7 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* } } + // Direct unit towards rally point or nudge away from factory AbstractClass* pRallyTarget = nullptr; if (pTypeExt->FlyingProduction_RallyPointFromSpawnBuilding.Get() && pSpawnBuilding->ArchiveTarget) pRallyTarget = pSpawnBuilding->ArchiveTarget; @@ -449,9 +458,11 @@ bool BuildingExt::TrySpawnFlyingProduction(BuildingClass* pFactory, TechnoClass* } } + // Trigger factory door/unload animation if requested if (pTypeExt->FlyingProduction_PlayFactoryAnim.Get()) pFactory->QueueMission(Mission::Unload, false); + // Reset house production indices, factory pointers, and replicate via cloning vats if (auto const pOwner = pFactory->Owner) { auto const pHouseExt = HouseExt::Fetch(pOwner);