From 3a3cd403b6d0af833ae32b69bc2d73dea39f9ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Mon, 20 Jul 2026 10:55:50 +0800 Subject: [PATCH 01/17] refactor(Techno): move TransferMindControlOnDeploy logic to TechnoExt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 68 ++++++++++++++++++++++++++++ src/Ext/Techno/Body.h | 1 + src/Ext/Unit/Hooks.DeploysInto.cpp | 72 +----------------------------- 3 files changed, 71 insertions(+), 70 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 1615942f11..55d32c366c 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -470,6 +471,73 @@ bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) return true; } +void TechnoExt::TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo) +{ + const auto pAnimType = pTechnoFrom->MindControlRingAnim + ? pTechnoFrom->MindControlRingAnim->Type + : TechnoExt::Fetch(pTechnoFrom)->MindControlRingAnimType; + + if (const auto Controller = pTechnoFrom->MindControlledBy) + { + if (const auto Manager = Controller->CaptureManager) + { + CaptureManagerExt::FreeUnit(Manager, pTechnoFrom, true); + + if (CaptureManagerExt::CaptureUnit(Manager, pTechnoTo, false, pAnimType, true)) + { + if (const auto pBld = abstract_cast(pTechnoTo)) + { + // Capturing the building after unlimbo before buildup has finished or even started appears to throw certain things off, + // Hopefully this is enough to fix most of it like anims playing prematurely etc. + pBld->ActuallyPlacedOnMap = false; + pBld->DestroyNthAnim(BuildingAnimSlot::All); + + pBld->BeginMode(BStateType::Construction); + pBld->QueueMission(Mission::Construction, false); + } + } + else + { + int nSound = pTechnoTo->GetTechnoType()->MindClearedSound; + if (nSound == -1) + nSound = RulesClass::Instance->MindClearedSound; + + if (nSound != -1) + VocClass::PlayIndexAtPos(nSound, pTechnoTo->Location); + } + } + } + else if (const auto MCHouse = pTechnoFrom->MindControlledByHouse) + { + pTechnoTo->MindControlledByHouse = MCHouse; + pTechnoFrom->MindControlledByHouse = nullptr; + } + else if (pTechnoFrom->MindControlledByAUnit) // Perma MC + { + pTechnoTo->MindControlledByAUnit = true; + + const auto pBuilding = abstract_cast(pTechnoTo); + CoordStruct location = pTechnoTo->GetCoords(); + + location.Z += pBuilding + ? pBuilding->Type->Height * Unsorted::LevelHeight + : pTechnoTo->GetTechnoType()->MindControlRingOffset; + + const auto pAnim = pAnimType + ? GameCreate(pAnimType, location, 0, 1) + : nullptr; + + if (pAnim) + { + if (pBuilding) + pAnim->ZAdjust = -1024; + + pTechnoTo->MindControlRingAnim = pAnim; + pAnim->SetOwnerObject(pTechnoTo); + } + } +} + bool TechnoExt::IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource) { if (!pThis || !pSource) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 72c255a142..0fb64fd69e 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -246,6 +246,7 @@ class TechnoExt : public RadioExt, public Detach::Listener static bool AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, TargetZoneScanType zoneScanType, WeaponTypeClass* pWeapon = nullptr, bool useZone = false, int zone = -1); static void UpdateAttachedAnimLayers(TechnoClass* pThis); static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); + static void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo); static bool IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource); static int GetTintColor(TechnoClass* pThis, bool invulnerability, bool airstrike, bool berserk); static int GetCustomTintColor(TechnoClass* pThis); diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 41aa1b3d43..473b91d001 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -3,7 +3,6 @@ #include "Body.h" #include -#include #include #pragma region AllowDeployControlledMCV @@ -18,79 +17,12 @@ DEFINE_HOOK(0x700ED0, TechnoClass_AllowDeployControlledMCV, 0x6)// UnitClass::Ca #pragma endregion -static inline void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo) -{ - const auto pAnimType = pTechnoFrom->MindControlRingAnim - ? pTechnoFrom->MindControlRingAnim->Type - : TechnoExt::Fetch(pTechnoFrom)->MindControlRingAnimType; - - if (const auto Controller = pTechnoFrom->MindControlledBy) - { - if (const auto Manager = Controller->CaptureManager) - { - CaptureManagerExt::FreeUnit(Manager, pTechnoFrom, true); - - if (CaptureManagerExt::CaptureUnit(Manager, pTechnoTo, false, pAnimType, true)) - { - if (const auto pBld = abstract_cast(pTechnoTo)) - { - // Capturing the building after unlimbo before buildup has finished or even started appears to throw certain things off, - // Hopefully this is enough to fix most of it like anims playing prematurely etc. - pBld->ActuallyPlacedOnMap = false; - pBld->DestroyNthAnim(BuildingAnimSlot::All); - - pBld->BeginMode(BStateType::Construction); - pBld->QueueMission(Mission::Construction, false); - } - } - else - { - int nSound = pTechnoTo->GetTechnoType()->MindClearedSound; - if (nSound == -1) - nSound = RulesClass::Instance->MindClearedSound; - - if (nSound != -1) - VocClass::PlayIndexAtPos(nSound, pTechnoTo->Location); - } - } - } - else if (const auto MCHouse = pTechnoFrom->MindControlledByHouse) - { - pTechnoTo->MindControlledByHouse = MCHouse; - pTechnoFrom->MindControlledByHouse = nullptr; - } - else if (pTechnoFrom->MindControlledByAUnit) // Perma MC - { - pTechnoTo->MindControlledByAUnit = true; - - const auto pBuilding = abstract_cast(pTechnoTo); - CoordStruct location = pTechnoTo->GetCoords(); - - location.Z += pBuilding - ? pBuilding->Type->Height * Unsorted::LevelHeight - : pTechnoTo->GetTechnoType()->MindControlRingOffset; - - const auto pAnim = pAnimType - ? GameCreate(pAnimType, location, 0, 1) - : nullptr; - - if (pAnim) - { - if (pBuilding) - pAnim->ZAdjust = -1024; - - pTechnoTo->MindControlRingAnim = pAnim; - pAnim->SetOwnerObject(pTechnoTo); - } - } -} - DEFINE_HOOK(0x739956, UnitClass_Deploy_Transfer, 0x6) { GET(UnitClass*, pUnit, EBP); GET(BuildingClass*, pStructure, EBX); - TransferMindControlOnDeploy(pUnit, pStructure); + TechnoExt::TransferMindControlOnDeploy(pUnit, pStructure); ShieldClass::SyncShieldToAnother(pUnit, pStructure); TechnoExt::SyncInvulnerability(pUnit, pStructure); AttachEffectClass::TransferAttachedEffects(pUnit, pStructure); @@ -103,7 +35,7 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) GET(BuildingClass*, pStructure, EBP); GET(UnitClass*, pUnit, EBX); - TransferMindControlOnDeploy(pStructure, pUnit); + TechnoExt::TransferMindControlOnDeploy(pStructure, pUnit); ShieldClass::SyncShieldToAnother(pStructure, pUnit); TechnoExt::SyncInvulnerability(pStructure, pUnit); AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); From 2e8dccc77bbb8951768c8b003eaba7c8a0d7ce7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Sun, 19 Jul 2026 08:22:26 +0800 Subject: [PATCH 02/17] feat(convert): delay convert logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.Update.cpp | 9 +++++++++ src/Ext/Techno/Body.h | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index c0f3f68e66..2e1fca1184 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -26,6 +26,15 @@ void TechnoExt::OnEarlyUpdate() if (this->CheckDeathConditions()) return; + // Handle pending conversion + if (this->PendingConvertTechnoType) + { + if (const auto pFoot = abstract_cast(this->OwnerObject())) + TechnoExt::ConvertToType(pFoot, this->PendingConvertTechnoType); + + this->PendingConvertTechnoType = nullptr; + } + this->ApplyInterceptor(); } diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 0fb64fd69e..5c6c239362 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -88,6 +88,8 @@ class TechnoExt : public RadioExt, public Detach::Listener bool PreventCrewEscape; + TechnoTypeClass* PendingConvertTechnoType; + TechnoExt(TechnoClass* OwnerObject) : RadioExt(OwnerObject) , TypeExtData { nullptr } , Shield {} @@ -137,6 +139,7 @@ class TechnoExt : public RadioExt, public Detach::Listener , DropCrate { -1 } , DropCrateType { Powerup::Money } , PreventCrewEscape { false } + , PendingConvertTechnoType { nullptr } { } void OnEarlyUpdate(); From 45e7609a3de86cd79a2e8b7d4711dd1fba0044ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Mon, 20 Jul 2026 11:08:08 +0800 Subject: [PATCH 03/17] feat(convert): allow InfantryType deploy to building. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 35 ++++++++++++++++++++++++++++++ src/Ext/Techno/Body.h | 3 +++ src/Ext/Unit/Body.cpp | 27 ----------------------- src/Ext/Unit/Body.h | 3 --- src/Ext/Unit/Hooks.DeploysInto.cpp | 12 +++++----- src/Misc/Hooks.BugFixes.cpp | 4 ++-- 6 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 55d32c366c..d0cf42b13b 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -14,6 +14,8 @@ #include #include +FootClass* TechnoExt::Deployer = nullptr; + TechnoExt::~TechnoExt() { auto const pTypeExt = this->TypeExtData; @@ -538,6 +540,39 @@ void TechnoExt::TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClas } } +// Checks if vehicle can deploy into a building at its current location. If unit has no DeploysInto set returns noDeploysIntoDefaultValue (def = false) instead. +bool TechnoExt::CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue) +{ + if (!pThis) + return false; + + BuildingTypeClass* pDeployType = nullptr; + if (auto const pUnit = abstract_cast(pThis)) + pDeployType = pUnit->Type->DeploysInto; + + if (!pDeployType) + { + if (auto const pType = TechnoTypeExt::Fetch(pThis->GetTechnoType())->Convert_Deploy.Get()) + pDeployType = abstract_cast(pType); + } + + if (!pDeployType) + return noDeploysIntoDefaultValue; + + auto mapCoords = CellClass::Coord2Cell(pThis->GetCoords()); + + if (pDeployType->GetFoundationWidth() > 2 || pDeployType->GetFoundationHeight(false) > 2) + mapCoords += CellStruct { -1, -1 }; + + // The vanilla game used an inappropriate approach here, resulting in potential risk of desync. + // Now, through additional checks, we can directly exclude the unit who want to deploy. + TechnoExt::Deployer = pThis; + const bool canDeploy = pDeployType->CanCreateHere(mapCoords, pThis->Owner); + TechnoExt::Deployer = nullptr; + + return canDeploy; +} + bool TechnoExt::IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource) { if (!pThis || !pSource) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 5c6c239362..9ed7ea2b2e 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -215,6 +215,8 @@ class TechnoExt : public RadioExt, public Detach::Listener // deprecated stand-in for the pre-rework container of all TechnoClass extensions static inline CompatExtMap ExtMap {}; + static FootClass* Deployer; + static bool LoadGlobals(PhobosStreamReader& Stm); static bool SaveGlobals(PhobosStreamWriter& Stm); @@ -249,6 +251,7 @@ class TechnoExt : public RadioExt, public Detach::Listener static bool AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, TargetZoneScanType zoneScanType, WeaponTypeClass* pWeapon = nullptr, bool useZone = false, int zone = -1); static void UpdateAttachedAnimLayers(TechnoClass* pThis); static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); + static bool CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue = false); static void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo); static bool IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource); static int GetTintColor(TechnoClass* pThis, bool invulnerability, bool airstrike, bool berserk); diff --git a/src/Ext/Unit/Body.cpp b/src/Ext/Unit/Body.cpp index baac35745d..9ec400e47a 100644 --- a/src/Ext/Unit/Body.cpp +++ b/src/Ext/Unit/Body.cpp @@ -8,8 +8,6 @@ UnitExt::ExtContainer UnitExt::ExtMap; -UnitClass* UnitExt::Deployer = nullptr; - UnitExt::~UnitExt() { if (this->UndergroundTracked) @@ -317,31 +315,6 @@ bool UnitExt::SimpleDeployerAllowedToDeploy(UnitClass* pThis, bool defaultValue, return true; } -// Checks if vehicle can deploy into a building at its current location. If unit has no DeploysInto set returns noDeploysIntoDefaultValue (def = false) instead. -bool UnitExt::CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue) -{ - if (!pThis) - return false; - - auto const pDeployType = pThis->Type->DeploysInto; - - if (!pDeployType) - return noDeploysIntoDefaultValue; - - auto mapCoords = CellClass::Coord2Cell(pThis->GetCoords()); - - if (pDeployType->GetFoundationWidth() > 2 || pDeployType->GetFoundationHeight(false) > 2) - mapCoords += CellStruct { -1, -1 }; - - // The vanilla game used an inappropriate approach here, resulting in potential risk of desync. - // Now, through additional checks, we can directly exclude the unit who want to deploy. - UnitExt::Deployer = pThis; - const bool canDeploy = pDeployType->CanCreateHere(mapCoords, pThis->Owner); - UnitExt::Deployer = nullptr; - - return canDeploy; -} - UnitTypeClass* UnitExt::GetUnitTypeExtra(UnitClass* pUnit, UnitTypeExt* pData) { if (pUnit->IsGreenHP()) diff --git a/src/Ext/Unit/Body.h b/src/Ext/Unit/Body.h index f314b6ef7b..f1346a0a07 100644 --- a/src/Ext/Unit/Body.h +++ b/src/Ext/Unit/Body.h @@ -50,13 +50,10 @@ class UnitExt final : public FootExt void UpdateRecoilData(); void RecordRecoilData(); - static UnitClass* Deployer; - static bool CannotMove(UnitClass* pThis); static bool HasAmmoToDeploy(UnitClass* pThis); static void HandleOnDeployAmmoChange(UnitClass* pThis, int maxAmmoOverride = -1); static bool SimpleDeployerAllowedToDeploy(UnitClass* pThis, bool defaultValue, bool alwaysCheckLandTypes); - static bool CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue = false); static UnitTypeClass* GetUnitTypeExtra(UnitClass* pUnit, UnitTypeExt* pData); UnitClass* OwnerObject() const diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 473b91d001..aa628bc81f 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -83,7 +83,7 @@ DEFINE_HOOK(0x73FEC1, UnitClass_WhatAction_DeploysIntoDesyncFix, 0x6) GET(UnitClass* const, pThis, ESI); REF_STACK(Action, action, STACK_OFFSET(0x20, 0x8)); - if (!UnitExt::CanDeployIntoBuilding(pThis)) + if (!TechnoExt::CanDeployIntoBuilding(pThis)) action = Action::NoDeploy; return SkipGameCode; @@ -128,7 +128,7 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else if (pBuildingType->LaserFencePost || pBuildingType->Gate) { - bool skipFlag = UnitExt::Deployer ? UnitExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; + bool skipFlag = TechnoExt::Deployer ? TechnoExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; bool builtOnCanBeBuiltOn = false; for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) @@ -142,7 +142,7 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else if (pObject->AbstractFlags & AbstractFlags::Techno) { - if (pObject == UnitExt::Deployer) + if (pObject == TechnoExt::Deployer) { skipFlag = true; } @@ -177,14 +177,14 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else { - bool skipFlag = UnitExt::Deployer ? UnitExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; + bool skipFlag = TechnoExt::Deployer ? TechnoExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; bool builtOnCanBeBuiltOn = false; for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { if (pObject->AbstractFlags & AbstractFlags::Techno) { - if (pObject == UnitExt::Deployer) + if (pObject == TechnoExt::Deployer) skipFlag = true; else return CanNotExistHere; @@ -198,7 +198,7 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } } - if (!builtOnCanBeBuiltOn && (pCell->OccupationFlags & (skipFlag ? 0x1F : 0x3F))) + if (!builtOnCanBeBuiltOn && (pCell->OccupationFlags & (skipFlag ? 0x03 : 0x3F))) return CanNotExistHere; } diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 3282cf7d48..93ad9f7266 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -780,7 +780,7 @@ DEFINE_HOOK(0x4D580B, FootClass_ApproachTarget_DeployToFire, 0x6) GET(UnitClass*, pThis, EBX); - R->EAX(UnitExt::CanDeployIntoBuilding(pThis, true)); + R->EAX(TechnoExt::CanDeployIntoBuilding(pThis, true)); return SkipGameCode; } @@ -791,7 +791,7 @@ DEFINE_HOOK(0x741050, UnitClass_CanFire_DeployToFire, 0x6) GET(UnitClass*, pThis, ESI); - if (pThis->Type->DeployToFire && pThis->CanDeployNow() && !UnitExt::CanDeployIntoBuilding(pThis, true)) + if (pThis->Type->DeployToFire && pThis->CanDeployNow() && !TechnoExt::CanDeployIntoBuilding(pThis, true)) return MustDeploy; return SkipGameCode; From c1a93fe91e54f41e8eeaa5e285ac23b0d1ba770d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Sun, 19 Jul 2026 08:33:24 +0800 Subject: [PATCH 04/17] feat(convert): support any TechnoType to BuildingType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 84 ++++++++++++++++++++++++++++++++++++++++- src/Ext/Techno/Body.h | 1 + 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index d0cf42b13b..626223f70a 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -357,12 +357,17 @@ bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) const auto pType = pThis->GetTechnoType(); // It really should be at the beginning. - if (pType == pToType || pType->WhatAmI() != pToType->WhatAmI()) + if (pType == pToType) { Debug::Log("Incompatible types between %s and %s\n", pThis->get_ID(), pToType->get_ID()); return false; } + if (pType->WhatAmI() != pToType->WhatAmI()) + { + return ConvertToTypeEx(pThis, pToType); + } + if (AresFunctions::ConvertTypeTo) { if (AresFunctions::ConvertTypeTo(pThis, pToType)) @@ -473,6 +478,83 @@ bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) return true; } +bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) +{ + auto const pHouse = pThis->Owner; + auto location = pThis->GetCoords(); + auto const facing = pThis->PrimaryFacing.Current().GetDir(); + + // Save state before removing the vehicle + bool const wasSelected = pThis->IsSelected; + auto const savedMission = pThis->CurrentMission; + auto const savedDestination = pThis->Destination; + auto const savedTarget = pThis->Target; + switch (pToType->WhatAmI()) + { + case AbstractType::BuildingType: + { + if (!TechnoExt::CanDeployIntoBuilding(pThis, false)) + { + return false; + } + + auto const pBuildingType = abstract_cast(pToType); + + auto const pBuilding = static_cast(pBuildingType->CreateObject(pHouse)); + if (!pBuilding) + { + return false; + } + + TransferMindControlOnDeploy(pThis, pBuilding); + ShieldClass::SyncShieldToAnother(pThis, pBuilding); + TechnoExt::SyncInvulnerability(pThis, pBuilding); + AttachEffectClass::TransferAttachedEffects(pThis, pBuilding); + + pBuilding->QueueMission(Mission::Construction, false); + pBuilding->NextMission(); + + if (!pBuilding->ForceCreate(location)) + { + pBuilding->UnInit(); + Debug::Log("ConvertToTypeEx: ForceCreate failed for building %s\n", pBuildingType->get_ID()); + + return false; + } + + // Remove unit permanently + pThis->Limbo(); + pThis->UnInit(); + + if (!pBuildingType->LoadBuildup()) + { + pBuilding->BeginMode(BStateType::Idle); + pBuilding->QueueMission(Mission::Guard, false); + pBuilding->NextMission(); + pBuilding->Place(false); + } + + auto const pBuildingExt = BuildingExt::Fetch(pBuilding); + pBuildingExt->DeployedTechno = true; + + // Should we need to set destination? may make it undeployed. + // pBuilding->SetDestination(savedDestination, true); + // pBuilding->QueueMission(savedMission, false); + // pBuilding->NextMission(); + pBuilding->SetTarget(savedTarget); + + // Keep selection + if (wasSelected) + pBuilding->Select(); + + return true; + } + default: + Debug::Log("Incompatible types between %s and %s\n", pThis->get_ID(), pToType->get_ID()); + return false; + } +} + void TechnoExt::TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo) { const auto pAnimType = pTechnoFrom->MindControlRingAnim diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 9ed7ea2b2e..9456bc1225 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -251,6 +251,7 @@ class TechnoExt : public RadioExt, public Detach::Listener static bool AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, TargetZoneScanType zoneScanType, WeaponTypeClass* pWeapon = nullptr, bool useZone = false, int zone = -1); static void UpdateAttachedAnimLayers(TechnoClass* pThis); static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); + static bool ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* toType); static bool CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue = false); static void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo); static bool IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource); From d34aef7214e8ed1bfdc7e304447d51ee820db348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Sun, 19 Jul 2026 08:43:26 +0800 Subject: [PATCH 05/17] feat(convert): support BuildingType to other TechnoType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/BuildingType/Body.cpp | 2 ++ src/Ext/BuildingType/Body.h | 2 ++ src/Ext/Unit/Hooks.DeploysInto.cpp | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index a4fbc51d69..7526db5855 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -240,6 +240,7 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->BuildingRepairedSound.Read(exINI, pSection, "BuildingRepairedSound"); this->Refinery_UseStorage.Read(exINI, pSection, "Refinery.UseStorage"); this->UndeploysInto_Sellable.Read(exINI, pSection, "UndeploysInto.Sellable"); + this->UndeploysInto_ConvertTo.Read(exINI, pSection, "UndeploysInto.ConvertTo"); this->BuildingRadioLink_SyncOwner.Read(exINI, pSection, "BuildingRadioLink.SyncOwner"); this->GuardRetryDelay.Read(exINI, pSection, "GuardRetryDelay"); @@ -431,6 +432,7 @@ void BuildingTypeExt::Serialize(T& Stm) .Process(this->Refinery_UseNormalActiveAnim) .Process(this->HasPowerUpAnim) .Process(this->UndeploysInto_Sellable) + .Process(this->UndeploysInto_ConvertTo) .Process(this->BuildingRadioLink_SyncOwner) .Process(this->GuardRetryDelay) .Process(this->TurretAnim_IdleFrames) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 7b9650f29b..68a6a49d60 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -108,6 +108,7 @@ class BuildingTypeExt final : public TechnoTypeExt ValueableVector HasPowerUpAnim; Valueable UndeploysInto_Sellable; + Valueable UndeploysInto_ConvertTo; Nullable BuildingRadioLink_SyncOwner; @@ -212,6 +213,7 @@ class BuildingTypeExt final : public TechnoTypeExt , Refinery_UseNormalActiveAnim { false } , HasPowerUpAnim {} , UndeploysInto_Sellable { false } + , UndeploysInto_ConvertTo {} , BuildingRadioLink_SyncOwner {} , GuardRetryDelay {} , TurretAnim_IdleFrames { 1 } diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index aa628bc81f..5f83c6780e 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -40,6 +40,18 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) TechnoExt::SyncInvulnerability(pStructure, pUnit); AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); + auto const pTypeExt = BuildingTypeExt::Fetch(pStructure->Type); + + // If UndeploysInto.ConvertTo is set, defer unit conversion to next frame. + // The vehicle created by undeploying a building is not fully initialized at this point + // (possibly still in a limbo state). Converting it right away would cause the new unit + // to lose states such as the current target and selection status. + if (const auto pTechnoType = pTypeExt->UndeploysInto_ConvertTo) + { + auto const pUnitExt = TechnoExt::Fetch(pUnit); + pUnitExt->PendingConvertTechnoType = pTechnoType; + } + // This line will break the bahavior of UnDeploysInto buildings. However, it might serve a purpose that no one knows yet // Comment out the line instead of removing it for now, so we can turn to it if something related goes wrong in the future // pUnit->QueueMission(Mission::Hunt, true); From db102c606272364e37207d7ecd9607ab3fce377c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Sun, 19 Jul 2026 08:44:00 +0800 Subject: [PATCH 06/17] feat(convert): support convert to TechnoType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 626223f70a..cfb8df12b2 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -549,6 +549,52 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) return true; } + case AbstractType::InfantryType: + case AbstractType::UnitType: + case AbstractType::AircraftType: + { + if (auto const pUnit = static_cast(pToType->CreateObject(pHouse))) + { + TransferMindControlOnDeploy(pThis, pUnit); + ShieldClass::SyncShieldToAnother(pThis, pUnit); + TechnoExt::SyncInvulnerability(pThis, pUnit); + AttachEffectClass::TransferAttachedEffects(pThis, pUnit); + + // Remove old techno from map + pThis->Limbo(); + + // Place unit on map + if (!pUnit->Unlimbo(location, facing)) + { + // Failed to place unit, clean up + pUnit->UnInit(); + Debug::Log("ConvertToTypeEx: Failed to place unit %s at (%d,%d)\n", + pToType->get_ID(), location.X, location.Y); + + pThis->Unlimbo(location, facing); + if (wasSelected) + pThis->Select(); + + return false; + } + + pThis->UnInit(); + + // Carry over the vehicle's move order + pUnit->SetDestination(savedDestination, true); + pUnit->QueueMission(savedMission, false); + pUnit->NextMission(); + pUnit->SetTarget(savedTarget); + + // Keep selection + if (wasSelected) + pUnit->Select(); + + return true; + } + + return false; + } default: Debug::Log("Incompatible types between %s and %s\n", pThis->get_ID(), pToType->get_ID()); return false; From a1e692835947169ed3bca13383eec8ce2010d581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Sun, 19 Jul 2026 08:53:57 +0800 Subject: [PATCH 07/17] feat(deploy): Prevent deployment Command due to insufficient Building space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Unit/Hooks.DeployFire.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Ext/Unit/Hooks.DeployFire.cpp b/src/Ext/Unit/Hooks.DeployFire.cpp index 7e9bd4c1b8..a329ae282e 100644 --- a/src/Ext/Unit/Hooks.DeployFire.cpp +++ b/src/Ext/Unit/Hooks.DeployFire.cpp @@ -28,6 +28,21 @@ DEFINE_HOOK(0x4C77E4, EventClass_Execute_DeployCommand, 0x6) } } + if (auto const pFoot = abstract_cast(pThis)) + { + const auto pExt = TechnoExt::Fetch(pThis); + auto const pConvertToType = pExt->TypeExtData->Convert_Deploy.Get(); + if (pConvertToType + && pConvertToType->WhatAmI() == AbstractType::BuildingType + && !TechnoExt::CanDeployIntoBuilding(pFoot, false)) + { + if (pThis->IsOwnedByCurrentPlayer) + VoxClass::PlayIndex(VoxClass::FindIndex(GameStrings::EVA_CannotDeployHere)); + + return DoNotExecute; + } + } + return 0; } From 2f0170e2ef5471b84506ce7ac60da68dd2b87027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Sun, 19 Jul 2026 08:58:03 +0800 Subject: [PATCH 08/17] feat(deploy): show NoDeploy icon when insufficient building space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Unit/Hooks.DeploysInto.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 5f83c6780e..59dc6eafb1 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -73,6 +73,23 @@ DEFINE_HOOK(0x449E2E, BuildingClass_Mi_Selling_CreateUnit, 0x6) return 0x449E34; } +DEFINE_HOOK(0x7000DF, TechnoClass_WhatAction_Deploy, 0x5) +{ + GET(TechnoClass*, pThis, ESI); + + if (auto pInfantry = abstract_cast(pThis)) + { + if (!TechnoExt::CanDeployIntoBuilding(pInfantry, true)) + { + R->EAX(Action::NoDeploy); + + return 0x7000E4; + } + } + + return 0; +} + DEFINE_HOOK(0x7396AD, UnitClass_Deploy_CreateBuilding, 0x6) { GET(UnitClass*, pUnit, EBP); From c80192509fbba2b27509df82f61cc81364550941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Mon, 20 Jul 2026 11:33:07 +0800 Subject: [PATCH 09/17] docs(convert): update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- CREDITS.md | 4 ++++ docs/Fixed-or-Improved-Logics.md | 3 ++- docs/New-or-Enhanced-Logics.md | 11 +++++++++ docs/Whats-New.md | 3 +++ docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 12 ++++++++++ .../LC_MESSAGES/Fixed-or-Improved-Logics.po | 21 ++++++++++++++--- .../LC_MESSAGES/New-or-Enhanced-Logics.po | 20 ++++++++++++++++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 23 +++++++++++++++++++ 8 files changed, 93 insertions(+), 4 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 2eded00650..a14c5c2152 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -933,3 +933,7 @@ This page lists all the individual contributions to the project by their author. - Add `ClampToScreen` tag for `BannerType` to control whether banner position is clamped to the visible area - **obsidianus** - Automatic conversion based on health - **Nuke** - Reload speed adjustment on promotion +- **frg2089 (舰队的偶像-岛风酱!)**: + - Cross-category type conversion support + - `UndeploysInto.ConvertTo` for buildings + - `Convert.Deploy` NoDeploy feedback improvements diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 4c24525455..04f55de6ad 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -334,7 +334,8 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - Weapons fired by EMPulse superweapons *(Ares feature)* without `EMPulse.TargetSelf=true` can now create radiation. - Weapons fired by EMPulse superweapons *(Ares feature)* now respect `Floater` and Phobos-added `Gravity` setting. - `IsSimpleDeployer` units with Hover locomotor and `DeployToLand` no longer get stuck after deploying or play their move sound indefinitely. -- `Convert.Deploy` displays 'NoDeploy' cursor if the new type is not allowed to move to the cell due to `SpeedType` etc. +- All forms of type conversion (including Ares' and Phobos') now support converting between different TechnoType categories (e.g., infantry to building via `Convert.Deploy`, building to other TechnoTypes via `UndeploysInto.ConvertTo`). Previously, only same-category conversion was supported. +- `Convert.Deploy` displays 'NoDeploy' cursor and plays `EVA_CannotDeployHere` sound if the new type is not allowed to move to the cell due to `SpeedType` etc., or if the target is a `BuildingType` and there is insufficient building space at the current location. - All forms of type conversion (including Ares') now correctly update the warp-in delay if unit with teleport `Locomotor` was converted while the delay was active. - All forms of type conversion (including Ares') now correctly update `MoveSound` if a moving unit has their type changed. - All forms of type conversion (including Ares') now correctly update `OpenTopped` state of passengers in transport that is converted. diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 161c1e238a..d86c4056c3 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -755,6 +755,17 @@ SpyEffect.VictimSuperWeapon= ; SuperWeaponType SpyEffect.InfiltratorSuperWeapon= ; SuperWeaponType ``` +### UndeploysInto convert on undeploy + +- When a building with `UndeploysInto` undeploys, the resulting vehicle can now be converted to a different `TechnoType` on the next frame. For example, a deployed base can undeploy into an infantry unit instead of the original vehicle type. + - `UndeploysInto.ConvertTo` specifies the `TechnoType` to convert the vehicle into after undeployment. If not set, no conversion occurs. + +In `rulesmd.ini`: +```ini +[SOMEBUILDING] ; BuildingType +UndeploysInto.ConvertTo= ; TechnoType +``` + ## Infantry ### Allow infantry to perform type conversion when deploying and undeploying diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 1244b7256d..fc47f39399 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -54,6 +54,9 @@ This serves as a changelog for when you just need to drop the new version in wit - `[WarheadType] -> KillWeapon.OnFirer.AffectsHouses` -> `[WarheadType] -> KillWeapon.OnFirer.AffectsHouse` - `[WarheadType/SuperWeaponType] -> Convert(N).AffectedHouses` -> `[WarheadType/SuperWeaponType] -> Convert(N).AffectsHouse` - `[SuperWeaponType] -> LimboKill.Affected` -> `[SuperWeaponType] -> LimboKill.AffectsHouse` +- All forms of type conversion now support cross-category conversion (e.g., infantry to building via `Convert.Deploy`, building to other TechnoTypes via `UndeploysInto.ConvertTo`, etc.) (by frg2089) +- BuildingType with `UndeploysInto` can now convert the resulting vehicle to a specified `TechnoType` on undeploy via `UndeploysInto.ConvertTo` (by frg2089) +- `Convert.Deploy` now provides proper NoDeploy feedback when the target is a BuildingType with insufficient space (by frg2089) ```{note} - If it is detected that you are using the old INI flags, a warning will be outputted to `debug.log`. diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index cfb4aeaf71..c2d3afae1e 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3169,6 +3169,18 @@ msgid "" " being cleaned up" msgstr "修复了 `Locomotor` 为 `Fly`、`Jumpjet` 或 `Rocket` 的单位在地图外坠毁时未能清除的问题" +msgid "**frg2089 (舰队的偶像-岛风酱!)**:" +msgstr "**frg2089 (舰队的偶像-岛风酱!)**:" + +msgid "Cross-category type conversion support" +msgstr "跨类别类型转换支持" + +msgid "`UndeploysInto.ConvertTo` for buildings" +msgstr "建筑的 `UndeploysInto.ConvertTo`" + +msgid "`Convert.Deploy` NoDeploy feedback improvements" +msgstr "`Convert.Deploy` 的 NoDeploy 反馈改进" + msgid "**Chang_zhi**:" msgstr "**Chang_zhi**:" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po index 1acc98e0ae..961fdfff74 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po @@ -1710,9 +1710,24 @@ msgid "" msgstr "拥有 Hover 运动模式的 `IsSimpleDeployer` 且 `DeployToLand` 的单位在部署后不再会卡住或无限播放移动音效。" msgid "" -"`Convert.Deploy` displays 'NoDeploy' cursor if the new type is not " -"allowed to move to the cell due to `SpeedType` etc." -msgstr "当 `Convert.Deploy` 转换的目标单位由于 `SpeedType` 等原因无法移动到目标单元格时将会显示 `NoDeploy` 光标。" +"All forms of type conversion (including Ares' and Phobos') now support " +"converting between different TechnoType categories (e.g., infantry to " +"building via `Convert.Deploy`, building to other TechnoTypes via " +"`UndeploysInto.ConvertTo`). Previously, only same-category conversion was supported." +msgstr "" +"所有形式的类型转换(包括 Ares 和 Phobos 的)现在均支持跨 TechnoType 类别" +"转换(例如通过 `Convert.Deploy` 将步兵转为建筑,通过 `UndeploysInto.ConvertTo` " +"将建筑转为其他 TechnoType)。此前仅允许同类别之间的转换。" + +msgid "" +"`Convert.Deploy` displays 'NoDeploy' cursor and plays " +"`EVA_CannotDeployHere` sound if the new type is not allowed to move to " +"the cell due to `SpeedType` etc., or if the target is a `BuildingType` " +"and there is insufficient building space at the current location." +msgstr "" +"`Convert.Deploy` 转换后的新类型若因 `SpeedType` 等原因无法移动到目标格," +"或目标为 `BuildingType` 且当前位置建筑空间不足时,将显示 `NoDeploy` 光标并" +"播放 `EVA_CannotDeployHere` 音效。" msgid "" "All forms of type conversion (including Ares') now correctly update the " 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..f429cb2d1f 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 @@ -1884,6 +1884,26 @@ msgid "" "Super Weapon's owner being the owner of the spying unit." msgstr "`SpyEffect.InfiltratorSuperWeapon` 同上但超级武器的所属方来自间谍单位。" +msgid "UndeploysInto convert on undeploy" +msgstr "反部署时通过 UndeploysInto 转换类型" + +msgid "" +"When a building with `UndeploysInto` undeploys, the resulting vehicle can" +" now be converted to a different `TechnoType` on the next frame. For " +"example, a deployed base can undeploy into an infantry unit instead of " +"the original vehicle type." +msgstr "" +"当带有 `UndeploysInto` 的建筑反部署时,生成的载具会在下一帧被转换为指定的" +" `TechnoType`。例如,一个展开的基地反部署后可以变成一个步兵单位,而不再是" +"原来的载具类型。" + +msgid "" +"`UndeploysInto.ConvertTo` specifies the `TechnoType` to convert the " +"vehicle into after undeployment. If not set, no conversion occurs." +msgstr "" +"`UndeploysInto.ConvertTo` 用于指定反部署后载具所要转换成的 `TechnoType`。" +"若不设置,则不进行转换。" + msgid "Infantry" msgstr "步兵" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index a520782bf1..233a74ad96 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -336,6 +336,29 @@ msgstr "" "`[SuperWeaponType] -> LimboKill.Affected` -> `[SuperWeaponType] -> " "LimboKill.AffectsHouse`" +msgid "" +"All forms of type conversion now support cross-category conversion (e.g.," +" infantry to building via `Convert.Deploy`, building to other TechnoTypes via " +"`UndeploysInto.ConvertTo`, etc.) (by frg2089)" +msgstr "" +"所有类型转换现支持跨类别操作(例如通过 `Convert.Deploy` 将步兵转为建筑," +"通过 `UndeploysInto.ConvertTo` 将建筑转为其他 TechnoType 等)(by frg2089)" + +msgid "" +"BuildingType with `UndeploysInto` can now convert the resulting vehicle " +"to a specified `TechnoType` on undeploy via `UndeploysInto.ConvertTo` " +"(by frg2089)" +msgstr "" +"带有 `UndeploysInto` 的 BuildingType 反部署时可通过 `UndeploysInto.ConvertTo` " +"将生成的载具转换为指定的 `TechnoType`(by frg2089)" + +msgid "" +"`Convert.Deploy` now provides proper NoDeploy feedback when the target is" +" a BuildingType with insufficient space (by frg2089)" +msgstr "" +"`Convert.Deploy` 目标为 BuildingType 且空间不足时,现在会正确展示 NoDeploy " +"反馈(by frg2089)" + msgid "" "If it is detected that you are using the old INI flags, a warning will be" " outputted to `debug.log`." From 8e0f1c3213635545f627590153877e5bcd10c229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 14:29:16 +0800 Subject: [PATCH 10/17] fix: keep health percentage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index cfb8df12b2..fb4a68148b 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -506,6 +506,7 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) return false; } + pBuilding->SetHealthPercentage(pThis->GetHealthPercentage()); TransferMindControlOnDeploy(pThis, pBuilding); ShieldClass::SyncShieldToAnother(pThis, pBuilding); TechnoExt::SyncInvulnerability(pThis, pBuilding); @@ -555,6 +556,7 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) { if (auto const pUnit = static_cast(pToType->CreateObject(pHouse))) { + pUnit->SetHealthPercentage(pThis->GetHealthPercentage()); TransferMindControlOnDeploy(pThis, pUnit); ShieldClass::SyncShieldToAnother(pThis, pUnit); TechnoExt::SyncInvulnerability(pThis, pUnit); From b16ec97f7925805eef753824631dae292f57b858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 14:39:51 +0800 Subject: [PATCH 11/17] feat(convert): keep upgrade multiplier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 8 +++++++- src/Ext/Unit/Hooks.DeploysInto.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index fb4a68148b..5344ca8bcc 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -511,6 +511,9 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) ShieldClass::SyncShieldToAnother(pThis, pBuilding); TechnoExt::SyncInvulnerability(pThis, pBuilding); AttachEffectClass::TransferAttachedEffects(pThis, pBuilding); + pBuilding->ArmorMultiplier = pThis->ArmorMultiplier; + pBuilding->FirepowerMultiplier = pThis->FirepowerMultiplier; + // pBuilding->SpeedMultiplier = pThis->SpeedMultiplier; pBuilding->QueueMission(Mission::Construction, false); pBuilding->NextMission(); @@ -554,13 +557,16 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) case AbstractType::UnitType: case AbstractType::AircraftType: { - if (auto const pUnit = static_cast(pToType->CreateObject(pHouse))) + if (auto const pUnit = static_cast(pToType->CreateObject(pHouse))) { pUnit->SetHealthPercentage(pThis->GetHealthPercentage()); TransferMindControlOnDeploy(pThis, pUnit); ShieldClass::SyncShieldToAnother(pThis, pUnit); TechnoExt::SyncInvulnerability(pThis, pUnit); AttachEffectClass::TransferAttachedEffects(pThis, pUnit); + pUnit->ArmorMultiplier = pThis->ArmorMultiplier; + pUnit->FirepowerMultiplier = pThis->FirepowerMultiplier; + pUnit->SpeedMultiplier = pThis->SpeedMultiplier; // Remove old techno from map pThis->Limbo(); diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 59dc6eafb1..b511cdbe90 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -26,6 +26,9 @@ DEFINE_HOOK(0x739956, UnitClass_Deploy_Transfer, 0x6) ShieldClass::SyncShieldToAnother(pUnit, pStructure); TechnoExt::SyncInvulnerability(pUnit, pStructure); AttachEffectClass::TransferAttachedEffects(pUnit, pStructure); + pStructure->ArmorMultiplier = pUnit->ArmorMultiplier; + pStructure->FirepowerMultiplier = pUnit->FirepowerMultiplier; + // pStructure->SpeedMultiplier = pUnit->SpeedMultiplier; return 0; } @@ -39,6 +42,9 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) ShieldClass::SyncShieldToAnother(pStructure, pUnit); TechnoExt::SyncInvulnerability(pStructure, pUnit); AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); + pUnit->ArmorMultiplier = pStructure->ArmorMultiplier; + pUnit->FirepowerMultiplier = pStructure->FirepowerMultiplier; + // pUnit->SpeedMultiplier = pStructure->SpeedMultiplier; auto const pTypeExt = BuildingTypeExt::Fetch(pStructure->Type); From d5a95ceed64859b869ce55b18215bc34ba857443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 14:54:15 +0800 Subject: [PATCH 12/17] docs: update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- CREDITS.md | 2 ++ docs/Fixed-or-Improved-Logics.md | 2 ++ docs/Whats-New.md | 2 ++ docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 6 ++++++ .../zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po | 11 +++++++++++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 9 +++++++++ 6 files changed, 32 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index a14c5c2152..beaa7b9b38 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -937,3 +937,5 @@ This page lists all the individual contributions to the project by their author. - Cross-category type conversion support - `UndeploysInto.ConvertTo` for buildings - `Convert.Deploy` NoDeploy feedback improvements + - Health percentage preservation during type conversion + - Upgrade multiplier (`ArmorMultiplier`, `FirepowerMultiplier`) preservation during type conversion diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 04f55de6ad..489cff9ab7 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -339,6 +339,8 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - All forms of type conversion (including Ares') now correctly update the warp-in delay if unit with teleport `Locomotor` was converted while the delay was active. - All forms of type conversion (including Ares') now correctly update `MoveSound` if a moving unit has their type changed. - All forms of type conversion (including Ares') now correctly update `OpenTopped` state of passengers in transport that is converted. +- All forms of type conversion (including Ares') now correctly preserve health percentage of the converted unit. +- All forms of type conversion (including Ares') now correctly preserve upgrade multipliers (`ArmorMultiplier`, `FirepowerMultiplier`) of the converted unit. - Fixed an issue introduced by Ares that caused building `ActiveAnim` to be incorrectly restored while `SpecialAnim` was playing and the building was sold, erased or destroyed. - Fixed Ares' Abductor weapon leaves permanent placement stats when abducting moving vehicles. - Suppressed Ares' swizzle warning when parsing `Tags` and `TaskForces` (typically begin with `[Developer fatal]Pointer 00000000 declared change to both`). diff --git a/docs/Whats-New.md b/docs/Whats-New.md index fc47f39399..a9ea1c8552 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -57,6 +57,8 @@ This serves as a changelog for when you just need to drop the new version in wit - All forms of type conversion now support cross-category conversion (e.g., infantry to building via `Convert.Deploy`, building to other TechnoTypes via `UndeploysInto.ConvertTo`, etc.) (by frg2089) - BuildingType with `UndeploysInto` can now convert the resulting vehicle to a specified `TechnoType` on undeploy via `UndeploysInto.ConvertTo` (by frg2089) - `Convert.Deploy` now provides proper NoDeploy feedback when the target is a BuildingType with insufficient space (by frg2089) +- All forms of type conversion now preserve health percentage (by frg2089) +- All forms of type conversion now preserve upgrade multipliers (`ArmorMultiplier`, `FirepowerMultiplier`) (by frg2089) ```{note} - If it is detected that you are using the old INI flags, a warning will be outputted to `debug.log`. diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index c2d3afae1e..28ba75b838 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3181,6 +3181,12 @@ msgstr "建筑的 `UndeploysInto.ConvertTo`" msgid "`Convert.Deploy` NoDeploy feedback improvements" msgstr "`Convert.Deploy` 的 NoDeploy 反馈改进" +msgid "Health percentage preservation during type conversion" +msgstr "类型转换时保留生命值百分比" + +msgid "Upgrade multiplier (`ArmorMultiplier`, `FirepowerMultiplier`) preservation during type conversion" +msgstr "类型转换时保留升级乘数(`ArmorMultiplier`、`FirepowerMultiplier`)" + msgid "**Chang_zhi**:" msgstr "**Chang_zhi**:" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po index 961fdfff74..fcde356358 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po @@ -1745,6 +1745,17 @@ msgid "" "`OpenTopped` state of passengers in transport that is converted." msgstr "现在所有单位转换形式(包括 Ares 的)在运输工具被转换时可以正确更新乘客的 `OpenTopped` 状态。" +msgid "" +"All forms of type conversion (including Ares') now correctly preserve " +"health percentage of the converted unit." +msgstr "现在所有单位转换形式(包括 Ares 的)可以正确保留被转换单位的生命值百分比。" + +msgid "" +"All forms of type conversion (including Ares') now correctly preserve " +"upgrade multipliers (`ArmorMultiplier`, `FirepowerMultiplier`) of the " +"converted unit." +msgstr "现在所有单位转换形式(包括 Ares 的)可以正确保留被转换单位的升级乘数(`ArmorMultiplier`、`FirepowerMultiplier`)。" + msgid "" "Fixed an issue introduced by Ares that caused building `ActiveAnim` to be" " incorrectly restored while `SpecialAnim` was playing and the building " diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 233a74ad96..9430519662 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -359,6 +359,15 @@ msgstr "" "`Convert.Deploy` 目标为 BuildingType 且空间不足时,现在会正确展示 NoDeploy " "反馈(by frg2089)" +msgid "" +"All forms of type conversion now preserve health percentage (by frg2089)" +msgstr "所有形式的类型转换现会保留生命值百分比(by frg2089)" + +msgid "" +"All forms of type conversion now preserve upgrade multipliers " +"(`ArmorMultiplier`, `FirepowerMultiplier`) (by frg2089)" +msgstr "所有形式的类型转换现会保留升级乘数(`ArmorMultiplier`、`FirepowerMultiplier`)(by frg2089)" + msgid "" "If it is detected that you are using the old INI flags, a warning will be" " outputted to `debug.log`." From 64c3f3772b735a2a65abde950b492f2ec2978941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 15:16:48 +0800 Subject: [PATCH 13/17] refactor(convert): SyncStatus method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 35 +++++++++++++++++++---------------- src/Ext/Techno/Body.h | 1 + 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 5344ca8bcc..24f445c855 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -193,6 +193,21 @@ bool TechnoExt::HasAdditionalAbility(TechnoClass* pThis, AdditionalAbility abili return pTypeExt->AdditionalVeteranAbilities.test(index); } +void TechnoExt::SyncStatus(TechnoClass* pFrom, TechnoClass* pTo) +{ + pTo->SetHealthPercentage(pFrom->GetHealthPercentage()); + pTo->Veterancy = pFrom->Veterancy; + TransferMindControlOnDeploy(pFrom, pTo); + ShieldClass::SyncShieldToAnother(pFrom, pTo); + TechnoExt::SyncInvulnerability(pFrom, pTo); + AttachEffectClass::TransferAttachedEffects(pFrom, pTo); + pTo->ArmorMultiplier = pFrom->ArmorMultiplier; + pTo->FirepowerMultiplier = pFrom->FirepowerMultiplier; + if (auto const pFromFoot = abstract_cast(pFrom)) + if (auto const pToFoot = abstract_cast(pTo)) + pToFoot->SpeedMultiplier = pFromFoot->SpeedMultiplier; +} + double TechnoExt::GetCurrentSpeedMultiplier(FootClass* pThis) { double houseMultiplier = 1.0; @@ -506,14 +521,7 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) return false; } - pBuilding->SetHealthPercentage(pThis->GetHealthPercentage()); - TransferMindControlOnDeploy(pThis, pBuilding); - ShieldClass::SyncShieldToAnother(pThis, pBuilding); - TechnoExt::SyncInvulnerability(pThis, pBuilding); - AttachEffectClass::TransferAttachedEffects(pThis, pBuilding); - pBuilding->ArmorMultiplier = pThis->ArmorMultiplier; - pBuilding->FirepowerMultiplier = pThis->FirepowerMultiplier; - // pBuilding->SpeedMultiplier = pThis->SpeedMultiplier; + SyncStatus(pThis, pBuilding); pBuilding->QueueMission(Mission::Construction, false); pBuilding->NextMission(); @@ -559,14 +567,7 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) { if (auto const pUnit = static_cast(pToType->CreateObject(pHouse))) { - pUnit->SetHealthPercentage(pThis->GetHealthPercentage()); - TransferMindControlOnDeploy(pThis, pUnit); - ShieldClass::SyncShieldToAnother(pThis, pUnit); - TechnoExt::SyncInvulnerability(pThis, pUnit); - AttachEffectClass::TransferAttachedEffects(pThis, pUnit); - pUnit->ArmorMultiplier = pThis->ArmorMultiplier; - pUnit->FirepowerMultiplier = pThis->FirepowerMultiplier; - pUnit->SpeedMultiplier = pThis->SpeedMultiplier; + SyncStatus(pThis, pUnit); // Remove old techno from map pThis->Limbo(); @@ -589,6 +590,8 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) pThis->UnInit(); // Carry over the vehicle's move order + pUnit->QueueMission(Mission::Guard, false); + pUnit->NextMission(); pUnit->SetDestination(savedDestination, true); pUnit->QueueMission(savedMission, false); pUnit->NextMission(); diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 9456bc1225..a1b33b5925 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -247,6 +247,7 @@ class TechnoExt : public RadioExt, public Detach::Listener static void DrawInsignia(TechnoClass* pThis, Point2D* pLocation, RectangleStruct* pBounds); static void ApplyGainedSelfHeal(TechnoClass* pThis); static void SyncInvulnerability(TechnoClass* pFrom, TechnoClass* pTo); + static void SyncStatus(TechnoClass* pFrom, TechnoClass* pTo); static CoordStruct PassengerKickOutLocation(TechnoClass* pThis, FootClass* pPassenger, int maxAttempts); static bool AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, TargetZoneScanType zoneScanType, WeaponTypeClass* pWeapon = nullptr, bool useZone = false, int zone = -1); static void UpdateAttachedAnimLayers(TechnoClass* pThis); From e12fa6e8623bc27e72f8a32672c3fc1b8a342853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 16:06:50 +0800 Subject: [PATCH 14/17] docs: update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- CREDITS.md | 1 + docs/Fixed-or-Improved-Logics.md | 1 + docs/Whats-New.md | 1 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 3 +++ docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po | 5 +++++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 5 +++++ 6 files changed, 16 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index beaa7b9b38..fa92a0fbe2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -939,3 +939,4 @@ This page lists all the individual contributions to the project by their author. - `Convert.Deploy` NoDeploy feedback improvements - Health percentage preservation during type conversion - Upgrade multiplier (`ArmorMultiplier`, `FirepowerMultiplier`) preservation during type conversion + - Veterancy preservation during type conversion diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 489cff9ab7..5e53ecffd6 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -341,6 +341,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - All forms of type conversion (including Ares') now correctly update `OpenTopped` state of passengers in transport that is converted. - All forms of type conversion (including Ares') now correctly preserve health percentage of the converted unit. - All forms of type conversion (including Ares') now correctly preserve upgrade multipliers (`ArmorMultiplier`, `FirepowerMultiplier`) of the converted unit. +- All forms of type conversion (including Ares') now correctly preserve veterancy (rank/experience) of the converted unit. - Fixed an issue introduced by Ares that caused building `ActiveAnim` to be incorrectly restored while `SpecialAnim` was playing and the building was sold, erased or destroyed. - Fixed Ares' Abductor weapon leaves permanent placement stats when abducting moving vehicles. - Suppressed Ares' swizzle warning when parsing `Tags` and `TaskForces` (typically begin with `[Developer fatal]Pointer 00000000 declared change to both`). diff --git a/docs/Whats-New.md b/docs/Whats-New.md index a9ea1c8552..f80495df79 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -59,6 +59,7 @@ This serves as a changelog for when you just need to drop the new version in wit - `Convert.Deploy` now provides proper NoDeploy feedback when the target is a BuildingType with insufficient space (by frg2089) - All forms of type conversion now preserve health percentage (by frg2089) - All forms of type conversion now preserve upgrade multipliers (`ArmorMultiplier`, `FirepowerMultiplier`) (by frg2089) +- All forms of type conversion now preserve veterancy (rank/experience) (by frg2089) ```{note} - If it is detected that you are using the old INI flags, a warning will be outputted to `debug.log`. diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 28ba75b838..07132e5584 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3187,6 +3187,9 @@ msgstr "类型转换时保留生命值百分比" msgid "Upgrade multiplier (`ArmorMultiplier`, `FirepowerMultiplier`) preservation during type conversion" msgstr "类型转换时保留升级乘数(`ArmorMultiplier`、`FirepowerMultiplier`)" +msgid "Veterancy preservation during type conversion" +msgstr "类型转换时保留军衔(rank/experience)" + msgid "**Chang_zhi**:" msgstr "**Chang_zhi**:" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po index fcde356358..69e61c609f 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po @@ -1756,6 +1756,11 @@ msgid "" "converted unit." msgstr "现在所有单位转换形式(包括 Ares 的)可以正确保留被转换单位的升级乘数(`ArmorMultiplier`、`FirepowerMultiplier`)。" +msgid "" +"All forms of type conversion (including Ares') now correctly preserve " +"veterancy (rank/experience) of the converted unit." +msgstr "现在所有单位转换形式(包括 Ares 的)可以正确保留被转换单位的军衔(rank/experience)。" + msgid "" "Fixed an issue introduced by Ares that caused building `ActiveAnim` to be" " incorrectly restored while `SpecialAnim` was playing and the building " diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 9430519662..1822de4c76 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -368,6 +368,11 @@ msgid "" "(`ArmorMultiplier`, `FirepowerMultiplier`) (by frg2089)" msgstr "所有形式的类型转换现会保留升级乘数(`ArmorMultiplier`、`FirepowerMultiplier`)(by frg2089)" +msgid "" +"All forms of type conversion now preserve veterancy (rank/experience) " +"(by frg2089)" +msgstr "所有形式的类型转换现会保留军衔(rank/experience)(by frg2089)" + msgid "" "If it is detected that you are using the old INI flags, a warning will be" " outputted to `debug.log`." From 090802e3cf246dd8f021da8af5b02842dab07fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 17:04:53 +0800 Subject: [PATCH 15/17] fix(convert): use SyncStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Unit/Hooks.DeploysInto.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index b511cdbe90..5380bee56d 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -22,13 +22,7 @@ DEFINE_HOOK(0x739956, UnitClass_Deploy_Transfer, 0x6) GET(UnitClass*, pUnit, EBP); GET(BuildingClass*, pStructure, EBX); - TechnoExt::TransferMindControlOnDeploy(pUnit, pStructure); - ShieldClass::SyncShieldToAnother(pUnit, pStructure); - TechnoExt::SyncInvulnerability(pUnit, pStructure); - AttachEffectClass::TransferAttachedEffects(pUnit, pStructure); - pStructure->ArmorMultiplier = pUnit->ArmorMultiplier; - pStructure->FirepowerMultiplier = pUnit->FirepowerMultiplier; - // pStructure->SpeedMultiplier = pUnit->SpeedMultiplier; + TechnoExt::SyncStatus(pUnit, pStructure); return 0; } @@ -38,13 +32,7 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) GET(BuildingClass*, pStructure, EBP); GET(UnitClass*, pUnit, EBX); - TechnoExt::TransferMindControlOnDeploy(pStructure, pUnit); - ShieldClass::SyncShieldToAnother(pStructure, pUnit); - TechnoExt::SyncInvulnerability(pStructure, pUnit); - AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); - pUnit->ArmorMultiplier = pStructure->ArmorMultiplier; - pUnit->FirepowerMultiplier = pStructure->FirepowerMultiplier; - // pUnit->SpeedMultiplier = pStructure->SpeedMultiplier; + TechnoExt::SyncStatus(pStructure, pUnit); auto const pTypeExt = BuildingTypeExt::Fetch(pStructure->Type); From 7d9fb41e44fae63d5ba781963cba2622a9167433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Tue, 21 Jul 2026 17:52:44 +0800 Subject: [PATCH 16/17] fix(convert): Inspection for removing redundancy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 24f445c855..1662d559f4 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -508,16 +508,14 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) { case AbstractType::BuildingType: { - if (!TechnoExt::CanDeployIntoBuilding(pThis, false)) - { - return false; - } - auto const pBuildingType = abstract_cast(pToType); auto const pBuilding = static_cast(pBuildingType->CreateObject(pHouse)); if (!pBuilding) { + pThis->Mission_Stop(); + if (wasSelected) + pThis->Select(); return false; } From 7eeaa245f618cc9b8abedc91022f170cc8a4b15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Fri, 14 Aug 2026 15:27:31 +0800 Subject: [PATCH 17/17] fix(deploy): place converted building centered on deploying foot Align the actual building placement in ConvertToTypeEx with the deploy cursor check so that the building ends up centered on the deploying unit. Extract the shared top-left cell computation into BuildingTypeExt::GetDeployBuildingTopLeftCell, mirroring the vanilla deploy logic which offsets by (-1,-1) for foundations larger than 2x2. --- src/Ext/BuildingType/Body.cpp | 14 ++++++++++++++ src/Ext/BuildingType/Body.h | 1 + src/Ext/Techno/Body.cpp | 11 +++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 7526db5855..1b6314e03c 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -149,6 +149,20 @@ int BuildingTypeExt::GetUpgradesAmount(BuildingTypeClass* pBuilding, HouseClass* return isUpgrade ? result : -1; } +// Computes the top-left cell of the building that would be placed if the given +// foot deployed into the given building type at its current location. Mirrors the +// vanilla deploy logic, which offsets the top-left cell by (-1,-1) for any +// foundation larger than 2x2. +CellStruct BuildingTypeExt::GetDeployBuildingTopLeftCell(BuildingTypeClass* pBuilding, const CellStruct& cell) +{ + auto mapCoords = cell; + + if (pBuilding->GetFoundationWidth() > 2 || pBuilding->GetFoundationHeight(false) > 2) + mapCoords += CellStruct { -1, -1 }; + + return mapCoords; +} + void BuildingTypeExt::Initialize() { TechnoTypeExt::Initialize(); diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 68a6a49d60..429778a8e6 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -292,5 +292,6 @@ class BuildingTypeExt final : public TechnoTypeExt static bool CanUpgrade(BuildingClass* pBuilding, BuildingTypeClass* pUpgradeType, HouseClass* pUpgradeOwner); static int CountOwnedNowWithDeployOrUpgrade(BuildingTypeClass* pBuilding, HouseClass* pHouse); static int GetUpgradesAmount(BuildingTypeClass* pBuilding, HouseClass* pHouse); + static CellStruct GetDeployBuildingTopLeftCell(BuildingTypeClass* pBuilding, const CellStruct &cell); }; diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 1662d559f4..cec1f886fb 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -524,6 +524,10 @@ bool TechnoExt::ConvertToTypeEx(FootClass* pThis, TechnoTypeClass* pToType) pBuilding->QueueMission(Mission::Construction, false); pBuilding->NextMission(); + // Place the building centered on the converting foot, matching the deploy cursor check. + const auto deployCell = BuildingTypeExt::GetDeployBuildingTopLeftCell(pBuildingType, CellClass::Coord2Cell(location)); + location = CellClass::Cell2Coord(deployCell, location.Z); + if (!pBuilding->ForceCreate(location)) { pBuilding->UnInit(); @@ -696,10 +700,9 @@ bool TechnoExt::CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaul if (!pDeployType) return noDeploysIntoDefaultValue; - auto mapCoords = CellClass::Coord2Cell(pThis->GetCoords()); - - if (pDeployType->GetFoundationWidth() > 2 || pDeployType->GetFoundationHeight(false) > 2) - mapCoords += CellStruct { -1, -1 }; + const auto mapCoords = BuildingTypeExt::GetDeployBuildingTopLeftCell( + pDeployType, + CellClass::Coord2Cell(pThis->GetCoords())); // The vanilla game used an inappropriate approach here, resulting in potential risk of desync. // Now, through additional checks, we can directly exclude the unit who want to deploy.