From c0c515637de4937c1b8e6407568bd233424438d3 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: Thu, 15 Oct 2026 09:01:16 +0800 Subject: [PATCH 1/9] feat(xconvert): allow converting a techno into any other type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conversion was limited to types of the same class, and cross-class changes had to go through Ares. TechnoExt::ConvertToType now handles every combination: it creates the target object, hands over what can be carried across classes (target, destination, occupants, deploy state and the whole TechnoExt status including attached effects, shields and mind control), places the new object and unlinks the source. The conversion is split into two steps so callers can ask before anything is created: * TechnoExt::CanConvertToType does the checks without any side effect - both types have to differ, a building source must not be busy with its own mission, and a building target needs its whole foundation free where the source stands. * TechnoExt::ConvertToType performs the conversion and runs the same check again, so no caller can bypass it. A failed check no longer leaves a half-created object behind. Building placement is shared with the deploy checks: TechnoExt::CanCreateBuildingHere computes the top-left cell of the foundation and asks CanCreateHere while FootExt::Deployer marks the source, which FootExt::CanDeployIntoBuilding now reuses instead of duplicating it. ConvertToType_Phobos, the Ares converter bridge, TypeConvertGroup, the warhead Convert.Apply pair and the auto-convert actions all take a TechnoClass now, so buildings can be converted too; HealthAutoConvertActions moved from FootExt to TechnoExt for the same reason. Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/BuildingType/Body.cpp | 14 + src/Ext/BuildingType/Body.h | 1 + src/Ext/Foot/Body.cpp | 30 +- src/Ext/Foot/Body.h | 3 +- src/Ext/House/Hooks.cpp | 23 +- src/Ext/Techno/Body.cpp | 278 ++++++++++++++++++- src/Ext/Techno/Body.h | 8 +- src/Ext/Techno/Hooks.cpp | 5 +- src/Ext/Unit/Body.cpp | 27 -- src/Ext/Unit/Body.h | 3 - src/Ext/Unit/Hooks.DeployFire.cpp | 15 + src/Ext/Unit/Hooks.DeploysInto.cpp | 135 ++++----- src/Ext/WarheadType/Detonate.cpp | 7 +- src/Interop/TechnoExt.cpp | 2 +- src/Interop/TechnoExt.h | 4 +- src/Misc/Hooks.Ares.cpp | 5 +- src/Misc/Hooks.BugFixes.cpp | 4 +- src/New/Type/Affiliated/TypeConvertGroup.cpp | 20 +- src/New/Type/Affiliated/TypeConvertGroup.h | 2 +- 19 files changed, 414 insertions(+), 172 deletions(-) diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 640fe6f751..67c5e380bf 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::GetBuildingTopLeftCellFromDeployCell(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 86e830bb01..2b86dcbefc 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -337,5 +337,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 GetBuildingTopLeftCellFromDeployCell(BuildingTypeClass* pBuilding, const CellStruct &cell); }; diff --git a/src/Ext/Foot/Body.cpp b/src/Ext/Foot/Body.cpp index fa05cccb58..86f2cee8fc 100644 --- a/src/Ext/Foot/Body.cpp +++ b/src/Ext/Foot/Body.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -912,23 +913,28 @@ void FootExt::AmmoAutoConvertActions() TechnoExt::ConvertToType(pThis, pTypeExt->Ammo_AutoConvertType); } -void FootExt::HealthAutoConvertActions() +// Checks if vehicle can deploy into a building at its current location. If unit has no DeploysInto set returns noDeploysIntoDefaultValue (def = false) instead. +bool FootExt::CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue) { - const auto pTypeExt = this->TypeExtData; - - if (!pTypeExt->Convert_Health.isset()) - return; + if (!pThis) + return false; - const double min = pTypeExt->Convert_Health_AbovePercent; - const double max = pTypeExt->Convert_Health_BelowPercent; + BuildingTypeClass* pDeployType = nullptr; + if (auto const pUnit = abstract_cast(pThis)) + pDeployType = pUnit->Type->DeploysInto; - if (min < 0 && max < 0) - return; + if (!pDeployType) + { + if (auto const pType = TechnoTypeExt::Fetch(pThis->GetTechnoType())->Convert_Deploy.Get()) + pDeployType = abstract_cast(pType); + } - const auto pThis = this->OwnerObject(); + if (!pDeployType) + return noDeploysIntoDefaultValue; - if (TechnoExt::IsHealthInThreshold(pThis, min, max)) - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Health); + // 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. + return TechnoExt::CanCreateBuildingHere(pThis, pDeployType); } // ============================= diff --git a/src/Ext/Foot/Body.h b/src/Ext/Foot/Body.h index 4a0488ddaa..ab73d798ea 100644 --- a/src/Ext/Foot/Body.h +++ b/src/Ext/Foot/Body.h @@ -68,12 +68,13 @@ class FootExt : public TechnoExt void UpdateOnTunnelEnter(); void UpdateOnTunnelExit(); void UpdateTypeData(TechnoTypeClass* pCurrentType); - void HealthAutoConvertActions(); void AmmoAutoConvertActions(); virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; + static bool CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue = false); + private: template void Serialize(T& Stm); diff --git a/src/Ext/House/Hooks.cpp b/src/Ext/House/Hooks.cpp index 70c6690e4c..a1a945fcbb 100644 --- a/src/Ext/House/Hooks.cpp +++ b/src/Ext/House/Hooks.cpp @@ -276,25 +276,22 @@ DEFINE_HOOK(0x7015C9, TechnoClass_Captured_UpdateTracking, 0x6) pNewOwnerExt->OwnedCountedHarvesters.push_back(pThis); } - if (pFoot) + if (humanAndComputer) { - if (humanAndComputer) + if (const auto pConvertTo = I_am_human + ? pTypeExt->Convert_HumanToComputer.Get() + : pTypeExt->Convert_ComputerToHuman.Get()) { - if (const auto pConvertTo = I_am_human - ? pTypeExt->Convert_HumanToComputer.Get() - : pTypeExt->Convert_ComputerToHuman.Get()) - { - TechnoExt::ConvertToType(pFoot, pConvertTo); - } - - if (!I_am_human) - TechnoExt::ChangeOwnerMissionFix(pFoot, pType); + TechnoExt::ConvertToType(pThis, pConvertTo); } - pThis->Owner->RecheckTechTree = true; - pNewOwner->RecheckTechTree = true; + if (pFoot && !I_am_human) + TechnoExt::ChangeOwnerMissionFix(pFoot, pType); } + pThis->Owner->RecheckTechTree = true; + pNewOwner->RecheckTechTree = true; + for (const auto& pTrail : pExt->LaserTrails) { if (pTrail->Type->IsHouseColor) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 564b7a4d1b..3b3ca32ef0 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -1,6 +1,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -348,19 +351,179 @@ bool TechnoExt::AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, Ta return true; } +// Can a building of the given type be created where pFromTechno currently stands? The source +// itself is excluded from the placement check through TechnoExt::Deployer, which +// CellClass::CanThisExistHere understands. +bool TechnoExt::CanCreateBuildingHere(TechnoClass* pFromTechno, BuildingTypeClass* pToBuildingType) +{ + if (!pFromTechno || !pToBuildingType) + return false; + + CellStruct location; + if (auto const pFromBuilding = abstract_cast(pFromTechno)) + { + auto const buildingCell = CellClass::Coord2Cell(pFromTechno->GetCoords()); + auto const tmpCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pFromBuilding->Type, buildingCell); + location = buildingCell + buildingCell - tmpCell; + } + else + { + location = CellClass::Coord2Cell(pFromTechno->GetCoords()); + } + + location = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pToBuildingType, location); + + auto const pPreviousDeployer = TechnoExt::Deployer; + TechnoExt::Deployer = pFromTechno; + const bool canCreate = pToBuildingType->CanCreateHere(location, pFromTechno->Owner); + TechnoExt::Deployer = pPreviousDeployer; + + return canCreate; +} + +// First of the two conversion steps: may pFromTechno be replaced by pToType right now? +// Unlike ConvertToType this has no side effects, so callers that only need to know (deploy +// checks, the infantry walk to the cell centre) can ask without changing anything. +bool TechnoExt::CanConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) +{ + if (!pFromTechno || !pToType || pFromTechno->GetTechnoType() == pToType) + return false; + + // A building that is busy with its own mission (building up, selling, ...) has no state + // to hand over yet. + if (auto const pFromBuilding = abstract_cast(pFromTechno)) + { + if (pFromBuilding->CurrentMission == Mission::None) + return false; + } + + // A building target needs its whole foundation free where the source stands, everything + // else is created on the spot the source already occupies. + if (auto const pToBuildingType = abstract_cast(pToType)) + return TechnoExt::CanCreateBuildingHere(pFromTechno, pToBuildingType); + + return true; +} + +// Second step: performs the conversion. Every caller is expected to have asked +// CanConvertToType first, but it is checked again here so nothing can bypass it. +static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) +{ + auto const pToTechno = static_cast(pToType->CreateObject(pFromTechno->Owner)); + if (!pToTechno) + return false; + + auto const wasSelected = pFromTechno->IsSelected; + + TechnoExt::TransferStatus(pFromTechno, pToTechno); + + pToTechno->SetTarget(pFromTechno->Target); + + auto location = pFromTechno->GetCoords(); + auto const pFromFoot = abstract_cast(pFromTechno); + auto const pFromBuilding = abstract_cast(pFromTechno); + auto const pToBuilding = abstract_cast(pToTechno); + auto const pToInfantry = abstract_cast(pToTechno); + if (pFromBuilding) + { + auto const buildingCell = CellClass::Coord2Cell(location); + auto const tmpCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pFromBuilding->Type, buildingCell); + location = CellClass::Cell2Coord(buildingCell + buildingCell - tmpCell, location.Z); + } + if (pToBuilding) + { + auto const deployCell = CellClass::Coord2Cell(location); + auto const buildingCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pToBuilding->Type, deployCell); + location = CellClass::Cell2Coord(buildingCell, location.Z); + } + + if (pFromFoot) + pToTechno->SetDestination(pFromFoot->Destination, true); + pToTechno->QueueMission(pFromTechno->CurrentMission, false); + + if (pFromBuilding && pFromBuilding->Occupants.Count > 0) + { + if (pToBuilding + && pToBuilding->Type->CanBeOccupied + && pToBuilding->Type->MaxNumberOccupants >= pFromBuilding->Occupants.Count) + { + pToBuilding->Occupants = pFromBuilding->Occupants; + } + else + { + pFromBuilding->Mission_Unload(); + } + } + + if (pFromFoot && pFromFoot->IsDeploying) + { + if (pToInfantry->Type->Sequence->GetSequence(Sequence::Deployed).CountFrames) + { + pToInfantry->PlayAnim(Sequence::Deployed, true); + } + else + { + pToInfantry->PlayAnim(Sequence::Ready, true); + } + } + + Unsorted::ScenarioInit++; + pToTechno->Unlimbo(location, pFromTechno->PrimaryFacing.Current().GetDir()); + Unsorted::ScenarioInit--; + + // Keep selection + if (wasSelected) + pToTechno->Select(); + + pFromTechno->Limbo(); + pFromTechno->UnInit(); + + if (!pToBuilding) + return true; + + if (pToBuilding->Type->Buildup) + { + pToBuilding->BeginMode(BStateType::Construction); + pToBuilding->QueueMission(Mission::Construction, false); + VoxClass::PlayAtPos(RulesClass::Instance->BuildingSlam, &location); + } + else + { + pToBuilding->BeginMode(BStateType::Idle); + pToBuilding->QueueMission(Mission::Guard, false); + pToBuilding->NextMission(); + } + + pToBuilding->Place(!pFromTechno->Owner->IsNeutral() || !pFromTechno->Owner->IsCurrentPlayer()); + + return true; +} + // Feature for common usage : TechnoType conversion -- Trsdy // BTW, who said it was merely a Type pointer replacement and he could make a better one than Ares? -bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) +bool TechnoExt::ConvertToType(TechnoClass* pThisTechno, TechnoTypeClass* pToType) { - const auto pType = pThis->GetTechnoType(); + const auto pType = pThisTechno->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()); + Debug::Log("Incompatible types between %s and %s\n", pThisTechno->get_ID(), pToType->get_ID()); return false; } + if (!TechnoExt::CanConvertToType(pThisTechno, pToType)) + return false; + + const auto absType = pType->WhatAmI(); + + if (absType != pToType->WhatAmI() || absType == AbstractType::BuildingType) + return XConvertToType(pThisTechno, pToType); + + auto const pThis = abstract_cast(pThisTechno); + if (!pThis) + return false; + if (AresFunctions::ConvertTypeTo) { if (AresFunctions::ConvertTypeTo(pThis, pToType)) @@ -471,6 +634,113 @@ bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) return true; } +void TechnoExt::HealthAutoConvertActions() +{ + const auto pTypeExt = this->TypeExtData; + + if (!pTypeExt->Convert_Health.isset()) + return; + + const double min = pTypeExt->Convert_Health_AbovePercent; + const double max = pTypeExt->Convert_Health_BelowPercent; + + if (min < 0 && max < 0) + return; + + const auto pThis = this->OwnerObject(); + + if (TechnoExt::IsHealthInThreshold(pThis, min, max)) + TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Health); +} + +void TechnoExt::TransferStatus(TechnoClass* pFrom, TechnoClass* pTo) +{ + pTo->SetHealthPercentage(pFrom->GetHealthPercentage()); + pTo->Veterancy = pFrom->Veterancy; + TechnoExt::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), + pToFoot = abstract_cast(pTo); + pFromFoot && pToFoot) + { + pToFoot->SpeedMultiplier = pFromFoot->SpeedMultiplier; + } + + if (pFrom->SlaveManager) + pFrom->SlaveManager->SetOwner(pTo); +} + +void TechnoExt::TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo) +{ + const auto pAnimType = pTechnoFrom->MindControlRingAnim + ? pTechnoFrom->MindControlRingAnim->Type + : TechnoExt::Fetch(pTechnoFrom)->MindControlRingAnimType; + + if (const auto pController = pTechnoFrom->MindControlledBy) + { + if (const auto pManager = pController->CaptureManager) + { + CaptureManagerExt::FreeUnit(pManager, pTechnoFrom, true); + + if (CaptureManagerExt::CaptureUnit(pManager, 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 phMC = pTechnoFrom->MindControlledByHouse) + { + pTechnoTo->MindControlledByHouse = phMC; + 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, TechnoTypeClass* pType, TechnoClass* pSource) { if (!pSource || !pType->TypeImmune) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 10156f70c6..f6c3854a79 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -189,6 +189,7 @@ class TechnoExt : public RadioExt, public Detach::Listener void UpdateTintValues(); void UpdateLastTargetCrd(); int GetSight(); + void HealthAutoConvertActions(); static bool CanReceiveEvent(TechnoClass* pThis, HouseClass* pHouse); @@ -217,6 +218,7 @@ class TechnoExt : public RadioExt, public Detach::Listener // deprecated stand-in for the pre-rework container of all TechnoClass extensions static inline CompatExtMap ExtMap {}; + static inline TechnoClass* Deployer { nullptr }; static bool LoadGlobals(PhobosStreamReader& Stm); static bool SaveGlobals(PhobosStreamWriter& Stm); @@ -251,7 +253,11 @@ class TechnoExt : public RadioExt, public Detach::Listener 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); - static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); + static bool CanConvertToType(TechnoClass* pThis, TechnoTypeClass* pToType); + static bool ConvertToType(TechnoClass* pThis, TechnoTypeClass* pToType); + static bool CanCreateBuildingHere(TechnoClass* pThis, BuildingTypeClass* pToBuildingType); + static void TransferStatus(TechnoClass* pFrom, TechnoClass* pTo); + static void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo); static bool IsTypeImmune(TechnoClass* pThis, TechnoTypeClass* pType, TechnoClass* pSource); static int GetTintColor(TechnoClass* pThis, bool invulnerability, bool airstrike, bool berserk); static int GetCustomTintColor(TechnoClass* pThis); diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index 082b1dc191..401b624aa1 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -31,7 +31,9 @@ DEFINE_HOOK(0x6F9E50, TechnoClass_AI, 0x5) { GET(TechnoClass*, pThis, ECX); - TechnoExt::Fetch(pThis)->OnEarlyUpdate(); + auto const pExt = TechnoExt::Fetch(pThis); + pExt->OnEarlyUpdate(); + pExt->HealthAutoConvertActions(); return 0; } @@ -45,7 +47,6 @@ DEFINE_HOOK(0x4DA54E, FootClass_AI, 0x6) pExt->UpdateWarpInDelay(); pExt->UpdateTiberiumEater(); pExt->AmmoAutoConvertActions(); - pExt->HealthAutoConvertActions(); if (pExt->AttackMoveFollowerTempCount) pExt->AttackMoveFollowerTempCount--; diff --git a/src/Ext/Unit/Body.cpp b/src/Ext/Unit/Body.cpp index 11d26305dd..cdb6c639fe 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) @@ -318,31 +316,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 5034821752..42386ca131 100644 --- a/src/Ext/Unit/Body.h +++ b/src/Ext/Unit/Body.h @@ -44,13 +44,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.DeployFire.cpp b/src/Ext/Unit/Hooks.DeployFire.cpp index 7e9bd4c1b8..ccd1ae98e3 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); + if (auto const pConvertToType = pExt->TypeExtData->Convert_Deploy.Get(); + pConvertToType + && pConvertToType->WhatAmI() == AbstractType::BuildingType + && !FootExt::CanDeployIntoBuilding(pFoot, false)) + { + if (pThis->IsOwnedByCurrentPlayer) + VoxClass::PlayIndex(VoxClass::FindIndex(GameStrings::EVA_CannotDeployHere)); + + return DoNotExecute; + } + } + return 0; } diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 41aa1b3d43..5e2b4d9a7b 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,82 +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); - ShieldClass::SyncShieldToAnother(pUnit, pStructure); - TechnoExt::SyncInvulnerability(pUnit, pStructure); - AttachEffectClass::TransferAttachedEffects(pUnit, pStructure); + TechnoExt::TransferStatus(pUnit, pStructure); return 0; } @@ -103,10 +32,7 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) GET(BuildingClass*, pStructure, EBP); GET(UnitClass*, pUnit, EBX); - TransferMindControlOnDeploy(pStructure, pUnit); - ShieldClass::SyncShieldToAnother(pStructure, pUnit); - TechnoExt::SyncInvulnerability(pStructure, pUnit); - AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); + TechnoExt::TransferStatus(pStructure, pUnit); // 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 @@ -129,6 +55,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 (!FootExt::CanDeployIntoBuilding(pInfantry, true)) + { + R->EAX(Action::NoDeploy); + + return 0x7000E4; + } + } + + return 0; +} + DEFINE_HOOK(0x7396AD, UnitClass_Deploy_CreateBuilding, 0x6) { GET(UnitClass*, pUnit, EBP); @@ -151,12 +94,40 @@ 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 (!FootExt::CanDeployIntoBuilding(pThis)) action = Action::NoDeploy; return SkipGameCode; } +// The deployer may be either a foot unit or a building (a building can deploy / sell +// into another one). Foot units only cover the cell they currently stand on, while +// buildings cover every cell of their foundation, so the two cases need handling +// separately - BuildingClass has no CurrentMapCoords. +static bool IsDeployerOnCell(const CellStruct& cellCoords) +{ + auto const pDeployer = TechnoExt::Deployer; + + if (!pDeployer) + return false; + + if (auto const pFoot = abstract_cast(pDeployer)) + return pFoot->CurrentMapCoords == cellCoords; + + if (auto const pBuilding = abstract_cast(pDeployer)) + { + auto const foundationCells = BuildingExt::GetFoundationCells(pBuilding, pBuilding->GetMapCoords(), true); + + for (auto const& foundationCell : foundationCells) + { + if (foundationCell == cellCoords) + return true; + } + } + + return false; +} + // Exclude the specific unit who want to deploy // Allow placing buildings on top of TerrainType with CanBeBuiltOn DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) @@ -196,7 +167,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 = IsDeployerOnCell(pCell->MapCoords); bool builtOnCanBeBuiltOn = false; for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) @@ -210,7 +181,7 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else if (pObject->AbstractFlags & AbstractFlags::Techno) { - if (pObject == UnitExt::Deployer) + if (pObject == TechnoExt::Deployer) { skipFlag = true; } @@ -245,14 +216,14 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else { - bool skipFlag = UnitExt::Deployer ? UnitExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; + bool skipFlag = IsDeployerOnCell(pCell->MapCoords); 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; @@ -266,7 +237,7 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } } - if (!builtOnCanBeBuiltOn && (pCell->OccupationFlags & (skipFlag ? 0x1F : 0x3F))) + if (!builtOnCanBeBuiltOn && (pCell->OccupationFlags & (skipFlag ? 0x02 : 0x3F))) return CanNotExistHere; } diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp index 4202273fc4..e5ba3014e7 100644 --- a/src/Ext/WarheadType/Detonate.cpp +++ b/src/Ext/WarheadType/Detonate.cpp @@ -671,12 +671,7 @@ void WarheadTypeExt::InterceptBullets(TechnoClass* pOwner, BulletClass* pInterce void WarheadTypeExt::ApplyConvert(HouseClass* pHouse, TechnoClass* pTarget) { - const auto pTargetFoot = abstract_cast(pTarget); - - if (!pTargetFoot) - return; - - TypeConvertGroup::Convert(pTargetFoot, this->Convert_Pairs, pHouse); + TypeConvertGroup::Convert(pTarget, this->Convert_Pairs, pHouse); } void WarheadTypeExt::ApplyLocomotorInfliction(TechnoClass* pTarget) diff --git a/src/Interop/TechnoExt.cpp b/src/Interop/TechnoExt.cpp index 8881d0312f..4b7346f98b 100644 --- a/src/Interop/TechnoExt.cpp +++ b/src/Interop/TechnoExt.cpp @@ -5,7 +5,7 @@ std::vector TechnoExtInterop::CalculateExtraThreatCallbacks = {}; std::vector TechnoExtInterop::CalculateSightCallbacks = {}; -DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, FootClass* pThis, TechnoTypeClass* toType) +DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, TechnoClass* pThis, TechnoTypeClass* toType) { if (!pThis || !toType) return E_POINTER; diff --git a/src/Interop/TechnoExt.h b/src/Interop/TechnoExt.h index e163828197..6953a31f5f 100644 --- a/src/Interop/TechnoExt.h +++ b/src/Interop/TechnoExt.h @@ -31,10 +31,10 @@ class TechnoExtInterop /// /// Converts a unit to a different type. /// -/// Pointer to the FootClass instance to convert +/// Pointer to the TechnoClass instance to convert /// Pointer to the target TechnoTypeClass /// S_OK if conversion was successful, E_INVALIDARG if types are incompatible, E_FAIL if conversion failed -DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, FootClass* pThis, TechnoTypeClass* toType); +DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, TechnoClass* pThis, TechnoTypeClass* toType); DEFINE_EXPORT(HRESULT, RegisterCalculateExtraThreatCallback, CalculateExtraThreatCallback callback); diff --git a/src/Misc/Hooks.Ares.cpp b/src/Misc/Hooks.Ares.cpp index 49ed47906d..94c53b6555 100644 --- a/src/Misc/Hooks.Ares.cpp +++ b/src/Misc/Hooks.Ares.cpp @@ -65,10 +65,7 @@ static void __fastcall LetGo(TemporalClass* pTemporal) static bool __stdcall ConvertToType(TechnoClass* pThis, TechnoTypeClass* pToType) { - if (const auto pFoot = abstract_cast(pThis)) - return TechnoExt::ConvertToType(pFoot, pToType); - - return false; + return TechnoExt::ConvertToType(pThis, pToType); } // Technically this replaces GetTechnoType() call. diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 46b4df6699..369aca4c7f 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(FootExt::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() && !FootExt::CanDeployIntoBuilding(pThis, true)) return MustDeploy; return SkipGameCode; diff --git a/src/New/Type/Affiliated/TypeConvertGroup.cpp b/src/New/Type/Affiliated/TypeConvertGroup.cpp index 306dd23508..828ca75cbf 100644 --- a/src/New/Type/Affiliated/TypeConvertGroup.cpp +++ b/src/New/Type/Affiliated/TypeConvertGroup.cpp @@ -1,33 +1,33 @@ #include #include "TypeConvertGroup.h" -void TypeConvertGroup::Convert(FootClass* pTargetFoot, const std::vector& convertPairs, HouseClass* pOwner) +void TypeConvertGroup::Convert(TechnoClass* pTarget, const std::vector& convertPairs, HouseClass* pOwner) { for (const auto& [fromTypes, toType, affectedHouses] : convertPairs) { if (!toType.Get()) continue; - if (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTargetFoot->Owner)) + if (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTarget->Owner)) continue; if (fromTypes.size()) { - const auto pType = pTargetFoot->GetTechnoType(); + const auto pType = pTarget->GetTechnoType(); for (const auto& from : fromTypes) { // Check if the target matches upgrade-from TechnoType and it has something to upgrade to if (from == pType) { - TechnoExt::ConvertToType(pTargetFoot, toType); + TechnoExt::ConvertToType(pTarget, toType); goto end; // Breaking out of nested loops without extra checks one of the very few remaining valid usecases for goto, leave it be. } } } else { - TechnoExt::ConvertToType(pTargetFoot, toType); + TechnoExt::ConvertToType(pTarget, toType); break; } } @@ -57,20 +57,18 @@ void TypeConvertGroup::ConvertSW(const std::vector& convertPai for (const auto pTarget : items) { - const auto pTargetFoot = abstract_cast(pTarget); - - if (!pTargetFoot || (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTargetFoot->Owner))) + if (!pTarget || (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTarget->Owner))) continue; - TechnoExt::ConvertToType(pTargetFoot, toType); + TechnoExt::ConvertToType(pTarget, toType); } } } else { - for (auto const pTargetFoot : FootClass::Array) + for (auto const pTarget : TechnoClass::Array) { - TypeConvertGroup::Convert(pTargetFoot, convertPairs, pOwner); + TypeConvertGroup::Convert(pTarget, convertPairs, pOwner); } } } diff --git a/src/New/Type/Affiliated/TypeConvertGroup.h b/src/New/Type/Affiliated/TypeConvertGroup.h index cc8178ef08..8ee636549b 100644 --- a/src/New/Type/Affiliated/TypeConvertGroup.h +++ b/src/New/Type/Affiliated/TypeConvertGroup.h @@ -15,7 +15,7 @@ class TypeConvertGroup bool Load(PhobosStreamReader& stm, bool registerForChange); bool Save(PhobosStreamWriter& stm) const; - static void Convert(FootClass* pTargetFoot, const std::vector& convertPairs, HouseClass* pOwner); + static void Convert(TechnoClass* pTarget, const std::vector& convertPairs, HouseClass* pOwner); static void ConvertSW(const std::vector& convertPairs, HouseClass* pOwner); private: From 33879b9bf306621795134b16e1b5305bed3022be 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: Thu, 15 Oct 2026 09:41:55 +0800 Subject: [PATCH 2/9] feat(infantry): walk to the cell centre before deploying into another type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An infantry stands on one of the sub-positions of its cell, but the object it turns into belongs on the centre of that cell. A conversion that an infantry orders itself to do (Convert.Deploy / Convert.Undeploy) therefore walks it there first instead of converting it on the spot; infantry targets keep the sub-position, as the new infantry simply takes over. The walk belongs to the deploy path, not to the conversion itself: the Mission_Unload and the DoingAI deploy hooks start it through InfantryExt::PrepareConvertAtCellCenter and InfantryExt::HandlePendingConvertToTypeAtCenter finishes the conversion once the infantry has arrived. CellClass::FindInfantrySubposition never yields the centre on its own - index 0 is unreachable and the selection logic always rewrites it into 2..4 - so CellExt::InfantryCenterAllowed lets the hook in Ext/Cell/Hooks.cpp force the centre entry while the infantry walks. Plain MoveTo cannot do the hop: the walk AI drops any destination inside the infantry's own cell as "already arrived" and the order is silently lost. The spot is checked again on arrival: if another object has taken it in the meantime the conversion fails instead of stacking two objects on one cell, and the pending conversion is dropped so the cell's centre flag is released again. Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Cell/Body.h | 9 +++ src/Ext/Cell/Hooks.cpp | 65 ++++++++++++++++++ src/Ext/Infantry/Body.cpp | 118 +++++++++++++++++++++++++++++++++ src/Ext/Infantry/Body.h | 5 ++ src/Ext/Infantry/Hooks.cpp | 6 +- src/Ext/Techno/Body.Update.cpp | 4 ++ 6 files changed, 204 insertions(+), 3 deletions(-) diff --git a/src/Ext/Cell/Body.h b/src/Ext/Cell/Body.h index 03055ff1a1..48fd10e435 100644 --- a/src/Ext/Cell/Body.h +++ b/src/Ext/Cell/Body.h @@ -47,6 +47,15 @@ class CellExt final : public AbstractExt std::vector RadLevels { }; int InfantryCount{ 0 }; + // Transient, deliberately not serialized: it only gates whether infantry walking + // within this cell may use the centre sub-position instead of a corner one + // (CellClass::FindInfantrySubposition, see Ext/Cell/Hooks.cpp). + // + // The caller that sets it is responsible for clearing it again, otherwise every + // infantry crossing this cell keeps using the centre. Leaving it out of Serialize + // means a loaded game starts with it false, which is the safe default. + int InfantryCenterAllowed { 0 }; + CellExt(CellClass* OwnerObject) : AbstractExt(OwnerObject) { } diff --git a/src/Ext/Cell/Hooks.cpp b/src/Ext/Cell/Hooks.cpp index 161a5463ee..b41ad82ddf 100644 --- a/src/Ext/Cell/Hooks.cpp +++ b/src/Ext/Cell/Hooks.cpp @@ -9,3 +9,68 @@ DEFINE_HOOK(0x480EA8, CellClass_DamageWall_AdjacentWallDamage, 0x5) pThis->DamageWall(RulesExt::Global()->AdjacentWallDamage); return SkipGameCode; } + +// CellClass::FindInfantrySubposition (0x481180) resolves the coordinate an infantry +// should occupy inside a cell. The per-cell sub-position offsets live in the table +// at 0x89E9F0, which holds five triples, and entry 0 is (0x80, 0x80, 0) - the exact +// cell centre. +// +// The engine only ever derives index 0 when the *requested* coordinate lies within +// 0x3C leptons of the centre (0x4811EC). Otherwise the derivation at +// 0x4811EC..0x481212 produces {2, 3, 4}: it sets bit 0 for X > 0x80 and bit 1 for +// Y > 0x80, then adds 1, which both shifts the pair into 2..4 and skips index 1 (the +// reserved "Top" slot, CellClass occupation bit 0x2). Index 1 is therefore +// unreachable and index 0 means "centre was requested". +// +// Either way index 0 can never survive the selection logic: +// * 0x481316 `TEST EDI,EDI / JZ 0x48138A` sends it to a random roll over the +// corner tables at 0x81CC84 / 0x81CC98, and those only contain indices 1..4, and +// * the per-infantry scan loop at 0x48136A explicitly skips indices 0 and 1 +// (`TEST ECX,ECX / JZ`, `CMP ECX,1 / JZ`). +// Infantry therefore only ever stand on the three corner/lower sub-positions. +// +// The shared tail at 0x481437 indexes the offset table with EDI before it reuses the +// register, so entering it with EDI == 0 produces the centre entry. This hook adds +// that route for infantry that are deliberately walking to the centre +// (CellExt::InfantryCenterAllowed). +// +// The flag is tested *before* dispatching on EDI, and the centre is forced for every +// EDI when it is set. That ordering matters: InfantryClass::UpdatePosition (0x51B967) +// calls this function again on every frame of the walk, re-deriving the index from +// the infantry's live position, which is still off-centre during the approach and so +// yields 2/3/4. Diverting only EDI == 0 would let the vanilla scan snap the infantry +// back onto a corner each frame and it could never reach the centre at all. +// +// The hook replaces the whole index-selection sequence (TEST/JZ/CMP/JZ, 9 bytes) and +// returns an explicit address on every path, so no branch ever falls through the +// patched bytes and no flag state has to be reconstructed. +DEFINE_HOOK(0x481316, CellClass_FindInfantrySubposition_AllowCenter, 0x9) +{ + enum + { + // Vanilla EDI == 0: roll a random corner out of the 0x81CC98 table. + RandomSubposition = 0x48138A, + // Vanilla EDI == 1 (unreachable per the derivation above, kept for fidelity). + SubpositionOne = 0x481356, + // Vanilla EDI >= 2: scan the 0x81CC84 table for a free sub-position. + ScanSubposition = 0x48131F, + // Shared tail; consumes EDI as the offset-table index. + ApplySubposition = 0x481437, + }; + + GET(CellClass*, pThis, ESI); + + auto const pExt = CellExt::TryFetch(pThis); + if (pExt && pExt->InfantryCenterAllowed) + { + // Force the centre entry, whatever index the engine derived. + R->EDI(0); + return ApplySubposition; + } + + // Not centring: reproduce the instructions this hook replaced. + if (R->EDI() == 0) + return RandomSubposition; + + return R->EDI() == 1 ? SubpositionOne : ScanSubposition; +} diff --git a/src/Ext/Infantry/Body.cpp b/src/Ext/Infantry/Body.cpp index 7bd1131ee4..465be2238c 100644 --- a/src/Ext/Infantry/Body.cpp +++ b/src/Ext/Infantry/Body.cpp @@ -1,5 +1,6 @@ #include "Body.h" +#include #include InfantryExt::ExtContainer InfantryExt::ExtMap; @@ -40,6 +41,123 @@ CoordStruct InfantryExt::GetSimpleFLH(InfantryClass* pThis, int weaponIndex, boo return FLH; } +// A conversion into something that is not an infantry has to happen on the centre of the +// infantry's cell, but an infantry stands on a sub-position of it. Starts the walk there and +// remembers the conversion; returns true while the caller has to wait for it to finish +// (UpdatePendingConvert does that once the infantry has arrived). +// +// MoveTo cannot do the walk. The walk locomotor's AI (0x75AEC0) asks the pathfinder for a +// route first (FootClass::UpdatePathfinding, 0x4D3920) and a destination inside the infantry's +// own cell yields none, so it falls through to a distance test (0x75B046). A sub-position -> +// centre hop is only ~90 leptons, well under the "already arrived" threshold at +// Rules+0x1718, so the AI calls foot->vt[0x480](0, 1) and stops: the order is silently +// dropped. +// +// ILocomotion::Force_Immediate_Destination is the generic route to that routine, and it is a +// plain ILocomotion method rather than a WalkLocomotionClass speciality: it sits at vtable +// slot 0x78 for every locomotor (identified by the slot's stack-cleanup arity matching +// `__stdcall (CoordStruct)`, i.e. RET 0x10). The other ten locomotors share a no-op stub +// (0x55AC00, a bare RET 0x10), while the walk locomotor overrides it at 0x75AE30 with a call +// to FUN_0075C240 - the same "resolve this cell's sub-position and set HeadToCoord" routine +// the AI reaches at 0x75BC13. +// +// Note that YRpp annotates this method with "Don't use this function." (Interfaces.h). Its +// behaviour here is exactly what is needed and it does not set IsMoving, relying on the AI +// picking HeadToCoord up on the next frame, so it is worth in-game verification rather than +// assuming. +bool InfantryExt::PrepareConvertToType(InfantryClass* pThis, TechnoTypeClass* pToType) +{ + if (!pThis->Locomotor || !pToType) + return false; + + if (!TechnoExt::CanConvertToType(pThis, pToType)) + return false; + + // Infantry to infantry keeps its sub-position: the new infantry simply takes over. + if (pToType->WhatAmI() == AbstractType::InfantryType) + return TechnoExt::ConvertToType(pThis, pToType); + + auto const pCell = MapClass::Instance.TryGetCellAt(pThis->GetCoords()); + + if (!pCell) + return false; + + if (pCell->OccupationFlags & 0x01) + return false; + + auto const pExt = InfantryExt::Fetch(pThis); + + // Already walking there for a conversion that was ordered before this one; keep it, so the + // cell's flag is only raised once for one walk. + if (pExt->PendingConvertType) + return true; + + pExt->PendingConvertType = pToType; + pThis->PlayAnim(Sequence::Ready, true); + auto const center = pCell->GetCenterCoords(); + auto const pCellExt = CellExt::Fetch(pCell); + + // CellClass::FindInfantrySubposition only yields the centre while InfantryCenterAllowed is + // raised (see the hook in Ext/Cell/Hooks.cpp). It has to stay raised for the whole walk: + // the AI re-enters FUN_0075C240 every frame (0x75BE18, with the Empty sentinel, so the + // routine re-resolves the foot's current position) and would otherwise overwrite + // HeadToCoord with a corner again. ClearPendingConvertToTypeAtCenter lowers it again. + + pCellExt->InfantryCenterAllowed++; + pThis->Locomotor->Force_Immediate_Destination(center); + pCellExt->InfantryCenterAllowed--; + + return true; +} + +// Finishes a conversion that was deferred until the infantry reached the centre of its cell. +// The spot is checked again on arrival: the world may have changed while walking, and a +// conversion that cannot be done there fails instead of stacking two objects on one cell. +void InfantryExt::UpdatePendingConvert(InfantryClass* pThis) +{ + auto const pThisExt = InfantryExt::Fetch(pThis); + auto const pToType = pThisExt->PendingConvertType; + + if (!pToType) + return; + + // Still on the way. Force_Immediate_Destination does not set IsMoving, the locomotor does + // that itself when it picks HeadToCoord up, so this only waits for an actual walk. + if (pThis->Locomotor && pThis->Locomotor->Is_Moving()) + return; + + const bool canConvert = InfantryExt::CanConvertAtCellCenter(pThis) + && TechnoExt::CanConvertToType(pThis, pToType); + + pThisExt->PendingConvertType = nullptr; + + if (canConvert) + { + TechnoExt::ConvertToType(pThis, pToType); + } + else if (pThis->Owner->IsCurrentPlayer() && (pThisExt->HasDeployConverted || pThisExt->HasUndeployConverted)) + { + VoxClass::PlayIndex(VoxClass::FindIndex(GameStrings::EVA_CannotDeployHere)); + } +} + +// Nobody else may stand on the spot the converted object needs. +bool InfantryExt::CanConvertAtCellCenter(InfantryClass* pThis) +{ + auto const pCell = MapClass::Instance.TryGetCellAt(pThis->GetCoords()); + + if (!pCell) + return false; + + for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) + { + if (pObject != pThis && (pObject->AbstractFlags & AbstractFlags::Techno)) + return false; + } + + return true; +} + // ============================= // load / save diff --git a/src/Ext/Infantry/Body.h b/src/Ext/Infantry/Body.h index fcd816b6e4..1eef255d11 100644 --- a/src/Ext/Infantry/Body.h +++ b/src/Ext/Infantry/Body.h @@ -15,11 +15,13 @@ class InfantryExt final : public FootExt bool SkipTargetChangeResetSequence; bool HasDeployConverted; bool HasUndeployConverted; + TechnoTypeClass* PendingConvertType; explicit InfantryExt(InfantryClass* const OwnerObject) : FootExt(OwnerObject) , SkipTargetChangeResetSequence { false } , HasDeployConverted { false } , HasUndeployConverted { false } + , PendingConvertType { nullptr } { } InfantryClass* OwnerObject() const @@ -34,6 +36,9 @@ class InfantryExt final : public FootExt } static CoordStruct GetSimpleFLH(InfantryClass* pThis, int weaponIndex, bool& FLHFound); + static bool PrepareConvertToType(InfantryClass* pThis, TechnoTypeClass* pToType); + static void UpdatePendingConvert(InfantryClass* pThis); + static bool CanConvertAtCellCenter(InfantryClass* pThis); class ExtContainer final : public Container { diff --git a/src/Ext/Infantry/Hooks.cpp b/src/Ext/Infantry/Hooks.cpp index df16c86d67..f2a4418fa0 100644 --- a/src/Ext/Infantry/Hooks.cpp +++ b/src/Ext/Infantry/Hooks.cpp @@ -30,7 +30,7 @@ DEFINE_HOOK(0x51F738, InfantryClass_Mission_Unload_ConvertDeploy_NoSequence, 0x5 // instead: it forces Mission_Guard, so Mission_Unload cannot run a second time for one // deploy command. A build with the guard and both assignments removed behaved identically. - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Deploy); + InfantryExt::PrepareConvertToType(pThis, pTypeExt->Convert_Deploy); return FinishMission; } @@ -56,7 +56,7 @@ DEFINE_HOOK(0x520B3E, InfantryClass_DoingAI_DeployConvert_Deploy, 0x6) { pExt->HasDeployConverted = true; pExt->HasUndeployConverted = false; - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Deploy); + InfantryExt::PrepareConvertToType(pThis, pTypeExt->Convert_Deploy); } return 0; @@ -73,7 +73,7 @@ DEFINE_HOOK(0x520B99, InfantryClass_DoingAI_DeployConvert_Undeploy, 0x6) { pExt->HasUndeployConverted = true; pExt->HasDeployConverted = false; - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Undeploy); + InfantryExt::PrepareConvertToType(pThis, pTypeExt->Convert_Undeploy); } return 0; diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 4f75d9cf08..dca605efd4 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,9 @@ void TechnoExt::OnEarlyUpdate() return; this->ApplyInterceptor(); + + if (auto const pInfantry = abstract_cast(this->OwnerObject())) + InfantryExt::UpdatePendingConvert(pInfantry); } void TechnoExt::ApplyInterceptor() From bb8b5a11796540ffeeeb12f551ed17ec25fcf189 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: Thu, 15 Oct 2026 10:02:00 +0800 Subject: [PATCH 3/9] feat(building): add Convert.ShowBuildUp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [SomeBuilding] -> Convert.ShowBuildUp (default false) decides whether a conversion into or out of this building plays the buildup animation and its BuildingSlam sound. Buildings that should appear instantly when another object is converted into them - or that should disappear without the animation when they are converted away - can set it to no. Only conversions are affected. Regular DeploysInto / UndeploysInto deploys do not go through the conversion code and keep playing the buildup. Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 95 ++++++++++++++++++++++--------------- src/Ext/TechnoType/Body.cpp | 6 +++ src/Ext/TechnoType/Body.h | 6 +++ 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 3b3ca32ef0..5aa67d0379 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -370,7 +370,7 @@ bool TechnoExt::CanCreateBuildingHere(TechnoClass* pFromTechno, BuildingTypeClas { location = CellClass::Coord2Cell(pFromTechno->GetCoords()); } - + location = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pToBuildingType, location); auto const pPreviousDeployer = TechnoExt::Deployer; @@ -412,7 +412,7 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) auto const pToTechno = static_cast(pToType->CreateObject(pFromTechno->Owner)); if (!pToTechno) return false; - + auto const wasSelected = pFromTechno->IsSelected; TechnoExt::TransferStatus(pFromTechno, pToTechno); @@ -475,13 +475,34 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) if (wasSelected) pToTechno->Select(); - pFromTechno->Limbo(); - pFromTechno->UnInit(); + bool showBuildupFrom = false; + if (pFromBuilding && pFromBuilding->CanBeSold() && !pFromBuilding->IsStrange()) + { + auto const pFromBuildingExt = BuildingExt::Fetch(pFromBuilding); + showBuildupFrom = pFromBuildingExt->TypeExtData->Convert_ShowBuildupFrom || pFromBuildingExt->TypeExtData->Convert_ShowBuildup; + } + + if (showBuildupFrom) + { + pFromBuilding->SetArchiveTarget(nullptr); + pFromBuilding->Sell(1); + } + else + { + pFromTechno->Limbo(); + pFromTechno->UnInit(); + } + if (!pToBuilding) return true; + auto const pToBuildingExt = BuildingExt::Fetch(pToBuilding); + bool showBuildupTo = false; if (pToBuilding->Type->Buildup) + showBuildupTo = pToBuildingExt->TypeExtData->Convert_ShowBuildupTo || pToBuildingExt->TypeExtData->Convert_ShowBuildup; + + if (showBuildupTo) { pToBuilding->BeginMode(BStateType::Construction); pToBuilding->QueueMission(Mission::Construction, false); @@ -494,7 +515,7 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) pToBuilding->NextMission(); } - pToBuilding->Place(!pFromTechno->Owner->IsNeutral() || !pFromTechno->Owner->IsCurrentPlayer()); + pToBuilding->Place(!pFromTechno->Owner->IsNeutral() || pFromTechno->Owner->IsCurrentPlayer()); return true; } @@ -1049,7 +1070,7 @@ void TechnoExt::ClickedApproachObject(FootClass* pThis, ObjectClass* pObject) if (Unsorted::MoveFeedback) pThis->VoiceMove(); - EventExt event {}; + EventExt event { }; event.Type = EventTypeExt::ApproachObject; event.HouseIndex = static_cast(pThis->Owner->ArrayIndex); event.Frame = Unsorted::CurrentFrame; @@ -1060,38 +1081,38 @@ void TechnoExt::ClickedApproachObject(FootClass* pThis, ObjectClass* pObject) bool TechnoExt::CanBeRecruitedFix(FootClass* pThis, HouseClass* pHouse) { - if (pThis->Team != nullptr || - !pThis->IsAlive || - pThis->Health <= 0 || - pThis->InLimbo || + if (pThis->Team != nullptr || + !pThis->IsAlive || + pThis->Health <= 0 || + pThis->InLimbo || pThis->IsSinking || - pThis->Owner != pHouse) - { - return false; - } - - if (!(pThis->RecruitableA && pThis->RecruitableB)) - { - return false; - } - - const Mission mission = pThis->GetCurrentMission(); - if (!MissionClass::IsRecruitableMission(mission)) - { - return false; - } - - if (pThis->ShouldEnterAbsorber || - pThis->ShouldEnterOccupiable || - pThis->ShouldGarrisonStructure || - pThis->DrainTarget != nullptr || - pThis->BunkerLinkedItem || - pThis->LocomotorSource != nullptr) - { - return false; - } - - return true; + pThis->Owner != pHouse) + { + return false; + } + + if (!(pThis->RecruitableA && pThis->RecruitableB)) + { + return false; + } + + const Mission mission = pThis->GetCurrentMission(); + if (!MissionClass::IsRecruitableMission(mission)) + { + return false; + } + + if (pThis->ShouldEnterAbsorber || + pThis->ShouldEnterOccupiable || + pThis->ShouldGarrisonStructure || + pThis->DrainTarget != nullptr || + pThis->BunkerLinkedItem || + pThis->LocomotorSource != nullptr) + { + return false; + } + + return true; } bool TechnoExt::EjectRandomly(FootClass* pEjectee, const CoordStruct& coords, int distance, bool select) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index f8c9b81246..aa1fc5097e 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1255,6 +1255,9 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->Convert_Health_AbovePercent.Read(exINI, pSection, "Convert.Health.AbovePercent"); this->Convert_Health_BelowPercent.Read(exINI, pSection, "Convert.Health.BelowPercent"); this->Convert_Health.Read(exINI, pSection, "Convert.Health"); + this->Convert_ShowBuildup.Read(exINI, pSection, "Convert.ShowBuildup"); + this->Convert_ShowBuildupFrom.Read(exINI, pSection, "Convert.ShowBuildupFrom"); + this->Convert_ShowBuildupTo.Read(exINI, pSection, "Convert.ShowBuildupTo"); if (this->Convert_Health_AbovePercent > this->Convert_Health_BelowPercent) Debug::Log("[Developer warning][%s] Convert.Health.AbovePercent is greater than Convert.Health.BelowPercent, resulting in no conversion.\n", pSection); @@ -1834,6 +1837,9 @@ void TechnoTypeExt::Serialize(T& Stm) .Process(this->Convert_Health_AbovePercent) .Process(this->Convert_Health_BelowPercent) .Process(this->Convert_Health) + .Process(this->Convert_ShowBuildup) + .Process(this->Convert_ShowBuildupFrom) + .Process(this->Convert_ShowBuildupTo) .Process(this->ExitThroughRoof) .Process(this->PsychicDetectable) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 0245798eea..38ec3901da 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -499,6 +499,9 @@ class TechnoTypeExt : public ObjectTypeExt Valueable Convert_Health_AbovePercent; Valueable Convert_Health_BelowPercent; Nullable Convert_Health; + Valueable Convert_ShowBuildup; + Valueable Convert_ShowBuildupFrom; + Valueable Convert_ShowBuildupTo; Nullable ExitThroughRoof; Valueable PsychicDetectable; @@ -924,6 +927,9 @@ class TechnoTypeExt : public ObjectTypeExt , Convert_Health_AbovePercent { -1.0 } , Convert_Health_BelowPercent { -1.0 } , Convert_Health {} + , Convert_ShowBuildup { false } + , Convert_ShowBuildupFrom { false } + , Convert_ShowBuildupTo { false } , PsychicDetectable { true } From 57ace632e16a1882d2bd941ecd32242c9cea3499 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: Thu, 15 Oct 2026 10:26:25 +0800 Subject: [PATCH 4/9] feat(convert): convert after building up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/BuildingType/Body.cpp | 5 +++++ src/Ext/BuildingType/Body.h | 6 ++++++ src/Ext/Techno/Body.cpp | 19 ++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 67c5e380bf..84e1992632 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -366,6 +366,9 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->Academy_Country_Ignore[idx].Read(exINI, pSection, tempBuffer); } + this->Convert_Health_AfterBuildup.Read(exINI, pSection, "Convert.Health.AfterBuildup"); + this->Convert_Direction.Read(exINI, pSection, "Convert.Direction"); + // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); if (SuperWeaponTypeClass::Array.Count > 0) @@ -534,6 +537,8 @@ void BuildingTypeExt::Serialize(T& Stm) .Process(this->Academy_Building_Veterancy) .Process(this->Academy_Country_Types) .Process(this->Academy_Country_Ignore) + .Process(this->Convert_Health_AfterBuildup) + .Process(this->Convert_Direction) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 2b86dcbefc..712061febd 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -149,6 +149,9 @@ class BuildingTypeExt final : public TechnoTypeExt ValueableVector Academy_Country_Types[16]; ValueableVector Academy_Country_Ignore[16]; + Valueable Convert_Health_AfterBuildup; + Nullable Convert_Direction; + // Ares 0.2 Valueable CloningFacility; @@ -269,6 +272,9 @@ class BuildingTypeExt final : public TechnoTypeExt , Academy_Country_Types {} , Academy_Country_Ignore {} + , Convert_Health_AfterBuildup { false } + , Convert_Direction {} + // Ares 0.2 , CloningFacility { false } diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 5aa67d0379..98a86c57b5 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -419,6 +419,7 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) pToTechno->SetTarget(pFromTechno->Target); + auto direction = pFromTechno->PrimaryFacing.Current().GetDir(); auto location = pFromTechno->GetCoords(); auto const pFromFoot = abstract_cast(pFromTechno); auto const pFromBuilding = abstract_cast(pFromTechno); @@ -429,6 +430,9 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) auto const buildingCell = CellClass::Coord2Cell(location); auto const tmpCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pFromBuilding->Type, buildingCell); location = CellClass::Cell2Coord(buildingCell + buildingCell - tmpCell, location.Z); + + auto const pFromBuildingTypeExt = BuildingTypeExt::Fetch(pFromBuilding->Type); + direction = pFromBuildingTypeExt->Convert_Direction.Get(direction); } if (pToBuilding) { @@ -468,7 +472,7 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) } Unsorted::ScenarioInit++; - pToTechno->Unlimbo(location, pFromTechno->PrimaryFacing.Current().GetDir()); + pToTechno->Unlimbo(location, direction); Unsorted::ScenarioInit--; // Keep selection @@ -668,6 +672,19 @@ void TechnoExt::HealthAutoConvertActions() if (min < 0 && max < 0) return; + if (const auto pBuilding = abstract_cast(this->OwnerObject())) + { + if (BuildingTypeExt::Fetch(pBuilding->Type)->Convert_Health_AfterBuildup.Get()) + { + switch (pBuilding->CurrentMission) + { + case Mission::None: + case Mission::Construction: + return; + } + } + } + const auto pThis = this->OwnerObject(); if (TechnoExt::IsHealthInThreshold(pThis, min, max)) From 5233083b157fc6dded93655b772002b16cdd63d1 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: Thu, 15 Oct 2026 11:09:43 +0800 Subject: [PATCH 5/9] docs(xconvert): document cross-type conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "Convert TechnoType into any other type" section describing conversions across categories and to/from BuildingTypes, the new Convert.ShowBuildup / Convert.ShowBuildupFrom / Convert.ShowBuildupTo and Convert.Direction tags - Document Convert.Health.AfterBuildup, drop the outdated "does not support BuildingTypes" warnings (4x), note that Convert.Deploy / Convert.Undeploy accept any TechnoType and that infantry walks to the cell centre before converting - Add Whats-New 0.6 and CREDITS entries, plus zh_CN translations Signed-off-by: 舰队的偶像-岛风酱! --- CREDITS.md | 6 +- docs/Fixed-or-Improved-Logics.md | 1 + docs/New-or-Enhanced-Logics.md | 51 ++++++++++--- docs/Whats-New.md | 3 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 20 +++-- .../LC_MESSAGES/Fixed-or-Improved-Logics.po | 3 + .../LC_MESSAGES/New-or-Enhanced-Logics.po | 75 +++++++++++++------ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 9 +++ 8 files changed, 129 insertions(+), 39 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 8ce28c67c4..8b31889b5b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -996,7 +996,11 @@ 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 (舰队的偶像-岛风酱!)** - Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed +- **frg2089 (舰队的偶像-岛风酱!)**: + - Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed + - Convert TechnoType into any other type, including to and from `BuildingType`s + - Infantry walk to the centre of their cell before converting into another type + - Carry over health percentage, veteran status, attribute multipliers and slave manager on conversion - **weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral houses - **dh381-1**: - Detonate ivan bomb on impact diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index f0bdc16673..a05d81c44f 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -337,6 +337,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - Fixed an issue where vehicles affected by warheads with `IsLocomotor=yes` would have their effects interrupted when deactivated or reactivated. - Fixed crashes when restarting missions that use TerrainTypes with cached SHP images. - Fixed the bug that units keep attacking ground after target wall has been destroyed by adjacent damage/crush/wave damage. +- Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over the health percentage, veteran status, armor / firepower / speed multipliers and slave manager of the source techno to the new one, in addition to the mind control, shields, invulnerability and attached effects that were already transferred. ## Fixes / interactions with other extensions diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 55ba02215f..89440542aa 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -872,12 +872,14 @@ SpyEffect.InfiltratorSuperWeapon= ; SuperWeaponType - Now infantry can perform type conversion immediately after the `Deploy` or `Undeploy` sequence action has been executed. - If a deployable infantry has no deploy sequence, it will immediately execute `Convert.Deploy` after a deploy order is issued, just like deployable vehicles. + - The new type does not have to be an infantry type (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). If it is not, the infantry first walks to the centre of the cell it is occupying. + - If the new type is a `BuildingType`, the deploy order is only carried out when the building can be placed on the spot the infantry stands on. Otherwise the `NoDeploy` cursor is shown and the *Cannot deploy here* EVA is played. In `rulesmd.ini`: ```ini [SOMEINFANTRY] ; InfantryType -Convert.Deploy= ; InfantryType -Convert.Undeploy= ; InfantryType +Convert.Deploy= ; TechnoType +Convert.Undeploy= ; TechnoType ``` ### Customizable FLH when infantry is prone or deployed @@ -1394,7 +1396,7 @@ SW.TechLevel=0 ; integer ### Convert TechnoType -- Warheads can now change TechnoTypes of affected units to other Types in the same category (infantry to infantry, vehicles to vehicles, aircraft to aircraft). +- Warheads can now change TechnoTypes of affected units to any other TechnoType, no matter which category it belongs to, including `BuildingType`s. - `ConvertN.From` (where N is 0, 1, 2...) specifies which TechnoTypes are valid for conversion. This entry can have many types listed, meanging that many types will be converted at once. When no types are included, conversion will affect all valid targets. - `ConvertN.To` specifies the TechnoType which is the result of conversion. - `ConvertN.AffectsHouse` specifies whose units can be converted. @@ -1423,7 +1425,7 @@ Convert.AffectsHouse=owner ; List of Affected House Enumeration (none|owner ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} @@ -1693,7 +1695,7 @@ AutoTargetAI.NoThreatBuildings=true ; boolean - Units can now be converted into another unit by ammo count. - `Ammo.AutoConvertMinimumAmount` determines the minimal number of ammo at which a unit converts automatically after the ammo update. - `Ammo.AutoConvertMaximumAmount` determines the maximum number of ammo at which a unit converts automatically after the ammo update. -- `Ammo.AutoConvertType` specify the new techno after the conversion. This unit must be of the same type of the original (infantry -> infantry, vehicle -> vehicle or aircraft -> aircraft). +- `Ammo.AutoConvertType` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). - Setting a negative number will disable the ammo count check, and when both checks are disabled, conversion will not occur. In `rulesmd.ini`: @@ -1705,7 +1707,7 @@ Ammo.AutoConvertType= ; TechnoType, after conversion ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} @@ -1717,7 +1719,8 @@ This feature requires Ares 3.0 or higher to function! When Ares 3.0+ is not dete - Units can now be converted into another unit by health percentage. - `Convert.Health.AbovePercent` determines the minimal health percentage at which a unit converts automatically. - `Convert.Health.BelowPercent` determines the maximum health percentage at which a unit converts automatically. -- `Convert.Health` specify the new techno after the conversion. This unit must be of the same type of the original (infantry -> infantry, vehicle -> vehicle or aircraft -> aircraft). +- `Convert.Health` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). +- `Convert.Health.AfterBuildup`, if set to true, delays the conversion of a `BuildingType` until its buildup has finished. - Setting a negative number will disable the HP check, and when both checks are disabled, conversion will not occur. In `rulesmd.ini`: @@ -1726,10 +1729,13 @@ In `rulesmd.ini`: Convert.Health.AbovePercent=-1.0 ; floating point value, percents or absolute Convert.Health.BelowPercent=-1.0 ; floating point value, percents or absolute Convert.Health= ; TechnoType, after conversion + +[SOMEBUILDING] ; BuildingType +Convert.Health.AfterBuildup=false ; boolean ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} @@ -1860,9 +1866,34 @@ DecloakAnims= ; List of Animation, default to [AudioVisual] -> D Cloak.KickOutParasite= ; boolean, default to [General] -> Cloak.KickOutParasite ``` +### Convert TechnoType into any other type + +- Now any TechnoType can be converted into any other TechnoType, no matter which category it belongs to, including to and from `BuildingType`s. + - This applies to every form of type conversion: warhead and superweapon `Convert` pairs, `Convert.Deploy`, `Convert.Undeploy`, `Convert.Health`, `Ammo.AutoConvertType`, `Convert.HumanToComputer` / `Convert.ComputerToHuman` and Ares' type conversion. + - If the new type is a `BuildingType`, it is placed on the spot the source techno stands on, as long as its whole foundation fits there. If it does not fit, the conversion does not happen; a deploy order for `Convert.Deploy` is refused with the `NoDeploy` cursor and the *Cannot deploy here* EVA. + - Infantry that deploy or undeploy into a type other than an infantry type first walk to the centre of the cell they are occupying, so the conversion animation is played in the middle of the cell. + - The new techno inherits the health percentage, veteran status, mind control, shields, invulnerability, attached effects, armor / firepower / speed multipliers and slave manager of the source techno. This also applies to vanilla `DeploysInto`, `UndeploysInto` and selling. + - `Convert.Direction` determines the direction the new techno faces when converting from a `BuildingType`. If not set, the new techno keeps the facing of the source. + - The building animations played while converting to or from a building are customizable with the following tags. + - `Convert.ShowBuildupFrom` makes the building being converted play its sell animation instead of disappearing immediately. Requires the building to be sellable. + - `Convert.ShowBuildupTo` makes the newly created building play its buildup animation and plays `[AudioVisual] -> BuildingSlam` at its location instead of appearing finished right away. Requires `Buildup=` (the name of an SHP file) to be set for the new building type in `artmd.ini`. + - `Convert.ShowBuildup` enables both of the above at once. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +Convert.ShowBuildup=false ; boolean +Convert.ShowBuildupFrom=false ; boolean +Convert.ShowBuildupTo=false ; boolean + +[SOMEBUILDING] ; BuildingType +Convert.Direction= ; Direction type (integers from 0-255) +``` + ### Convert TechnoType on owner house change - You can now change a unit's type when changing ownership from human to computer or from computer to human. + - This also works for `BuildingType`s (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). In `rulesmd.ini`: ```ini @@ -3114,7 +3145,7 @@ ChangeOwner.MindControlAnim= ; Animation ![image](_static/images/convertwh.gif) *Vehicle version of Genetic Converter in [NanoStorm](https://www.bilibili.com/opus/896077937747427433)* -- Warheads can now change TechnoTypes of affected units to other Types in the same category (infantry to infantry, vehicles to vehicles, aircraft to aircraft). +- Warheads can now change TechnoTypes of affected units to any other TechnoType, no matter which category it belongs to, including `BuildingType`s. - `ConvertN.From` (where N is 0, 1, 2...) specifies which TechnoTypes are valid for conversion. This entry can have many types listed, meanging that many types will be converted at once. When no types are included, conversion will affect all valid targets. - `ConvertN.To` specifies the TechnoType which is the result of conversion. - `ConvertN.AffectsHouse` specifies whose units can be converted. @@ -3142,7 +3173,7 @@ Convert.AffectsHouse=all ; List of Affected House Enumeration (none|owner ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 466b7c6dec..64156cd7bb 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -2053,6 +2053,9 @@ HideShakeEffects=false ; boolean - Customize turn rate when jumpjet vehicle crashing (by NetsuNegi) - Customize insignias palette (by Flactine & NetsuNegi) - Allowed deployable infantry without a deploy sequence to directly execute `Convert.Deploy` like deployable vehicles (by Noble_Fish) +- [Convert TechnoType into any other type](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type) (by frg2089) +- [Infantry deploying or undeploying into a type other than an infantry type now walk to the centre of their cell first](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (by frg2089) +- Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over health percentage, veteran status, attribute multipliers and slave manager of the source techno (by frg2089) #### Vanilla fixes: - Fixed the bug where a building with `Factory=BuildingType` owned by the AI did not play `ProductionAnim` when placing a produced building (by Noble_Fish) diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 0555cf371e..b796bc442b 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3430,12 +3430,20 @@ msgstr "**obsidianus** - 自动根据血量变形" msgid "**Nuke** - Reload speed adjustment on promotion" msgstr "**Nuke** - 升级时调整装填速率" -msgid "" -"**frg2089 (舰队的偶像-岛风酱!)** - Fix `Slaved.OwnerWhenMasterKilled` not being " -"respected when the master is sold or self-destructed" -msgstr "" -"**frg2089 (舰队的偶像-岛风酱!)** - 修复了奴隶矿场被出售或自毁时 `Slaved.OwnerWhenMasterKilled` " -"不被遵守的问题" +msgid "**frg2089 (舰队的偶像-岛风酱!)**:" +msgstr "**frg2089 (舰队的偶像-岛风酱!)**:" + +msgid "Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed" +msgstr "修复了奴隶矿场被出售或自毁时 `Slaved.OwnerWhenMasterKilled` 不被遵守的问题" + +msgid "Convert TechnoType into any other type, including to and from `BuildingType`s" +msgstr "允许任意科技类型转换为任意其他科技类型,包括与 `BuildingType` 之间的相互转换" + +msgid "Infantry walk to the centre of their cell before converting into another type" +msgstr "步兵在转换为其他类型前会先走到所在单元格的中央" + +msgid "Carry over health percentage, veteran status, attribute multipliers and slave manager on conversion" +msgstr "转换时继承生命值百分比、老兵等级、属性倍增器与奴隶管理器" msgid "" "**weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral " 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 5fd782e571..6a9cdd3983 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 @@ -1754,6 +1754,9 @@ msgid "" msgstr "" "修复了目标墙壁被相邻伤害/碾压/声波伤害摧毁后单位仍持续攻击地面的 Bug。" +msgid "Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over the health percentage, veteran status, armor / firepower / speed multipliers and slave manager of the source techno to the new one, in addition to the mind control, shields, invulnerability and attached effects that were already transferred." +msgstr "原版的 `DeploysInto`、`UndeploysInto` 与出售现在除了原本就会传递的精神控制、护盾、无敌效果与附加效果之外,还会将源单位的生命值百分比、老兵等级、护甲/火力/速度倍增器与奴隶管理器传递给新单位。" + msgid "Fixes / interactions with other extensions" msgstr "修复或与其他扩展的交互" 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 a755a30a9a..68b8b64fc4 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 @@ -2163,6 +2163,12 @@ msgid "" msgstr "" "如果一个可部署的步兵没有部署序列,那么它将会像可部署的载具那样在下达部署命令后立即执行 `Convert.Deploy`。" +msgid "The new type does not have to be an infantry type (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). If it is not, the infantry first walks to the centre of the cell it is occupying." +msgstr "新类型不必是步兵类型(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。若不是步兵类型,步兵会先走到其所在单元格的中央。" + +msgid "If the new type is a `BuildingType`, the deploy order is only carried out when the building can be placed on the spot the infantry stands on. Otherwise the `NoDeploy` cursor is shown and the *Cannot deploy here* EVA is played." +msgstr "若新类型是 `BuildingType`,只有当建筑可以放置在步兵所处的位置时部署命令才会被执行。否则将显示 `NoDeploy` 光标并播放 *Cannot deploy here* 语音。" + msgid "Customizable FLH when infantry is prone or deployed" msgstr "自定义步兵匍匐或部署状态的 FLH" @@ -3330,11 +3336,8 @@ msgstr "" msgid "Convert TechnoType" msgstr "单位转换超武" -msgid "" -"Warheads can now change TechnoTypes of affected units to other Types in " -"the same category (infantry to infantry, vehicles to vehicles, aircraft " -"to aircraft)." -msgstr "现在弹头可以将受影响单位的科技类型更改为同大类的其他科技类型(步兵到步兵,车辆到车辆,战机到战机)。" +msgid "Warheads can now change TechnoTypes of affected units to any other TechnoType, no matter which category it belongs to, including `BuildingType`s." +msgstr "现在弹头可以将受影响单位的科技类型更改为任意其他科技类型,无论其属于哪个大类,包括 `BuildingType`。" msgid "" "`ConvertN.From` (where N is 0, 1, 2...) specifies which TechnoTypes are " @@ -3372,13 +3375,8 @@ msgid "" "`SOLDIERA` and `SOLDIERB` to `NEWSOLDIER`:" msgstr "例如,这个超级武器可以将所有被影响的己方和友方 `SOLDIERA` 和 `SOLDIERB` 转换为 `NEWSOLDIER`:" -msgid "" -"This feature has the same limitations as [Ares' Type Conversion](https" -"://ares-developers.github.io/Ares-docs/new/typeconversion.html). This " -"feature does not support BuildingTypes." -msgstr "" -"此功能与 [Ares 的单位转换](https://ares-developers.github.io/Ares-" -"docs/new/typeconversion.html) 存在同样的限制。此功能不支持建筑类型。" +msgid "This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)." +msgstr "当在同一大类的科技类型之间转换时,此功能与 [Ares 的单位转换](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) 存在同样的限制。与 `BuildingType` 之间的转换由 Phobos 自行处理,详见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type)。" msgid "" "This feature requires Ares 3.0 or higher to function! When Ares 3.0+ is " @@ -3920,11 +3918,8 @@ msgid "" "which a unit converts automatically after the ammo update." msgstr "`Ammo.AutoConvertMaximumAmount` 决定触发自动变形的最大弹药量。" -msgid "" -"`Ammo.AutoConvertType` specify the new techno after the conversion. This " -"unit must be of the same type of the original (infantry -> infantry, " -"vehicle -> vehicle or aircraft -> aircraft)." -msgstr "`Ammo.AutoConvertType` 指定变形成哪个新单位。该单位必须与原单位同属一类(载具变载具,步兵变步兵,战机变战机)。" +msgid "`Ammo.AutoConvertType` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type))." +msgstr "`Ammo.AutoConvertType` 指定变形成哪个新单位。新单位可以是任意科技类型,无论其属于哪个大类,包括 `BuildingType`(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。" msgid "" "Setting a negative number will disable the ammo count check, and when " @@ -3947,11 +3942,11 @@ msgid "" " which a unit converts automatically." msgstr "`Convert.Health.BelowPercent` 决定了触发单位自动变身的最大生命值百分比。" -msgid "" -"`Convert.Health` specify the new techno after the conversion. This unit " -"must be of the same type of the original (infantry -> infantry, vehicle " -"-> vehicle or aircraft -> aircraft)." -msgstr "`Convert.Health` 指定变形成哪个新单位。该单位必须与原单位同属一类(载具变载具,步兵变步兵,战机变战机)。" +msgid "`Convert.Health` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type))." +msgstr "`Convert.Health` 指定变形成哪个新单位。新单位可以是任意科技类型,无论其属于哪个大类,包括 `BuildingType`(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。" + +msgid "`Convert.Health.AfterBuildup`, if set to true, delays the conversion of a `BuildingType` until its buildup has finished." +msgstr "`Convert.Health.AfterBuildup` 设为 true 时,会将 `BuildingType` 的转换推迟到其建造动画结束之后。" msgid "" "Setting a negative number will disable the HP check, and when both checks" @@ -4212,6 +4207,39 @@ msgstr "当单位开始或停止隐形时,可以在其位置播放动画。" msgid "You can also set whether cloak units kick out parasites." msgstr "您也可以设置隐形单位是否踢出寄生者。" +msgid "Convert TechnoType into any other type" +msgstr "允许转换为任意其他科技类型" + +msgid "Now any TechnoType can be converted into any other TechnoType, no matter which category it belongs to, including to and from `BuildingType`s." +msgstr "现在任意科技类型都可以转换为任意其他科技类型,无论其属于哪个大类,包括与 `BuildingType` 之间的相互转换。" + +msgid "This applies to every form of type conversion: warhead and superweapon `Convert` pairs, `Convert.Deploy`, `Convert.Undeploy`, `Convert.Health`, `Ammo.AutoConvertType`, `Convert.HumanToComputer` / `Convert.ComputerToHuman` and Ares' type conversion." +msgstr "这一改动适用于所有类型的转换方式:弹头与超级武器的 `Convert` 组合、`Convert.Deploy`、`Convert.Undeploy`、`Convert.Health`、`Ammo.AutoConvertType`、`Convert.HumanToComputer` / `Convert.ComputerToHuman` 以及 Ares 的类型转换。" + +msgid "If the new type is a `BuildingType`, it is placed on the spot the source techno stands on, as long as its whole foundation fits there. If it does not fit, the conversion does not happen; a deploy order for `Convert.Deploy` is refused with the `NoDeploy` cursor and the *Cannot deploy here* EVA." +msgstr "若新类型是 `BuildingType`,它会被放置在源单位所处的位置,前提是其完整地基能够放下。若放不下则不会执行转换;`Convert.Deploy` 的部署命令会被拒绝并显示 `NoDeploy` 光标,同时播放 *Cannot deploy here* 语音。" + +msgid "Infantry that deploy or undeploy into a type other than an infantry type first walk to the centre of the cell they are occupying, so the conversion animation is played in the middle of the cell." +msgstr "步兵在部署或反部署为非步兵类型时,会先走到其所在单元格的中央,以便在单元格中央播放转换动画。" + +msgid "The new techno inherits the health percentage, veteran status, mind control, shields, invulnerability, attached effects, armor / firepower / speed multipliers and slave manager of the source techno. This also applies to vanilla `DeploysInto`, `UndeploysInto` and selling." +msgstr "新单位会继承源单位的生命值百分比、老兵等级、精神控制、护盾、无敌效果、附加效果、护甲/火力/速度倍增器与奴隶管理器。这一点同样适用于原版的 `DeploysInto`、`UndeploysInto` 与出售。" + +msgid "`Convert.Direction` determines the direction the new techno faces when converting from a `BuildingType`. If not set, the new techno keeps the facing of the source." +msgstr "`Convert.Direction` 决定从该 `BuildingType` 转换而出的新单位的朝向。未设置时新单位保持源单位的朝向。" + +msgid "The building animations played while converting to or from a building are customizable with the following tags." +msgstr "转换为建筑或从建筑转换时播放的建筑动画可以通过以下标签自定义。" + +msgid "`Convert.ShowBuildupFrom` makes the building being converted play its sell animation instead of disappearing immediately. Requires the building to be sellable." +msgstr "`Convert.ShowBuildupFrom` 使被转换的建筑播放其出售动画而不是立即消失。要求该建筑可以被出售。" + +msgid "`Convert.ShowBuildupTo` makes the newly created building play its buildup animation and plays `[AudioVisual] -> BuildingSlam` at its location instead of appearing finished right away. Requires `Buildup=` (the name of an SHP file) to be set for the new building type in `artmd.ini`." +msgstr "`Convert.ShowBuildupTo` 使新建的建筑播放其建造动画,并在其位置播放 `[AudioVisual] -> BuildingSlam`,而不是立即以完成状态出现。要求新建筑类型在 `artmd.ini` 中的 `[BuildingType]` 下 `Buildup=`(shp 文件名)非空。" + +msgid "`Convert.ShowBuildup` enables both of the above at once." +msgstr "`Convert.ShowBuildup` 同时启用以上两项。" + msgid "Convert TechnoType on owner house change" msgstr "根据操作者变形" @@ -4220,6 +4248,9 @@ msgid "" "computer or from computer to human." msgstr "现在你可以在操作者从人类改为 AI 玩家或从 AI 玩家改为人类玩家时执行单位转换。" +msgid "This also works for `BuildingType`s (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type))." +msgstr "该功能同样适用于 `BuildingType`(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。" + msgid "Custom tint on TechnoTypes" msgstr "染色" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 14889b1cf4..defafc7e8a 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -1122,6 +1122,15 @@ msgid "" msgstr "" "允许无部署序列的可部署步兵像可部署载具那样直接执行 `Convert.Deploy`(by Noble_Fish)" +msgid "[Convert TechnoType into any other type](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type) (by frg2089)" +msgstr "[允许转换为任意其他科技类型](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type)(by frg2089)" + +msgid "[Infantry deploying or undeploying into a type other than an infantry type now walk to the centre of their cell first](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (by frg2089)" +msgstr "[步兵部署或反部署为非步兵类型时会先走到所在单元格的中央](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying)(by frg2089)" + +msgid "Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over health percentage, veteran status, attribute multipliers and slave manager of the source techno (by frg2089)" +msgstr "原版的 `DeploysInto`、`UndeploysInto` 与出售现在同样会将源单位的生命值百分比、老兵等级、属性倍增器与奴隶管理器传递给新单位(by frg2089)" + msgid "Vanilla fixes:" msgstr "原版问题修复:" From 8f40eae88ab74d81748226caffb3f5e53c5fd9f9 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: Thu, 15 Oct 2026 12:31:39 +0800 Subject: [PATCH 6/9] feat(infantry): prevent conversion if not at cell center MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Infantry/Body.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Ext/Infantry/Body.cpp b/src/Ext/Infantry/Body.cpp index 465be2238c..6924a429db 100644 --- a/src/Ext/Infantry/Body.cpp +++ b/src/Ext/Infantry/Body.cpp @@ -82,9 +82,6 @@ bool InfantryExt::PrepareConvertToType(InfantryClass* pThis, TechnoTypeClass* pT if (!pCell) return false; - if (pCell->OccupationFlags & 0x01) - return false; - auto const pExt = InfantryExt::Fetch(pThis); // Already walking there for a conversion that was ordered before this one; keep it, so the @@ -126,6 +123,11 @@ void InfantryExt::UpdatePendingConvert(InfantryClass* pThis) if (pThis->Locomotor && pThis->Locomotor->Is_Moving()) return; + auto const current = pThis->GetCoords(); + auto const center = MapClass::Instance.TryGetCellAt(current)->GetCenterCoords(); + if (current != center) + return; + const bool canConvert = InfantryExt::CanConvertAtCellCenter(pThis) && TechnoExt::CanConvertToType(pThis, pToType); From 9c49567ae51ab67815405e52d00692d6831a4d07 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, 16 Oct 2026 09:37:04 +0800 Subject: [PATCH 7/9] feat(convert): add global defaults for Convert.ShowBuildup tags --- src/Ext/Rules/Body.cpp | 6 ++++++ src/Ext/Rules/Body.h | 6 ++++++ src/Ext/Techno/Body.cpp | 6 ++++-- src/Ext/TechnoType/Body.h | 12 ++++++------ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index f0dd63aaa3..845b588c41 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -221,6 +221,9 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->DestroyAnim_Random.Read(exINI, GameStrings::General, "DestroyAnim.Random"); this->UseDisguiseMovementSpeed.Read(exINI, GameStrings::General, "UseDisguiseMovementSpeed"); this->Convert_ResetMindControl.Read(exINI, GameStrings::General, "Convert.ResetMindControl"); + this->Convert_ShowBuildup.Read(exINI, GameStrings::General, "Convert.ShowBuildup"); + this->Convert_ShowBuildupFrom.Read(exINI, GameStrings::General, "Convert.ShowBuildupFrom"); + this->Convert_ShowBuildupTo.Read(exINI, GameStrings::General, "Convert.ShowBuildupTo"); this->BuildLimitGroup_ContentIfAnyMatch.Read(exINI, GameStrings::General, "BuildLimitGroup.ContentIfAnyMatch"); this->BuildLimitGroup_NotBuildableIfQueueMatch.Read(exINI, GameStrings::General, "BuildLimitGroup.NotBuildableIfQueueMatch"); this->DigitalDisplay_Health_FakeAtDisguise.Read(exINI, GameStrings::AudioVisual, "DigitalDisplay.Health.FakeAtDisguise"); @@ -843,6 +846,9 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->DestroyAnim_Random) .Process(this->UseDisguiseMovementSpeed) .Process(this->Convert_ResetMindControl) + .Process(this->Convert_ShowBuildup) + .Process(this->Convert_ShowBuildupFrom) + .Process(this->Convert_ShowBuildupTo) .Process(this->BuildLimitGroup_ContentIfAnyMatch) .Process(this->BuildLimitGroup_NotBuildableIfQueueMatch) .Process(this->ForceWeapon_InRange_TechnoOnly) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 9973cfaae6..c8e9802b77 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -127,6 +127,9 @@ class RulesExt Valueable DestroyAnim_Random; Valueable UseDisguiseMovementSpeed; Valueable Convert_ResetMindControl; + Valueable Convert_ShowBuildup; + Valueable Convert_ShowBuildupFrom; + Valueable Convert_ShowBuildupTo; Valueable BuildLimitGroup_ContentIfAnyMatch; Valueable BuildLimitGroup_NotBuildableIfQueueMatch; Valueable DigitalDisplay_Health_FakeAtDisguise; @@ -670,6 +673,9 @@ class RulesExt , DestroyAnim_Random { true } , UseDisguiseMovementSpeed { false } , Convert_ResetMindControl { false } + , Convert_ShowBuildup { false } + , Convert_ShowBuildupFrom { false } + , Convert_ShowBuildupTo { false } , BuildLimitGroup_ContentIfAnyMatch { false } , BuildLimitGroup_NotBuildableIfQueueMatch { false } , DigitalDisplay_Health_FakeAtDisguise { true } diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 98a86c57b5..a60f039992 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -483,7 +483,8 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) if (pFromBuilding && pFromBuilding->CanBeSold() && !pFromBuilding->IsStrange()) { auto const pFromBuildingExt = BuildingExt::Fetch(pFromBuilding); - showBuildupFrom = pFromBuildingExt->TypeExtData->Convert_ShowBuildupFrom || pFromBuildingExt->TypeExtData->Convert_ShowBuildup; + showBuildupFrom = pFromBuildingExt->TypeExtData->Convert_ShowBuildupFrom.Get(RulesExt::Global()->Convert_ShowBuildupFrom) + || pFromBuildingExt->TypeExtData->Convert_ShowBuildup.Get(RulesExt::Global()->Convert_ShowBuildup); } if (showBuildupFrom) @@ -504,7 +505,8 @@ static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) auto const pToBuildingExt = BuildingExt::Fetch(pToBuilding); bool showBuildupTo = false; if (pToBuilding->Type->Buildup) - showBuildupTo = pToBuildingExt->TypeExtData->Convert_ShowBuildupTo || pToBuildingExt->TypeExtData->Convert_ShowBuildup; + showBuildupTo = pToBuildingExt->TypeExtData->Convert_ShowBuildupTo.Get(RulesExt::Global()->Convert_ShowBuildupTo) + || pToBuildingExt->TypeExtData->Convert_ShowBuildup.Get(RulesExt::Global()->Convert_ShowBuildup); if (showBuildupTo) { diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 38ec3901da..a12d2a094d 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -499,9 +499,9 @@ class TechnoTypeExt : public ObjectTypeExt Valueable Convert_Health_AbovePercent; Valueable Convert_Health_BelowPercent; Nullable Convert_Health; - Valueable Convert_ShowBuildup; - Valueable Convert_ShowBuildupFrom; - Valueable Convert_ShowBuildupTo; + Nullable Convert_ShowBuildup; + Nullable Convert_ShowBuildupFrom; + Nullable Convert_ShowBuildupTo; Nullable ExitThroughRoof; Valueable PsychicDetectable; @@ -927,9 +927,9 @@ class TechnoTypeExt : public ObjectTypeExt , Convert_Health_AbovePercent { -1.0 } , Convert_Health_BelowPercent { -1.0 } , Convert_Health {} - , Convert_ShowBuildup { false } - , Convert_ShowBuildupFrom { false } - , Convert_ShowBuildupTo { false } + , Convert_ShowBuildup {} + , Convert_ShowBuildupFrom {} + , Convert_ShowBuildupTo {} , PsychicDetectable { true } From 493d551d288d13b91e313ac4d5c7123da3a0a084 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, 16 Oct 2026 09:56:22 +0800 Subject: [PATCH 8/9] docs(convert): document global defaults for Convert.ShowBuildup tags --- CREDITS.md | 1 + docs/New-or-Enhanced-Logics.md | 9 +++++++-- docs/Whats-New.md | 1 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 3 +++ docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po | 4 ++-- docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 3 +++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 8b31889b5b..93984aed46 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1001,6 +1001,7 @@ This page lists all the individual contributions to the project by their author. - Convert TechnoType into any other type, including to and from `BuildingType`s - Infantry walk to the centre of their cell before converting into another type - Carry over health percentage, veteran status, attribute multipliers and slave manager on conversion + - Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo` - **weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral houses - **dh381-1**: - Detonate ivan bomb on impact diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 89440542aa..452009c6a1 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1874,18 +1874,23 @@ Cloak.KickOutParasite= ; boolean, default to [General] -> Cloak.KickOutPa - Infantry that deploy or undeploy into a type other than an infantry type first walk to the centre of the cell they are occupying, so the conversion animation is played in the middle of the cell. - The new techno inherits the health percentage, veteran status, mind control, shields, invulnerability, attached effects, armor / firepower / speed multipliers and slave manager of the source techno. This also applies to vanilla `DeploysInto`, `UndeploysInto` and selling. - `Convert.Direction` determines the direction the new techno faces when converting from a `BuildingType`. If not set, the new techno keeps the facing of the source. - - The building animations played while converting to or from a building are customizable with the following tags. + - The building animations played while converting to or from a building are customizable with the following tags. Each of them can also be set in `[General]` to define its global default value. - `Convert.ShowBuildupFrom` makes the building being converted play its sell animation instead of disappearing immediately. Requires the building to be sellable. - `Convert.ShowBuildupTo` makes the newly created building play its buildup animation and plays `[AudioVisual] -> BuildingSlam` at its location instead of appearing finished right away. Requires `Buildup=` (the name of an SHP file) to be set for the new building type in `artmd.ini`. - `Convert.ShowBuildup` enables both of the above at once. In `rulesmd.ini`: ```ini -[SOMETECHNO] ; TechnoType +[General] Convert.ShowBuildup=false ; boolean Convert.ShowBuildupFrom=false ; boolean Convert.ShowBuildupTo=false ; boolean +[SOMETECHNO] ; TechnoType +Convert.ShowBuildup= ; boolean, default to [General] -> Convert.ShowBuildup +Convert.ShowBuildupFrom= ; boolean, default to [General] -> Convert.ShowBuildupFrom +Convert.ShowBuildupTo= ; boolean, default to [General] -> Convert.ShowBuildupTo + [SOMEBUILDING] ; BuildingType Convert.Direction= ; Direction type (integers from 0-255) ``` diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 64156cd7bb..42efb6bdf7 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -2056,6 +2056,7 @@ HideShakeEffects=false ; boolean - [Convert TechnoType into any other type](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type) (by frg2089) - [Infantry deploying or undeploying into a type other than an infantry type now walk to the centre of their cell first](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (by frg2089) - Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over health percentage, veteran status, attribute multipliers and slave manager of the source techno (by frg2089) +- Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo` (by frg2089) #### Vanilla fixes: - Fixed the bug where a building with `Factory=BuildingType` owned by the AI did not play `ProductionAnim` when placing a produced building (by Noble_Fish) diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index b796bc442b..a1eb2e7b4a 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3445,6 +3445,9 @@ msgstr "步兵在转换为其他类型前会先走到所在单元格的中央" msgid "Carry over health percentage, veteran status, attribute multipliers and slave manager on conversion" msgstr "转换时继承生命值百分比、老兵等级、属性倍增器与奴隶管理器" +msgid "Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo`" +msgstr "`Convert.ShowBuildup`、`Convert.ShowBuildupFrom` 和 `Convert.ShowBuildupTo` 的全局默认值" + msgid "" "**weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral " "houses" 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 68b8b64fc4..237ac9e2c0 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 @@ -4228,8 +4228,8 @@ msgstr "新单位会继承源单位的生命值百分比、老兵等级、精神 msgid "`Convert.Direction` determines the direction the new techno faces when converting from a `BuildingType`. If not set, the new techno keeps the facing of the source." msgstr "`Convert.Direction` 决定从该 `BuildingType` 转换而出的新单位的朝向。未设置时新单位保持源单位的朝向。" -msgid "The building animations played while converting to or from a building are customizable with the following tags." -msgstr "转换为建筑或从建筑转换时播放的建筑动画可以通过以下标签自定义。" +msgid "The building animations played while converting to or from a building are customizable with the following tags. Each of them can also be set in `[General]` to define its global default value." +msgstr "转换为建筑或从建筑转换时播放的建筑动画可以通过以下标签自定义。这些标签也都可以在 `[General]` 中设置以定义其全局默认值。" msgid "`Convert.ShowBuildupFrom` makes the building being converted play its sell animation instead of disappearing immediately. Requires the building to be sellable." msgstr "`Convert.ShowBuildupFrom` 使被转换的建筑播放其出售动画而不是立即消失。要求该建筑可以被出售。" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index defafc7e8a..d2334f83f7 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -1131,6 +1131,9 @@ msgstr "[步兵部署或反部署为非步兵类型时会先走到所在单元 msgid "Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over health percentage, veteran status, attribute multipliers and slave manager of the source techno (by frg2089)" msgstr "原版的 `DeploysInto`、`UndeploysInto` 与出售现在同样会将源单位的生命值百分比、老兵等级、属性倍增器与奴隶管理器传递给新单位(by frg2089)" +msgid "Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo` (by frg2089)" +msgstr "`Convert.ShowBuildup`、`Convert.ShowBuildupFrom` 和 `Convert.ShowBuildupTo` 的全局默认值(by frg2089)" + msgid "Vanilla fixes:" msgstr "原版问题修复:" From 63f0cf01627621be67635c08f841284913dd4ec4 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, 16 Oct 2026 12:51:49 +0800 Subject: [PATCH 9/9] fix(health-convert): skip conversion when Convert.Health.AfterBuildup=true and buildings are in Selling state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 舰队的偶像-岛风酱! --- src/Ext/Techno/Body.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index a60f039992..cf98a6af2e 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -682,6 +682,7 @@ void TechnoExt::HealthAutoConvertActions() { case Mission::None: case Mission::Construction: + case Mission::Selling: return; } }