From cd31914643268621319022aa07864dfcf760d593 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 fe77bd279b94a40fbe43ecb4790b00683e66224a 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 | 2 + docs/New-or-Enhanced-Logics.md | 27 ++++++++ docs/Whats-New.md | 1 + docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 6 ++ .../LC_MESSAGES/New-or-Enhanced-Logics.po | 66 +++++++++++++++++++ docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 5 ++ 7 files changed, 110 insertions(+) diff --git a/.gitignore b/.gitignore index 8591b8d397..13d3d02f43 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ out # Python virtual environment for docs .venv/ + +# Personal agent instructions +/AGENTS.md diff --git a/CREDITS.md b/CREDITS.md index 5e1776fb12..57ceddc2be 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -931,3 +931,5 @@ 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**: + - Switch Voxel Turret Depends on MultiWeapon diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 2842e1fa43..7118ffb750 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2620,6 +2620,33 @@ BurstPerTurret=0 ; integer This is essentially designed to restore multi-turrets in RA1, not to help you make a Zantos. Therefore, features like different turrets using different weapons/targeting separately/calculating cooldown separately/having separate health are not within the scope of this function. Please use techno attachment for such requirements. ``` +### Switch Voxel Turret Depends on MultiWeapon + +- Now you can make a VXL vehicle swap its turret and barrel VXL models based on the currently selected weapon by setting `MultiWeapon=yes` together with `TurretCount>0`. + - When the selected weapon changes, the turret switches to `turN.vxl` and the barrel to `barlN.vxl`, where `N` is the weapon index (weapon 1 -> `tur.vxl`, weapon 2 -> `tur1.vxl`, weapon 3 -> `tur2.vxl`, and so on). + - 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, e.g. with `WeaponCount=3` and `TurretCount=2`, weapon 3 uses `tur.vxl`. + - Elite weapons follow the same rule: `EliteWeaponN` also uses `turN.vxl`. + - No changes to `artmd.ini` are needed. Only place the extra `.vxl` / `.hva` files into the game directory following the naming convention. + - Turret and barrel shadows are drawn as usual when `UseTurretShadow=yes`. + +In `rulesmd.ini`: +```ini +[SOMEVEHICLE] ; VehicleType +MultiWeapon=yes ; boolean +TurretCount=2 ; integer +``` + +Files to provide in the game directory (named after the unit's `ImageFile`): +- `.vxl` / `.hva` - the body +- `tur.vxl` / `tur.hva` - turret for weapon 1 +- `barl.vxl` / `barl.hva` - barrel for weapon 1 +- `tur1.vxl` / `tur1.hva` - turret for weapon 2 +- `barl1.vxl` / `barl1.hva` - barrel for weapon 2 + +```{note} +This feature applies to regular VXL vehicles. It does not work together with `IsChargeTurret=yes`, `IsGattling=yes` or `Gunner=yes`. +``` + ### Turret Response - When the vehicle loses its target, you can customize whether to align the turret direction with the vehicle body. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 428466adc7..ac26a9dd8d 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -72,6 +72,7 @@ This serves as a changelog for when you just need to drop the new version in wit - `PassengerDeletion.SoylentMultiplier`: `0.0` -> `1.0` - `PassengerDeletion.SoylentAllowedHouses`: `none` -> `enemies` - `PassengerDeletion.DisplaySoylentOffset`: `none` -> `all` +- VXL vehicles can now swap their turret and barrel models per weapon index by using `MultiWeapon` together with `TurretCount`. (by Nuke) #### Changes compared to inter-version builds / pre-releases diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 1d02d30893..35ac607998 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3183,3 +3183,9 @@ msgstr "为 `BannerType` 添加了 `ClampToScreen` 标签以控制横幅位置 msgid "**obsidianus** - Automatic conversion based on health" msgstr "**obsidianus** - 自动根据血量变形" +msgid "**Nuke**:" +msgstr "**Nuke**:" + +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 d073303cea..b4640b6626 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 @@ -5493,6 +5493,72 @@ msgstr "" "的多炮塔,而不是帮你造一台赞托斯。因此,不同炮塔使用不同武器/独立瞄准/独立计算冷却/拥有独立生命值等功能均不在该功能范围内。如有此类需求请使用科技类型附件(techno" " attachment)。" +msgid "Switch Voxel Turret Depends on MultiWeapon" +msgstr "根据 MultiWeapon 切换体素炮塔" + +msgid "" +"Now you can make a VXL vehicle swap its turret and barrel VXL models " +"based on the currently selected weapon by setting `MultiWeapon=yes` " +"together with `TurretCount>0`." +msgstr "现在你可以通过同时设置 `MultiWeapon=yes` 和 `TurretCount>0`,让 VXL 载具根据当前选中的武器切换炮塔和炮管模型。" + +msgid "" +"When the selected weapon changes, the turret switches to " +"`turN.vxl` and the barrel to `barlN.vxl`, where `N`" +" is the weapon index (weapon 1 -> `tur.vxl`, weapon 2 -> " +"`tur1.vxl`, weapon 3 -> `tur2.vxl`, and so on)." +msgstr "" +"当选中的武器变化时,炮塔切换到 `turN.vxl`,炮管切换到 `barlN.vxl`,其中 " +"`N` 是武器索引(武器 1 -> `tur.vxl`,武器 2 -> `tur1.vxl`,武器 3 -> " +"`tur2.vxl`,以此类推)。" + +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, e.g. with `WeaponCount=3` and `TurretCount=2`, weapon" +" 3 uses `tur.vxl`." +msgstr "" +"有效炮塔数量取 `WeaponCount` 和 `TurretCount` 中较小的值。武器索引超出该范围的武器回退到默认炮塔,例如 " +"`WeaponCount=3` 且 `TurretCount=2` 时,武器 3 使用 `tur.vxl`。" + +msgid "" +"Elite weapons follow the same rule: `EliteWeaponN` also uses " +"`turN.vxl`." +msgstr "精英武器遵循同样的规则:`EliteWeaponN` 同样使用 `turN.vxl`。" + +msgid "" +"No changes to `artmd.ini` are needed. Only place the extra `.vxl` / " +"`.hva` files into the game directory following the naming convention." +msgstr "无需修改 `artmd.ini`。只需按照命名约定把额外的 `.vxl` / `.hva` 文件放入游戏目录。" + +msgid "Turret and barrel shadows are drawn as usual when `UseTurretShadow=yes`." +msgstr "当 `UseTurretShadow=yes` 时,炮塔和炮管阴影照常绘制。" + +msgid "" +"Files to provide in the game directory (named after the unit's " +"`ImageFile`):" +msgstr "需要放入游戏目录的文件(以单位的 `ImageFile` 命名):" + +msgid "`.vxl` / `.hva` - the body" +msgstr "`.vxl` / `.hva` - 车体" + +msgid "`tur.vxl` / `tur.hva` - turret for weapon 1" +msgstr "`tur.vxl` / `tur.hva` - 武器 1 的炮塔" + +msgid "`barl.vxl` / `barl.hva` - barrel for weapon 1" +msgstr "`barl.vxl` / `barl.hva` - 武器 1 的炮管" + +msgid "`tur1.vxl` / `tur1.hva` - turret for weapon 2" +msgstr "`tur1.vxl` / `tur1.hva` - 武器 2 的炮塔" + +msgid "`barl1.vxl` / `barl1.hva` - barrel for weapon 2" +msgstr "`barl1.vxl` / `barl1.hva` - 武器 2 的炮管" + +msgid "" +"This feature applies to regular VXL vehicles. It does not work together " +"with `IsChargeTurret=yes`, `IsGattling=yes` or `Gunner=yes`." +msgstr "该功能适用于普通 VXL 载具。它不能与 `IsChargeTurret=yes`、`IsGattling=yes` 或 `Gunner=yes` 一起使用。" + msgid "Turret Response" msgstr "炮塔响应" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 575fca91cf..de58aa47f9 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -416,6 +416,11 @@ msgstr "`PassengerDeletion.SoylentAllowedHouses`:`none`→`enemies`" msgid "`PassengerDeletion.DisplaySoylentOffset`: `none` -> `all`" msgstr "`PassengerDeletion.DisplaySoylentOffset`:`none`→`all`" +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 "Changes compared to inter-version builds / pre-releases" msgstr "相较于版本间构建/预发布版本的更改"