From d7b5e36d0863c515c04e6c9a5886cca9d4a89c4b Mon Sep 17 00:00:00 2001 From: andreasw Date: Wed, 17 Jun 2026 21:23:20 +0200 Subject: [PATCH 1/2] Initial implementation (Claude) --- .../GameClient/Module/W3DModelDraw.h | 1 + .../GameClient/Drawable/Draw/W3DModelDraw.cpp | 185 +++++++++++++++++- 2 files changed, 184 insertions(+), 2 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h index cdbdf119471..9eafd33e86c 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h @@ -308,6 +308,7 @@ class W3DModelDrawModuleData : public ModuleData mutable Vector3 m_attachToDrawableBoneOffset; #endif Int m_defaultState; + Int m_lastRealConditionStateIndex; ///< index of last DefaultConditionState/ConditionState, used by AutoConditionState Int m_projectileBoneFeedbackEnabledSlots; ///< Hide and show the launch bone geometries according to clip status adjustments. Real m_initialRecoil; Real m_maxRecoil; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index 656e4693bc1..aa840abb950 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -1056,7 +1056,8 @@ W3DModelDrawModuleData::W3DModelDrawModuleData() : m_attachToDrawableBoneOffsetValid(false), #endif m_minLODRequired(STATIC_GAME_LOD_LOW), - m_defaultState(-1) + m_defaultState(-1), + m_lastRealConditionStateIndex(-1) { const Real MAX_SHIFT = 3.0f; const Real INITIAL_RECOIL_RATE = 2.0f; @@ -1216,7 +1217,8 @@ enum ParseCondStateType CPP_11(: Int) PARSE_NORMAL, PARSE_DEFAULT, PARSE_TRANSITION, - PARSE_ALIAS + PARSE_ALIAS, + PARSE_AUTO }; //------------------------------------------------------------------------------------------------- @@ -1246,6 +1248,7 @@ void W3DModelDrawModuleData::buildFieldParse(MultiIniFieldParse& p) { "DefaultConditionState", W3DModelDrawModuleData::parseConditionState, (void*)PARSE_DEFAULT, 0 }, { "ConditionState", W3DModelDrawModuleData::parseConditionState, (void*)PARSE_NORMAL, 0 }, { "AliasConditionState", W3DModelDrawModuleData::parseConditionState, (void*)PARSE_ALIAS, 0 }, + { "AutoConditionState", W3DModelDrawModuleData::parseConditionState, (void*)PARSE_AUTO, 0 }, { "TransitionState", W3DModelDrawModuleData::parseConditionState, (void*)PARSE_TRANSITION, 0 }, { "TrackMarks", parseAsciiStringLC, nullptr, offsetof(W3DModelDrawModuleData, m_trackFile) }, { "ExtraPublicBone", INI::parseAsciiStringVectorAppend, nullptr, offsetof(W3DModelDrawModuleData, m_extraPublicBones) }, @@ -1475,6 +1478,100 @@ static Bool doesStateExist(const ModelConditionVector& v, const ModelConditionFl return false; } +//------------------------------------------------------------------------------------------------- +// Like doesStateExist, but returns the index of the first state whose condition set matches 'f' +// exactly (or -1 if none). Used by AutoConditionState to locate its base state. +static Int findStateIndexMatching(const ModelConditionVector& v, const ModelConditionFlags& f) +{ + for (Int idx = 0; idx < (Int)v.size(); ++idx) + { + const ModelConditionInfo& info = v[idx]; + for (Int i = info.getConditionsYesCount()-1; i >= 0; --i) + { + if (f == info.getNthConditionsYes(i)) + return idx; + } + } + return -1; +} + +//------------------------------------------------------------------------------------------------- +// Overlay onto 'dst' the authored fields that 'prev' set differently from the default state 'def'. +// Used by AutoConditionState: 'dst' starts as the matched base state, 'prev' is the previously +// defined ConditionState whose explicit overrides we want applied on top of the base. +// Note: a normal ConditionState is built as (default-copy then block-fields), so a scalar field +// counts as "explicitly set" iff it differs from the default state's value. Animations are +// clear-on-first-write (signaled by the GOT_*_ANIMS read flags). The hide/show, particle-bone and +// FX vectors are append-based, so 'prev' = default's entries followed by the block's appended ones. +static void mergeConditionStateOverrides(ModelConditionInfo& dst, const ModelConditionInfo& prev, const ModelConditionInfo& def) +{ + if (prev.m_modelName != def.m_modelName) + dst.m_modelName = prev.m_modelName; + + if (prev.m_mode != def.m_mode) + dst.m_mode = prev.m_mode; + if (prev.m_animMinSpeedFactor != def.m_animMinSpeedFactor) + dst.m_animMinSpeedFactor = prev.m_animMinSpeedFactor; + if (prev.m_animMaxSpeedFactor != def.m_animMaxSpeedFactor) + dst.m_animMaxSpeedFactor = prev.m_animMaxSpeedFactor; + if (prev.m_animBlendTime != def.m_animBlendTime) + dst.m_animBlendTime = prev.m_animBlendTime; + if (prev.m_flags != def.m_flags) + dst.m_flags = prev.m_flags; + if (prev.m_transitionKey != def.m_transitionKey) + dst.m_transitionKey = prev.m_transitionKey; + if (prev.m_allowToFinishKey != def.m_allowToFinishKey) + dst.m_allowToFinishKey = prev.m_allowToFinishKey; + + // turrets: only the 4 INI-read members; runtime bone members are recomputed in validateStuff + for (Int t = 0; t < MAX_TURRETS; ++t) + { + if (prev.m_turrets[t].m_turretAngleNameKey != def.m_turrets[t].m_turretAngleNameKey) + dst.m_turrets[t].m_turretAngleNameKey = prev.m_turrets[t].m_turretAngleNameKey; + if (prev.m_turrets[t].m_turretPitchNameKey != def.m_turrets[t].m_turretPitchNameKey) + dst.m_turrets[t].m_turretPitchNameKey = prev.m_turrets[t].m_turretPitchNameKey; + if (prev.m_turrets[t].m_turretArtAngle != def.m_turrets[t].m_turretArtAngle) + dst.m_turrets[t].m_turretArtAngle = prev.m_turrets[t].m_turretArtAngle; + if (prev.m_turrets[t].m_turretArtPitch != def.m_turrets[t].m_turretArtPitch) + dst.m_turrets[t].m_turretArtPitch = prev.m_turrets[t].m_turretArtPitch; + } + + for (Int w = 0; w < WEAPONSLOT_COUNT; ++w) + { + if (prev.m_weaponFireFXBoneName[w] != def.m_weaponFireFXBoneName[w]) + dst.m_weaponFireFXBoneName[w] = prev.m_weaponFireFXBoneName[w]; + if (prev.m_weaponRecoilBoneName[w] != def.m_weaponRecoilBoneName[w]) + dst.m_weaponRecoilBoneName[w] = prev.m_weaponRecoilBoneName[w]; + if (prev.m_weaponMuzzleFlashName[w] != def.m_weaponMuzzleFlashName[w]) + dst.m_weaponMuzzleFlashName[w] = prev.m_weaponMuzzleFlashName[w]; + if (prev.m_weaponProjectileLaunchBoneName[w] != def.m_weaponProjectileLaunchBoneName[w]) + dst.m_weaponProjectileLaunchBoneName[w] = prev.m_weaponProjectileLaunchBoneName[w]; + if (prev.m_weaponProjectileHideShowName[w] != def.m_weaponProjectileHideShowName[w]) + dst.m_weaponProjectileHideShowName[w] = prev.m_weaponProjectileHideShowName[w]; + } + + // animations: 'prev' replaced (not appended) its anims iff a GOT_*_ANIMS flag is set + const Int animBits = (1<m_lastRealConditionStateIndex < 0) + { + DEBUG_CRASH(("*** ASSET ERROR: AutoConditionState must refer to a previous ConditionState!")); + throw INI_INVALID_DATA; + } + + // Take a COPY of the previously defined state; m_conditionStates may reallocate on push_back below. + const ModelConditionInfo prevState = self->m_conditionStates.at(self->m_lastRealConditionStateIndex); + + ModelConditionFlags autoFlags; + #if defined(RTS_DEBUG) + AsciiString description; + autoFlags.parse(ini, &description); + #else + autoFlags.parse(ini, nullptr); + #endif + + if (!autoFlags.any()) + { + DEBUG_CRASH(("*** ASSET ERROR: AutoConditionState requires at least one condition flag")); + throw INI_INVALID_DATA; + } + + if (autoFlags.anyIntersectionWith(self->m_ignoreConditionStates)) + { + DEBUG_CRASH(("You should not specify bits in a state once they are used in IgnoreConditionStates (%s)", TheThingTemplateBeingParsedName.str())); + throw INI_INVALID_DATA; + } + + // Base values come from the existing state matching the auto flags (e.g. the DAMAGED state). + // If none matches, fall back to a full copy of the previous state. + Int baseIdx = findStateIndexMatching(self->m_conditionStates, autoFlags); + if (baseIdx >= 0) + { + info = self->m_conditionStates.at(baseIdx); + + // Overlay the fields the previous state set differently from the default state. + const ModelConditionInfo defState = + (self->m_defaultState >= 0) ? self->m_conditionStates.at(self->m_defaultState) : ModelConditionInfo(); + mergeConditionStateOverrides(info, prevState, defState); + } + else + { + info = prevState; + } + + // Combined conditions = each of the previous state's condition sets, OR'd with the auto flags. + info.m_conditionsYesVec.clear(); + for (Int i = 0; i < prevState.getConditionsYesCount(); ++i) + { + ModelConditionFlags combined = prevState.getNthConditionsYes(i); + combined.set(autoFlags); + + if (doesStateExist(self->m_conditionStates, combined)) + { + DEBUG_CRASH(("*** ASSET ERROR: AutoConditionState produced a duplicate condition state (%s)", TheThingTemplateBeingParsedName.str())); + throw INI_INVALID_DATA; + } + info.m_conditionsYesVec.push_back(combined); + } + + #if defined(RTS_DEBUG) + info.m_description.clear(); + info.m_description.concat(TheThingTemplateBeingParsedName); + info.m_description.concat("\n AUTO: "); + info.m_description.concat(description); + #endif + + // Normalize model name (the normal path does this after initFromINI, which we skip). + if (info.m_modelName.isNone()) + info.m_modelName.clear(); + + self->m_conditionStates.push_back(info); + + // yes, return, NOT break! (no body to parse for AutoConditionState) + return; + } + case PARSE_NORMAL: { if (self->m_defaultState >= 0 && cst != PARSE_ALIAS) @@ -1786,6 +1963,10 @@ void W3DModelDrawModuleData::parseConditionState(INI* ini, void *instance, void } else { + // Track the last *defined* condition state so AutoConditionState refers to it (and not to an + // earlier auto-generated state). size() == index of the element about to be pushed. + if (cst == PARSE_NORMAL || cst == PARSE_DEFAULT) + self->m_lastRealConditionStateIndex = (Int)self->m_conditionStates.size(); self->m_conditionStates.push_back(info); } } From 7724dab77355a3b9adc9004c0a3c7380a046619a Mon Sep 17 00:00:00 2001 From: andreasw Date: Thu, 18 Jun 2026 17:45:32 +0200 Subject: [PATCH 2/2] Fixed same Recoil/MuzzleFlash not working for multiple weapon slots --- .../GameClient/Drawable/Draw/W3DModelDraw.cpp | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index aa840abb950..53c910bc94f 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -35,6 +35,7 @@ #define NO_DEBUG_CRC #include +#include #include "Common/crc.h" #include "Common/CRCDebug.h" @@ -2909,14 +2910,19 @@ void W3DModelDraw::handleClientRecoil() return; } - // do recoil, if any + // Multiple weapon slots may reference the same recoil / muzzle-flash bone name (hence the + // same bone index). Applying per-slot directly to the bone makes the last processed slot + // clobber the others, so a shared muzzle flash / recoil only ever shows for the last slot. + // Instead, aggregate across all slots that share a bone: the muzzle flash is visible if ANY + // sharing slot just fired, and the recoil bone uses the LARGEST shift among sharing slots. + std::map muzzleFlashVisible; // muzzle-flash bone -> any sharing slot wants flash shown + std::map recoilShift; // recoil bone -> max shift among sharing slots + + const Real TINY_RECOIL = 0.01f; + + // Pass 1: advance every recoil state machine and accumulate the per-bone aggregates. for (int wslot = 0; wslot < WEAPONSLOT_COUNT; ++wslot) { - if (wslot == 0 && stricmp(m_curState->m_modelName.str(), "avjug_deploy") == 0) { - int i = 0; - i += 1; - } - if (!m_curState->m_hasRecoilBonesOrMuzzleFlashes[wslot]) continue; @@ -2930,12 +2936,12 @@ void W3DModelDraw::handleClientRecoil() { if (barrels[i].m_muzzleFlashBone != 0) { - Bool hidden = recoils[i].m_state != WeaponRecoilInfo::RECOIL_START; - //DEBUG_LOG(("adjust muzzleflash %08lx for Draw %08lx state %s to %d at frame %d",subObjToHide,this,m_curState->m_description.str(),hidden?1:0,TheGameLogic->getFrame())); - barrels[i].setMuzzleFlashHidden(m_renderObject, hidden); + // operator[] inserts a default (hidden) entry, so bones whose slots are all idle + // still get hidden in pass 2; OR-accumulate so any firing slot shows the flash. + Bool& visible = muzzleFlashVisible[barrels[i].m_muzzleFlashBone]; + visible = visible || (recoils[i].m_state == WeaponRecoilInfo::RECOIL_START); } - const Real TINY_RECOIL = 0.01f; if (barrels[i].m_recoilBone != 0) { switch (recoils[i].m_state ) @@ -2969,16 +2975,10 @@ void W3DModelDraw::handleClientRecoil() break; } - Matrix3D gunXfrm; - gunXfrm.Make_Identity(); - gunXfrm.Translate_X( -recoils[i].m_shift ); - //DEBUG_ASSERTLOG(recoils[i].m_shift==0.0f,("adjust bone %d by %f",recoils[i].m_recoilBone,recoils[i].m_shift)); - - if (m_renderObject) - { - m_renderObject->Capture_Bone( barrels[i].m_recoilBone ); - m_renderObject->Control_Bone( barrels[i].m_recoilBone, gunXfrm ); - } + // keep the largest shift among slots sharing this bone (operator[] inserts 0.0f). + Real& shift = recoilShift[barrels[i].m_recoilBone]; + if (recoils[i].m_shift > shift) + shift = recoils[i].m_shift; } else { @@ -2987,6 +2987,35 @@ void W3DModelDraw::handleClientRecoil() } } } + + // Pass 2: apply the aggregated state. Muzzle flashes go through the barrel helper (keeps the + // asset-error diagnostic); recoil bones are driven directly from the per-bone max shift. + if (m_renderObject) + { + for (int wslot = 0; wslot < WEAPONSLOT_COUNT; ++wslot) + { + if (!m_curState->m_hasRecoilBonesOrMuzzleFlashes[wslot]) + continue; + + const ModelConditionInfo::WeaponBarrelInfoVec& barrels = m_curState->m_weaponBarrelInfoVec[wslot]; + for (ModelConditionInfo::WeaponBarrelInfoVec::const_iterator it = barrels.begin(); it != barrels.end(); ++it) + { + if (it->m_muzzleFlashBone != 0) + it->setMuzzleFlashHidden(m_renderObject, !muzzleFlashVisible[it->m_muzzleFlashBone]); + } + } + + for (std::map::const_iterator it = recoilShift.begin(); it != recoilShift.end(); ++it) + { + Matrix3D gunXfrm; + gunXfrm.Make_Identity(); + gunXfrm.Translate_X( -it->second ); + //DEBUG_ASSERTLOG(it->second==0.0f,("adjust bone %d by %f",it->first,it->second)); + + m_renderObject->Capture_Bone( it->first ); + m_renderObject->Control_Bone( it->first, gunXfrm ); + } + } } //-------------------------------------------------------------------------------------------------