From 8c6d3ce028b62e54564b3319ebe2d331cc137e52 Mon Sep 17 00:00:00 2001 From: NukeAtty Date: Tue, 4 Aug 2026 22:55:56 +0800 Subject: [PATCH 1/2] Add per-weapon VXL turret switching for MultiWeapon units Units with MultiWeapon=yes and TurretCount>0 now swap their VXL turret and barrel per weapon index. Extra turrets are loaded by naming convention (turN.vxl / barlN.vxl) with no art changes; weapons beyond min(WeaponCount, TurretCount) fall back to the default turret. --- src/Ext/Techno/Hooks.cpp | 38 +++++ src/Ext/TechnoType/Hooks.MatrixOp.cpp | 195 +++++++++++++++++++------- src/Ext/UnitType/Body.cpp | 141 +++++++++++++++++++ src/Ext/UnitType/Body.h | 16 +++ 4 files changed, 343 insertions(+), 47 deletions(-) diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index 2ed3b4c243..4d80f0234e 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #pragma region GetTechnoType @@ -78,6 +79,35 @@ DEFINE_HOOK(0x736480, UnitClass_AI, 0x6) pThis->CurrentFiringFrame = animCounter; } + // Make the displayed VXL turret / barrel follow the currently selected + // weapon for MultiWeapon + TurretCount units. When the unit has no target + // (e.g. after issuing an order with a mouse click) the turret stays on the + // last used weapon instead of snapping back to the default turret. + if (pThis->IsAlive && pExt->GetTypeExtData()->IsMultiWeaponTurretEnabled()) + { + if (pThis->Target) + { + const int weaponIndex = pThis->SelectWeapon(pThis->Target); + const auto pType = pThis->GetTechnoType(); + + // Keep the raw weapon index. Weapons beyond the loaded turret count + // (min(WeaponCount, TurretCount)) are handled by the drawing code and + // fall back to the default turret, so they must not be clamped onto the + // last valid turret here. + const int turret = Math::clamp(weaponIndex, 0, Math::max(pType->WeaponCount - 1, 0)); + + if (pThis->CurrentTurretNumber != turret) + { + Debug::Log("[MultiWeaponTurret] %s SelectWeapon=%d Target=%p -> CurrentTurretNumber=%d (WeaponCount=%d TurretCount=%d).\n", + pType->ID, weaponIndex, static_cast(pThis->Target), turret, pType->WeaponCount, pType->TurretCount); + } + + pThis->CurrentTurretNumber = turret; + } + // No target: keep the last used weapon's turret (CurrentTurretNumber is + // left untouched so the displayed turret does not jump around). + } + return 0; } @@ -228,6 +258,14 @@ DEFINE_HOOK(0x6FA540, TechnoClass_AI_ChargeTurret, 0x6) GET(TechnoClass*, pThis, ESI); + // Do not let the charge turret logic touch CurrentTurretNumber for units + // that use the MultiWeapon turret switching feature. + if (auto const pUnitType = abstract_cast(pThis->GetTechnoType())) + { + if (UnitTypeExt::Fetch(pUnitType)->IsMultiWeaponTurretEnabled()) + return SkipGameCode; + } + if (pThis->ChargeTurretDelay <= 0) { pThis->CurrentTurretNumber = 0; diff --git a/src/Ext/TechnoType/Hooks.MatrixOp.cpp b/src/Ext/TechnoType/Hooks.MatrixOp.cpp index ad8cdd8dca..5cfe2d3c9e 100644 --- a/src/Ext/TechnoType/Hooks.MatrixOp.cpp +++ b/src/Ext/TechnoType/Hooks.MatrixOp.cpp @@ -69,7 +69,7 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) GET(UnitClass* const, pThis, EBP); GET(UnitTypeClass* const, pDrawType, EBX); GET_STACK(const bool, haveTurretCache, STACK_OFFSET(0x1C4, -0x1B3)); - GET_STACK(const bool, haveBar, STACK_OFFSET(0x1C4, -0x1B2)); + GET_STACK(const bool, gameHaveBar, STACK_OFFSET(0x1C4, -0x1B2)); GET(const bool, haveBarrelCache, EAX); REF_STACK(Matrix3D, drawMatrix, STACK_OFFSET(0x1C4, -0x130)); GET_STACK(const int, flags, STACK_OFFSET(0x1C4, -0x198)); @@ -85,34 +85,94 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) const auto pExt = UnitExt::Fetch(pThis); const auto pDrawTypeExt = UnitTypeExt::Fetch(pDrawType); const bool notChargeTurret = pThis->Type->TurretCount <= 0 || pThis->Type->IsGattling; + const bool isMultiWeaponTurret = pDrawTypeExt->IsMultiWeaponTurretEnabled(); - auto getTurretVoxel = [pDrawType, notChargeTurret, currentTurretNumber]() -> VoxelStruct* + // The vanilla / charge-turret selection. When TurretCount > 0 the game keeps + // the turret model in ChargerTurrets instead of TurretVoxel, so the fallback + // for the MultiWeapon switching must reuse this exact logic. + auto getVanillaTurretVoxel = [pDrawType, notChargeTurret, currentTurretNumber]() -> VoxelStruct* { - if (notChargeTurret) - return &pDrawType->TurretVoxel; + auto pResult = &pDrawType->TurretVoxel; + + if (!notChargeTurret) + { + const int index = Math::clamp(currentTurretNumber, 0, Math::max(pDrawType->TurretCount - 1, 0)); + + // Not considering the situation where there is no Ares and the limit is exceeded + if (index < 18 || !AresHelper::CanUseAres) + pResult = &pDrawType->ChargerTurrets[index]; + else + { + auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); + pResult = &aresTypeExt->ChargerTurrets[index - 18]; + } - // Not considering the situation where there is no Ares and the limit is exceeded - if (currentTurretNumber < 18 || !AresHelper::CanUseAres) - return &pDrawType->ChargerTurrets[currentTurretNumber]; + // The game may only keep the model in the first slot; always fall + // back to it so a visible turret is guaranteed. + if (!(pResult->VXL && pResult->HVA) + && pDrawType->ChargerTurrets[0].VXL && pDrawType->ChargerTurrets[0].HVA) + { + pResult = &pDrawType->ChargerTurrets[0]; + } + } - auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); - return &aresTypeExt->ChargerTurrets[currentTurretNumber - 18]; + return (pResult->VXL && pResult->HVA) ? pResult : &pDrawType->TurretVoxel; }; - const auto pTurretVoxel = getTurretVoxel(); + const auto pVanillaTurret = getVanillaTurretVoxel(); + const auto pTurretVoxel = isMultiWeaponTurret + ? pDrawTypeExt->GetMultiWeaponTurret(currentTurretNumber, pVanillaTurret) + : pVanillaTurret; - auto getBarrelVoxel = [pDrawType, notChargeTurret, currentTurretNumber]() -> VoxelStruct* + auto getVanillaBarrelVoxel = [pDrawType, notChargeTurret, currentTurretNumber]() -> VoxelStruct* { - if (notChargeTurret) - return &pDrawType->BarrelVoxel; + auto pResult = &pDrawType->BarrelVoxel; - // Not considering the situation where there is no Ares and the limit is exceeded - if (currentTurretNumber < 18 || !AresHelper::CanUseAres) - return &pDrawType->ChargerBarrels[currentTurretNumber]; + if (!notChargeTurret) + { + const int index = Math::clamp(currentTurretNumber, 0, Math::max(pDrawType->TurretCount - 1, 0)); + + // Not considering the situation where there is no Ares and the limit is exceeded + if (index < 18 || !AresHelper::CanUseAres) + pResult = &pDrawType->ChargerBarrels[index]; + else + { + auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); + pResult = &aresTypeExt->ChargerBarrels[index - 18]; + } + + if (!(pResult->VXL && pResult->HVA) + && pDrawType->ChargerBarrels[0].VXL && pDrawType->ChargerBarrels[0].HVA) + { + pResult = &pDrawType->ChargerBarrels[0]; + } + } - auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); - return &aresTypeExt->ChargerBarrels[currentTurretNumber - 18]; + return (pResult->VXL && pResult->HVA) ? pResult : &pDrawType->BarrelVoxel; }; - const auto pBarrelVoxel = haveBar ? getBarrelVoxel() : nullptr; + const auto pVanillaBarrel = getVanillaBarrelVoxel(); + const auto pBarrelVoxel = (gameHaveBar || isMultiWeaponTurret) + ? (isMultiWeaponTurret + ? pDrawTypeExt->GetMultiWeaponBarrel(currentTurretNumber, pVanillaBarrel) + : pVanillaBarrel) + : nullptr; + const bool haveBar = isMultiWeaponTurret + ? (pBarrelVoxel && pBarrelVoxel->VXL && pBarrelVoxel->HVA) + : gameHaveBar; + + // The frame index needs to be recalculated when the custom turret / barrel + // HVA frame counts differ from the vanilla ones. The vanilla voxels keep the + // frame index the game already computed. + const bool customTurret = isMultiWeaponTurret && pTurretVoxel != pVanillaTurret; + const bool customBarrel = isMultiWeaponTurret && pBarrelVoxel != pVanillaBarrel; + + int turretHvaFrame = hvaFrameIdx; + int barrelHvaFrame = hvaFrameIdx; + + if (customTurret && pTurretVoxel && pTurretVoxel->HVA) + turretHvaFrame = pThis->TurretAnimFrame % pTurretVoxel->HVA->FrameCount; + + if (customBarrel && pBarrelVoxel && pBarrelVoxel->HVA) + barrelHvaFrame = pThis->TurretAnimFrame % pBarrelVoxel->HVA->FrameCount; constexpr BlitterFlags blit = BlitterFlags::Alpha | BlitterFlags::Flat; // When in recoiling or have no cache, need to recalculate drawing matrix @@ -127,8 +187,9 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) const auto pTurData = pDrawType->TurretRecoil ? ((turIdx < 0) ? &pThis->TurretRecoil : &pExt->ExtraTurretRecoil[turIdx]) : nullptr; const bool turretInRecoil = pTurData && pTurData->State != RecoilData::RecoilState::Inactive; - // When in recoiling or is not main turret, need to bypass cache and draw without saving - const bool turShouldRedraw = turretInRecoil || turIdx >= 0; + // When in recoiling, is not main turret, or the turret VXL switches + // per weapon, need to bypass cache and draw without saving + const bool turShouldRedraw = turretInRecoil || turIdx >= 0 || isMultiWeaponTurret; const auto turKey = turShouldRedraw ? -1 : flags; const auto turCache = turShouldRedraw ? nullptr : &pDrawType->VoxelTurretWeaponCache; @@ -161,8 +222,9 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) const auto pBrlData = pDrawType->TurretRecoil ? ((idx < 0) ? &pThis->BarrelRecoil : &pExt->ExtraBarrelRecoil[idx]) : nullptr; const bool barrelInRecoil = pBrlData && pBrlData->State != RecoilData::RecoilState::Inactive; - // When in recoiling or is not main barrel, need to bypass cache and draw without saving - const bool brlShouldRedraw = turretInRecoil || barrelInRecoil || idx >= 0; + // When in recoiling, is not main barrel, or the barrel VXL + // switches per weapon, need to bypass cache and draw without saving + const bool brlShouldRedraw = turretInRecoil || barrelInRecoil || idx >= 0 || isMultiWeaponTurret; const auto brlKey = brlShouldRedraw ? -1 : flags; const auto brlCache = brlShouldRedraw ? nullptr : &pDrawType->VoxelTurretBarrelCache; @@ -183,7 +245,7 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) auto mtx_barrel = (shouldRedraw || brlShouldRedraw) ? getBarrelMatrix() : mtx; // draw barrel - pThis->Draw_A_VXL(pBarrelVoxel, hvaFrameIdx, brlKey, brlCache, rect, center, &mtx_barrel, brightness, blit, 0); + pThis->Draw_A_VXL(pBarrelVoxel, barrelHvaFrame, brlKey, brlCache, rect, center, &mtx_barrel, brightness, blit, 0); }; auto drawBarrels = [&drawBarrel, pDrawTypeExt, turretDir]() @@ -220,7 +282,7 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) if (barrelOverTechno) { // draw turret - pThis->Draw_A_VXL(pTurretVoxel, hvaFrameIdx, turKey, turCache, rect, center, &mtx_turret, brightness, blit, 0); + pThis->Draw_A_VXL(pTurretVoxel, turretHvaFrame, turKey, turCache, rect, center, &mtx_turret, brightness, blit, 0); if (haveBar) drawBarrels(); @@ -231,7 +293,7 @@ DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6) drawBarrels(); // draw turret - pThis->Draw_A_VXL(pTurretVoxel, hvaFrameIdx, turKey, turCache, rect, center, &mtx_turret, brightness, blit, 0); + pThis->Draw_A_VXL(pTurretVoxel, turretHvaFrame, turKey, turCache, rect, center, &mtx_turret, brightness, blit, 0); } }; @@ -803,21 +865,41 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5) if (main_vxl == &pDrawType->TurretVoxel || (notUseTurretShadow && !pDrawTypeExt->TurretShadow.Get(RulesExt::Global()->DrawTurretShadow))) return SkipDrawing; - auto getTurretVoxel = [pDrawType](int idx) ->VoxelStruct* + // The vanilla / charge-turret selection. When TurretCount > 0 the game keeps + // the turret model in ChargerTurrets instead of TurretVoxel, so the fallback + // for the MultiWeapon switching must reuse this exact logic. + auto getVanillaTurretVoxel = [pDrawType](int idx) ->VoxelStruct* { - if (pDrawType->TurretCount == 0 || pDrawType->IsGattling || idx < 0) - return &pDrawType->TurretVoxel; - - if (idx < 18) - return &pDrawType->ChargerTurrets[idx]; + auto pResult = &pDrawType->TurretVoxel; - if (AresHelper::CanUseAres) + if (pDrawType->TurretCount > 0 && !pDrawType->IsGattling) { - auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); - return &aresTypeExt->ChargerTurrets[idx - 18]; + const int index = Math::clamp(idx, 0, Math::max(pDrawType->TurretCount - 1, 0)); + + if (index < 18) + pResult = &pDrawType->ChargerTurrets[index]; + else if (AresHelper::CanUseAres) + { + auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); + pResult = &aresTypeExt->ChargerTurrets[index - 18]; + } + + // The game may only keep the model in the first slot; always fall + // back to it so a visible turret is guaranteed. + if (!(pResult->VXL && pResult->HVA) + && pDrawType->ChargerTurrets[0].VXL && pDrawType->ChargerTurrets[0].HVA) + { + pResult = &pDrawType->ChargerTurrets[0]; + } } - return nullptr; + return (pResult->VXL && pResult->HVA) ? pResult : &pDrawType->TurretVoxel; + }; + auto getTurretVoxel = [pDrawTypeExt, getVanillaTurretVoxel](int idx) ->VoxelStruct* + { + return pDrawTypeExt->IsMultiWeaponTurretEnabled() + ? pDrawTypeExt->GetMultiWeaponTurret(idx, getVanillaTurretVoxel(idx)) + : getVanillaTurretVoxel(idx); }; const auto pTurretVoxel = getTurretVoxel(pThis->CurrentTurretNumber); @@ -827,25 +909,44 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5) if (vxlIndexKey.Is_Valid_Key()) vxlIndexKey.MinorVoxel.TurretFacing = pThis->SecondaryFacing.Current().GetFacing<32>(); - auto getBarrelVoxel = [pDrawType](int idx)->VoxelStruct* + auto getVanillaBarrelVoxel = [pDrawType](int idx)->VoxelStruct* { - if (pDrawType->TurretCount == 0 || pDrawType->IsGattling || idx < 0) - return &pDrawType->BarrelVoxel; + auto pResult = &pDrawType->BarrelVoxel; - if (idx < 18) - return &pDrawType->ChargerBarrels[idx]; - - if (AresHelper::CanUseAres) + if (pDrawType->TurretCount > 0 && !pDrawType->IsGattling) { - auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); - return &aresTypeExt->ChargerBarrels[idx - 18]; + const int index = Math::clamp(idx, 0, Math::max(pDrawType->TurretCount - 1, 0)); + + if (index < 18) + pResult = &pDrawType->ChargerBarrels[index]; + else if (AresHelper::CanUseAres) + { + auto* aresTypeExt = reinterpret_cast(pDrawType->align_2FC); + pResult = &aresTypeExt->ChargerBarrels[index - 18]; + } + + if (!(pResult->VXL && pResult->HVA) + && pDrawType->ChargerBarrels[0].VXL && pDrawType->ChargerBarrels[0].HVA) + { + pResult = &pDrawType->ChargerBarrels[0]; + } } - return nullptr; + return (pResult->VXL && pResult->HVA) ? pResult : &pDrawType->BarrelVoxel; + }; + auto getBarrelVoxel = [pDrawTypeExt, getVanillaBarrelVoxel](int idx)->VoxelStruct* + { + return pDrawTypeExt->IsMultiWeaponTurretEnabled() + ? pDrawTypeExt->GetMultiWeaponBarrel(idx, getVanillaBarrelVoxel(idx)) + : getVanillaBarrelVoxel(idx); }; const auto pBarrelVoxel = getBarrelVoxel(pThis->CurrentTurretNumber); - const auto haveBar = pBarrelVoxel && pBarrelVoxel->VXL && pBarrelVoxel->HVA && !pBarrelVoxel->VXL->Initialized; + // When the turret VXL switches per weapon the turret shadow is always drawn + // standalone (no cache), so the barrel shadow has to be drawn on its own. + const bool isMultiWeaponShadow = pDrawTypeExt->IsMultiWeaponTurretEnabled(); + const auto haveBar = pBarrelVoxel && pBarrelVoxel->VXL && pBarrelVoxel->HVA + && (isMultiWeaponShadow || !pBarrelVoxel->VXL->Initialized); auto pCache = &pDrawType->VoxelShadowCache; const auto pExt = UnitExt::Fetch(pThis); diff --git a/src/Ext/UnitType/Body.cpp b/src/Ext/UnitType/Body.cpp index 5f6cd33986..312f802b01 100644 --- a/src/Ext/UnitType/Body.cpp +++ b/src/Ext/UnitType/Body.cpp @@ -1,5 +1,7 @@ #include "Body.h" +#include + UnitTypeExt::ExtContainer UnitTypeExt::ExtMap; void UnitTypeExt::ApplyTurretOffsetUnit(Matrix3D* mtx, double factor, int turIdx) @@ -13,6 +15,139 @@ void UnitTypeExt::ApplyTurretOffsetUnit(Matrix3D* mtx, double factor, int turIdx mtx->Translate(x, y, z); } +// ============================= +// MultiWeapon VXL turret / barrel + +static VoxelStruct LoadMultiWeaponVoxelStruct(const char* pBaseName) +{ + char vxlFile[0x100]; + char hvaFile[0x100]; + _snprintf_s(vxlFile, _TRUNCATE, "%s.vxl", pBaseName); + _snprintf_s(hvaFile, _TRUNCATE, "%s.hva", pBaseName); + + CCFileClass vxlCC(vxlFile); + CCFileClass hvaCC(hvaFile); + + if (!vxlCC.Exists() || !hvaCC.Exists()) + return { nullptr, nullptr }; + + auto pVXL = GameCreate(&vxlCC, false); + auto pHVA = GameCreate(&hvaCC); + + // Match the game's own LoadVoxel behaviour: Initialized is only set once the + // voxel has gone through lighting setup at first draw, so it cannot be used + // here to validate the freshly loaded file. + if (!pVXL || !pHVA || pHVA->LoadedFailed) + { + if (pVXL) + GameDelete(pVXL); + if (pHVA) + GameDelete(pHVA); + return { nullptr, nullptr }; + } + + return { pVXL, pHVA }; +} + +void UnitTypeExt::LoadMultiWeaponTurrets() +{ + auto pThis = this->OwnerObject(); + const bool enabled = this->MultiWeapon.Get() && pThis->TurretCount > 0; + const int count = enabled ? Math::min(pThis->WeaponCount, pThis->TurretCount) : 0; + + if (enabled) + { + // Keep the native turret -> weapon mapping consistent with the weapon + // index used as CurrentTurretNumber, so GetTurretWeapon() returns the + // weapon that the currently displayed turret corresponds to. + const int weaponCount = Math::min(count, TechnoTypeClass::MaxWeapons); + + for (int i = 0; i < weaponCount; ++i) + pThis->TurretWeapon[i] = i; + } + + if (this->MultiWeaponTurrets.size() != (count > 0 ? static_cast(count - 1) : 0)) + { + for (auto& vs : this->MultiWeaponTurrets) + { + if (vs.VXL) + GameDelete(vs.VXL); + if (vs.HVA) + GameDelete(vs.HVA); + } + + this->MultiWeaponTurrets.clear(); + this->MultiWeaponBarrels.clear(); + + for (int i = 1; i < count; ++i) + { + char turretName[0x100]; + char barrelName[0x100]; + _snprintf_s(turretName, _TRUNCATE, "%stur%u", pThis->ImageFile, i); + _snprintf_s(barrelName, _TRUNCATE, "%sbarl%u", pThis->ImageFile, i); + + this->MultiWeaponTurrets.emplace_back(LoadMultiWeaponVoxelStruct(turretName)); + this->MultiWeaponBarrels.emplace_back(LoadMultiWeaponVoxelStruct(barrelName)); + } + + if (enabled) + Debug::Log("UnitTypeExt: Loaded %zu extra VXL turrets / %zu barrels for %s.\n", + this->MultiWeaponTurrets.size(), this->MultiWeaponBarrels.size(), pThis->ID); + } +} + +bool UnitTypeExt::IsMultiWeaponTurretEnabled() const +{ + return this->MultiWeapon.Get() && this->OwnerObject()->TurretCount > 0; +} + +VoxelStruct* UnitTypeExt::GetMultiWeaponTurret(int weaponIndex, VoxelStruct* pFallback) +{ + if (weaponIndex <= 0) + return pFallback; + + const size_t index = static_cast(weaponIndex - 1); + + if (index >= this->MultiWeaponTurrets.size()) + return pFallback; + + auto& vs = this->MultiWeaponTurrets[index]; + return (vs.VXL && vs.HVA) ? &vs : pFallback; +} + +VoxelStruct* UnitTypeExt::GetMultiWeaponBarrel(int weaponIndex, VoxelStruct* pFallback) +{ + if (weaponIndex <= 0) + return pFallback; + + const size_t index = static_cast(weaponIndex - 1); + + if (index >= this->MultiWeaponBarrels.size()) + return pFallback; + + auto& vs = this->MultiWeaponBarrels[index]; + return (vs.VXL && vs.HVA) ? &vs : pFallback; +} + +UnitTypeExt::~UnitTypeExt() +{ + for (auto& vs : this->MultiWeaponTurrets) + { + if (vs.VXL) + GameDelete(vs.VXL); + if (vs.HVA) + GameDelete(vs.HVA); + } + + for (auto& vs : this->MultiWeaponBarrels) + { + if (vs.VXL) + GameDelete(vs.VXL); + if (vs.HVA) + GameDelete(vs.HVA); + } +} + // ============================= // load / save @@ -135,6 +270,9 @@ void UnitTypeExt::LoadFromINIFile(CCINIClass* const pINI) { this->ExtraTurretCount = 0; } + + // Load the extra VXL turrets / barrels for MultiWeapon + TurretCount units. + this->LoadMultiWeaponTurrets(); } template @@ -207,6 +345,9 @@ void UnitTypeExt::LoadFromStream(PhobosStreamReader& Stm) { TechnoTypeExt::LoadFromStream(Stm); this->Serialize(Stm); + + // Reload the extra VXL turrets / barrels after a savegame load. + this->LoadMultiWeaponTurrets(); } void UnitTypeExt::SaveToStream(PhobosStreamWriter& Stm) diff --git a/src/Ext/UnitType/Body.h b/src/Ext/UnitType/Body.h index f232759a20..b29e4f1ff7 100644 --- a/src/Ext/UnitType/Body.h +++ b/src/Ext/UnitType/Body.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include // Concrete leaf extension for UnitTypeClass. @@ -83,6 +84,12 @@ class UnitTypeExt final : public TechnoTypeExt std::vector ExtraTurretOffsets; Valueable BurstPerTurret; + // VXL turrets / barrels that get swapped in per weapon index when MultiWeapon + // + TurretCount is in use. Index 0 of a weapon is always the vanilla turret, + // these hold the extra ones (weapon 1 -> MultiWeaponTurrets[0] and so on). + std::vector MultiWeaponTurrets; + std::vector MultiWeaponBarrels; + explicit UnitTypeExt(UnitTypeClass* const OwnerObject) : TechnoTypeExt(OwnerObject) , SinkSpeed {} , Sinkable {} @@ -173,8 +180,17 @@ class UnitTypeExt final : public TechnoTypeExt virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; + virtual ~UnitTypeExt() override; + void ApplyTurretOffsetUnit(Matrix3D* mtx, double factor, int turIdx); + // Loads the extra VXL turrets / barrels (named turN.vxl and + // barlN.vxl) for units using MultiWeapon + TurretCount. + void LoadMultiWeaponTurrets(); + bool IsMultiWeaponTurretEnabled() const; + VoxelStruct* GetMultiWeaponTurret(int weaponIndex, VoxelStruct* pFallback); + VoxelStruct* GetMultiWeaponBarrel(int weaponIndex, VoxelStruct* pFallback); + private: template void Serialize(T& Stm); From 6cd03937e12b925c893d49b1b352e82e7fae2291 Mon Sep 17 00:00:00 2001 From: NukeAtty Date: Wed, 5 Aug 2026 19:11:02 +0800 Subject: [PATCH 2/2] Add documentation for per-weapon VXL turret switching --- .gitignore | 3 +++ CREDITS.md | 4 ++- docs/New-or-Enhanced-Logics.md | 3 +++ docs/Whats-New.md | 1 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 10 +++++-- .../LC_MESSAGES/New-or-Enhanced-Logics.po | 26 +++++++++++++++++++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 5 ++++ 7 files changed, 49 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2f2b9c2dde..72f6c27496 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ out # Python bytecode caches __pycache__/ *.py[cod] + +# Personal agent instructions +/AGENTS.md diff --git a/CREDITS.md b/CREDITS.md index 4e95706f1c..59d04c1ed0 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -936,4 +936,6 @@ This page lists all the individual contributions to the project by their author. - Interop export interface for accessing scenario local/global variables - 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 +- **Nuke**: + - Reload speed adjustment on promotion + - Switch Voxel Turret Depends on MultiWeapon diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 2266f70d42..66115e5058 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2179,6 +2179,9 @@ MindControlSize=1 ; integer - `MultiWeapon.SelectCount` determines the number of weapons that can be selected by default weapon selection logic. Notice that higher number is bad for performance. - If the number is smaller than the total amount of weapons, the ones with smaller indices will be picked. - Other weapons can still be used for logic that specify a weapon index, such as [ForceWeapon](#forcing-specific-weapon-against-certain-targets). + - For VXL vehicles that also set `TurretCount>0`, the turret and barrel VXL models swap based on the currently selected weapon. Weapon index `N` uses `turN.vxl` / `barlN.vxl` (weapon 1 -> `tur.vxl`, weapon 2 -> `tur1.vxl`, weapon 3 -> `tur2.vxl`, and so on). No `artmd.ini` changes are needed - just place the extra `.vxl` / `.hva` files into the game directory following the naming convention. + - The number of effective turrets is the smaller of `WeaponCount` and `TurretCount`; a weapon whose index is out of this range falls back to the default turret. Elite weapons follow the same per-index rule. When the unit has no target, the turret stays on the last used weapon instead of reverting to the default. + - Turret and barrel shadows are drawn as usual when `UseTurretShadow=yes`. In `rulesmd.ini`: ```ini diff --git a/docs/Whats-New.md b/docs/Whats-New.md index efdb5675bb..0e0349204e 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -27,6 +27,7 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] #### Changes to Phobos behavior +- VXL vehicles can now swap their turret and barrel models per weapon index by using `MultiWeapon` together with `TurretCount`. (by Nuke) - When the value of the `ProjectileRange` (Ares feature) field on a weapon is greater than 0, weapon range modifiers will be applied. This behavior can be disabled by explicitly setting `ProjectileRange.ApplyModifiers=false` on that weapon; if `ProjectileRange.ApplyModifiers` is not explicitly set, it will fall back to the global default `[CombatDamage] -> ProjectileRange.ApplyModifiers`. - `Splits.TargetCellRange` < 0 now applies special behaviour where the projectile does not consider nearby cells as additional targets if there are not enough techno targets to match `Cluster` count at all. - Combat light customizations introduced a bug that removed vanilla behaviour of ignoring detail level / framerate checks for colored combat light. This bug has been fixed but the previous behaviour can be restored by setting `CombatLightDetailLevel.CheckColored` on Warhead or globally under `[AudioVisual]`. diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 6a92c5b18e..861d2ba21e 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3198,6 +3198,12 @@ msgstr "为 `BannerType` 添加了 `ClampToScreen` 标签以控制横幅位置 msgid "**obsidianus** - Automatic conversion based on health" msgstr "**obsidianus** - 自动根据血量变形" -msgid "**Nuke** - Reload speed adjustment on promotion" -msgstr "**Nuke** - 升级时调整装填速率" +msgid "**Nuke**:" +msgstr "**Nuke**:" + +msgid "Reload speed adjustment on promotion" +msgstr "升级时调整装填速率" + +msgid "Switch Voxel Turret Depends on MultiWeapon" +msgstr "根据 MultiWeapon 切换体素炮塔" 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 e339391b69..6b3ed05454 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 @@ -4815,6 +4815,32 @@ msgstr "" "其他武器仍可通过 [对特定目标使用特定武器](#forcing-specific-weapon-against-certain-targets) " "等指定武器索引序号的逻辑来使用。" +msgid "" +"For VXL vehicles that also set `TurretCount>0`, the turret and barrel VXL " +"models swap based on the currently selected weapon. Weapon index `N` uses " +"`turN.vxl` / `barlN.vxl` (weapon 1 -> " +"`tur.vxl`, weapon 2 -> `tur1.vxl`, weapon 3 -> " +"`tur2.vxl`, and so on). No `artmd.ini` changes are needed - " +"just place the extra `.vxl` / `.hva` files into the game directory " +"following the naming convention." +msgstr "" +"对于同时设置了 `TurretCount>0` 的 VXL 载具,炮塔和炮管模型会根据当前选中的武器切换。武器索引 `N` 使用 " +"`turN.vxl` / `barlN.vxl`(武器 1 -> `tur.vxl`,武器 2 -> " +"`tur1.vxl`,武器 3 -> `tur2.vxl`,以此类推)。无需修改 `artmd.ini` - " +"只需按照命名约定把额外的 `.vxl` / `.hva` 文件放入游戏目录。" + +msgid "" +"The number of effective turrets is the smaller of `WeaponCount` and " +"`TurretCount`; a weapon whose index is out of this range falls back to " +"the default turret. Elite weapons follow the same per-index rule. When " +"the unit has no target, the turret stays on the last used weapon instead " +"of reverting to the default." +msgstr "" +"有效炮塔数量取 `WeaponCount` 和 `TurretCount` 中较小的值;武器索引超出该范围的武器回退到默认炮塔。精英武器遵循同样的按索引规则。当单位没有目标时,炮塔保持在上一个使用的武器而不是回退到默认炮塔。" + +msgid "Turret and barrel shadows are drawn as usual when `UseTurretShadow=yes`." +msgstr "当 `UseTurretShadow=yes` 时,炮塔和炮管阴影照常绘制。" + msgid "Multi VoiceAttack" msgstr "多武器的攻击指令音效" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 68d897ef41..4260356641 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -109,6 +109,11 @@ msgstr "" msgid "Changes to Phobos behavior" msgstr "Phobos 行为更改" +msgid "" +"VXL vehicles can now swap their turret and barrel models per weapon index " +"by using `MultiWeapon` together with `TurretCount`." +msgstr "VXL 载具现在可以通过同时使用 `MultiWeapon` 和 `TurretCount`,按武器索引切换炮塔和炮管模型。" + msgid "" "When the value of the `ProjectileRange` (Ares feature) field on a weapon " "is greater than 0, weapon range modifiers will be applied. This behavior "