From 720b657cf09f461b3f40ac22d60544797d4f4c5f 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, 23 Aug 2026 12:06:49 +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 32c92a5f7f15a20bf0b4bcfa8cdaf4eed74e76e8 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, 23 Aug 2026 12:06:49 +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 93e6931649..5148525e55 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 5bf704a96361a4bc4d73aebdc8a772ac97418d64 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, 23 Aug 2026 12:06:49 +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 77946f12a5..f6f64d17ee 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 9e3739278253b8b15e950abe58cd3592b663d283 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, 23 Aug 2026 12:06:49 +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 adb44f79486df020a1d809e86fdf1a61fbd0aad8 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, 23 Aug 2026 12:06:49 +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 b3e48e1a32..ac18b4e58b 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -254,6 +254,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"); @@ -457,6 +458,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 26d994247c..898d025146 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 2f7673be1edb398c3942c6d04b2a5dc3e895d716 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, 23 Aug 2026 12:06:49 +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 8e94aae4f8928e269640c896080352600418c6b1 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, 23 Aug 2026 12:06:50 +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 78c288bb80a87b3cec407d4da6a1342537901282 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, 23 Aug 2026 12:06:50 +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 98197e42ea99e2d4f534a6db43101ca06fa6a2db 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, 23 Aug 2026 12:07:00 +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 | 3 +++ 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, 92 insertions(+), 4 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 9ac61c0ff4..f1c4655946 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -943,3 +943,6 @@ 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 + - 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 01541723e6..b9cd0650d9 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -337,7 +337,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 9c25519340..c9eb3a1844 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -759,6 +759,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 c48a236256..27f3b70d12 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -51,6 +51,9 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] - `[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 2b39f66985..cc7155ee90 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3187,6 +3187,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 11d946b71a..b52b42ed4e 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 @@ -1732,9 +1732,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 478f017abd..d4bd028534 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 @@ -1887,6 +1887,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 9b3de7af5a..c3d36abd7c 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -280,6 +280,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 ed98d1d79f9b46fa38aae48ea6d620b0caf64fdb 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, 23 Aug 2026 12:07:00 +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 a62cf9ce2ce2194f6bc53a92bb8bc7db95696e24 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, 23 Aug 2026 12:07:01 +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 f9dadef57e443d95fdd2b67ed15e8a1671588b3f 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, 23 Aug 2026 12:07:01 +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 f1c4655946..e826b35ba3 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -946,3 +946,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 b9cd0650d9..bca9d4fa46 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -342,6 +342,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 27f3b70d12..f34c87a78e 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -54,6 +54,8 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] - 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 cc7155ee90..bae7625df2 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3199,6 +3199,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 b52b42ed4e..77430f018f 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 @@ -1767,6 +1767,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 c3d36abd7c..f7e333bdd8 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -303,6 +303,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 70d6a6d90a13943a13d7fc13b01796f3e067834c 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, 23 Aug 2026 12:07:01 +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 96ffd31ed5dc511a46affd4d6fd4f2ace1c58f01 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, 23 Aug 2026 12:07:01 +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 e826b35ba3..1516745f8f 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -948,3 +948,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 bca9d4fa46..9024da8679 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -344,6 +344,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 f34c87a78e..3ca982fd8a 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -56,6 +56,7 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] - `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 bae7625df2..6c360bda77 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3205,6 +3205,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 77430f018f..0adbc92f16 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 @@ -1778,6 +1778,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 f7e333bdd8..9b1dca3402 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -312,6 +312,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 2ffbf5a7acabae1bd2a275c1312afd0ff1dbf7ed 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, 23 Aug 2026 12:07:01 +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 d2b3e9bc8cbedb3e4b4bc6c5026916bd45937623 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, 23 Aug 2026 12:07:01 +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 ec8d3a2e72bf4573740298c2aeb415303cf8a988 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, 23 Aug 2026 12:07:01 +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 ac18b4e58b..16a2ca11dc 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -163,6 +163,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 898d025146..f2222cda95 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -293,5 +293,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.