Jump to content

[FIX-Bug] Client Crash During Warp / Weapon Attach Race Condition (Deferred Weapon Attach Fix)


Recommended Posts

  • Premium

Over the last few weeks I ran into a very specific client-side crash that only occurs during warp / map change / respawn moments. The crash would happen randomly when the client tries to attach a weapon model before the race data, model instance, or bone map is actually ready.

In other words:
AttachWeapon() was being called during a window where the model for that character wasn’t finished loading yet.
This results in a nullptr dereference inside:

CGraphicThingInstance::RegisterModelThing CActorInstance::AttachWeapon CInstanceBase::Update

Depending on your client, this may appear only when a protection module like CShield is enabled (because it shifts frame timing slightly and exposes the race condition more often).

Why This Happens

During warp/spawn, the weapon attachment code fires immediately.

But the model + bones are not always fully available in the same frame.

So the game tries to attach to a bone that does not exist yet → crash.

This has existed in the client codebase for years.
Not related to CShield itself.
This is a real engine bug.

Solution

The correct fix is to defer weapon attachment until the character’s model and bone hierarchy become valid.

This was implemented by:

Hardening RegisterModelThing() against null resources.

Introducing a deferred attach queue (QueuePendingWeaponAttach / ProcessPendingWeaponAttach).

Running ProcessPendingWeaponAttach() early in CInstanceBase::Update() so the weapon attaches as soon as the model is ready.

Minimal Patch Highlights

Resource / Debug Safe Names
Source-client/EterLib/Resource.h


 protected:
 	static bool ms_bDeleteImmediately;
 
 };

Add
#ifdef ENABLE_CSHIELD_DEBUG
static inline const char* CSafeResName(const CResource* r) {
    return r ? r->GetFileName() : "<null-resource>";
}
#else
static inline const char* CSafeResName(const CResource* r) {
    return r ? r->GetFileName() : "";
}
#endif

RegisterModelThing Null Guard
Source-client/EterGrnLib/ThingInstance.cpp

search for :
void CGraphicThingInstance::RegisterModelThing(int iModelThing, CGraphicThing* pModelThing)
{
	if (!CheckModelThingIndex(iModelThing))
	{
		TraceError("CGraphicThingInstance::RegisterModelThing(iModelThing=%d, pModelThing=%s)\n", iModelThing, pModelThing->GetFileName());
		return;
	}

	m_modelThingSetVector[iModelThing].Clear();

	if (pModelThing)
		RegisterLODThing(iModelThing, pModelThing);
}
replace it with:
void CGraphicThingInstance::RegisterModelThing(int iModelThing, CGraphicThing* pModelThing)
{
	// Validate destination slot
	if (!CheckModelThingIndex(iModelThing))
	{
#ifdef ENABLE_CSHIELD_DEBUG
		TraceError("CGraphicThingInstance::RegisterModelThing: invalid index iModelThing=%d this=%p", iModelThing, this);
#endif
		return;
	}

	// Null model guard
#ifdef ENABLE_CSHIELD_DEBUG
	if (!pModelThing)
	{
		TraceError("[MODEL] RegisterModelThing: null pModelThing iModelThing=%d this=%p", iModelThing, this);
		// Clear destination and bail
		m_modelThingSetVector[iModelThing].Clear();
		return;
	}
#endif
	if (!pModelThing)
	{
		m_modelThingSetVector[iModelThing].Clear();
		return;
	}

	// Resource guard
	auto* __res = static_cast<CResource*>(pModelThing);
	if (!__res)
	{
#ifdef ENABLE_CSHIELD_DEBUG
		TraceError("[MODEL] RegisterModelThing: null Resource ptr iModelThing=%d this=%p", iModelThing, this);
#endif
		m_modelThingSetVector[iModelThing].Clear();
		return;
	}

	// Clear previous LOD refs
	m_modelThingSetVector[iModelThing].Clear();

	// Register primary LOD when resource is valid
	RegisterLODThing(iModelThing, pModelThing);
}

Deferred Attach Support
Source-client/GameLib/ActorInstance.h

search for:
	void AttachWeapon(DWORD dwItemIndex, DWORD dwParentPartIndex = CRaceData::PART_MAIN, DWORD dwPartIndex = CRaceData::PART_WEAPON);
	void AttachWeapon(DWORD dwParentPartIndex, DWORD dwPartIndex, CItemData* pItemData);
replace it with :
	void AttachWeapon(DWORD dwItemIndex, DWORD dwParentPartIndex = CRaceData::PART_MAIN, DWORD dwPartIndex = CRaceData::PART_WEAPON);
	void AttachWeapon(DWORD dwParentPartIndex, DWORD dwPartIndex, CItemData* pItemData);

	// Deferred weapon attach API (to avoid crashes when model resource isn't ready yet)
	void QueuePendingWeaponAttach(DWORD dwItemIndex, DWORD dwParentPartIndex = CRaceData::PART_MAIN, DWORD dwPartIndex = CRaceData::PART_WEAPON);
	bool ProcessPendingWeaponAttach();
also search for :
	DWORD m_adwPartItemID[CRaceData::PART_MAX_NUM];
replace it with :
	DWORD m_adwPartItemID[CRaceData::PART_MAX_NUM];

	// Pending weapon attachment (deferred until model resource is ready)
	struct SPendingWeapon
	{
		DWORD dwItemIndex;
		DWORD dwParentPartIndex;
		DWORD dwPartIndex;
		DWORD dwRetryCount;
		SPendingWeapon()
			: dwItemIndex(0)
			, dwParentPartIndex(CRaceData::PART_MAIN)
			, dwPartIndex(CRaceData::PART_WEAPON)
			, dwRetryCount(0)
		{}
	};
	bool m_bPendingWeaponAttach;
	SPendingWeapon m_PendingWeapon;



The Part That Was Missing Before (Important!)

Source-client/GameLib/ActorInstanceAttach.cpp

This is the critical logic to retry attach attempts until bones exist

search for :
-------
DWORD Vietnam_ConvertWeaponVnum(DWORD vnum)
{
	DWORD base = vnum / 10 * 10;
	DWORD rest = vnum % 10;
	switch (base)
	{
	case 10:base = 5000; break;
	case 20:base = 5010; break;
	case 30:base = 5020; break;
	case 40:base = 5030; break;
	case 50:base = 5030; break;
	case 60:base = 5040; break;
	case 70:base = 5040; break;
	case 80:base = 5050; break;
	case 90:base = 5050; break;
	case 100:base = 5060; break;
	case 110:base = 5060; break;
	case 120:base = 5070; break;
	case 130:base = 5070; break;
	case 140:base = 5080; break;
	case 150:base = 5080; break;
	case 160:base = 5090; break;
	case 170:base = 5090; break;
	case 180:base = 5100; break;
	case 190:base = 5100; break;
	case 200:base = 5110; break;
	case 210:base = 5110; break;
	case 220:base = 5120; break;
	case 230:base = 5120; break;
	case 240:base = 5130; break;
	case 250:base = 5130; break;
	case 260:base = 5140; break;
	case 270:base = 5140; break;
	case 280:base = 5150; break;
	case 290:base = 5150; break;
	case 1000:base = 5000; break;
	case 1010:base = 5010; break;
	case 1020:base = 5020; break;
	case 1030:base = 5030; break;
	case 1040:base = 5040; break;
	case 1050:base = 5050; break;
	case 1060:base = 5060; break;
	case 1070:base = 5070; break;
	case 1080:base = 5080; break;
	case 1090:base = 5090; break;
	case 1100:base = 5100; break;
	case 1110:base = 5110; break;
	case 1120:base = 5120; break;
	case 1130:base = 5130; break;
	case 1140:base = 5140; break;
	case 1150:base = 5150; break;
	case 1160:base = 5150; break;
	case 1170:base = 5150; break;
	case 3000:base = 5000; break;
	case 3010:base = 5010; break;
	case 3020:base = 5020; break;
	case 3030:base = 5030; break;
	case 3040:base = 5040; break;
	case 3050:base = 5050; break;
	case 3060:base = 5060; break;
	case 3070:base = 5070; break;
	case 3080:base = 5080; break;
	case 3090:base = 5090; break;
	case 3100:base = 5100; break;
	case 3110:base = 5100; break;
	case 3120:base = 5110; break;
	case 3130:base = 5110; break;
	case 3140:base = 5120; break;
	case 3150:base = 5120; break;
	case 3160:base = 5130; break;
	case 3170:base = 5130; break;
	case 3180:base = 5140; break;
	case 3190:base = 5140; break;
	case 3200:base = 5150; break;
	case 3210:base = 5150; break;
	}
	return base + rest;
}
replace with : 
DWORD Vietnam_ConvertWeaponVnum(DWORD vnum)
{
	DWORD base = vnum / 10 * 10;
	DWORD rest = vnum % 10;
	switch (base)
	{
	case 10:base = 5000; break;
	case 20:base = 5010; break;
	case 30:base = 5020; break;
	case 40:base = 5030; break;
	case 50:base = 5030; break;
	case 60:base = 5040; break;
	case 70:base = 5040; break;
	case 80:base = 5050; break;
	case 90:base = 5050; break;
	case 100:base = 5060; break;
	case 110:base = 5060; break;
	case 120:base = 5070; break;
	case 130:base = 5070; break;
	case 140:base = 5080; break;
	case 150:base = 5080; break;
	case 160:base = 5090; break;
	case 170:base = 5090; break;
	case 180:base = 5100; break;
	case 190:base = 5100; break;
	case 200:base = 5110; break;
	case 210:base = 5110; break;
	case 220:base = 5120; break;
	case 230:base = 5120; break;
	case 240:base = 5130; break;
	case 250:base = 5130; break;
	case 260:base = 5140; break;
	case 270:base = 5140; break;
	case 280:base = 5150; break;
	case 290:base = 5150; break;
	case 1000:base = 5000; break;
	case 1010:base = 5010; break;
	case 1020:base = 5020; break;
	case 1030:base = 5030; break;
	case 1040:base = 5040; break;
	case 1050:base = 5050; break;
	case 1060:base = 5060; break;
	case 1070:base = 5070; break;
	case 1080:base = 5080; break;
	case 1090:base = 5090; break;
	case 1100:base = 5100; break;
	case 1110:base = 5110; break;
	case 1120:base = 5120; break;
	case 1130:base = 5130; break;
	case 1140:base = 5140; break;
	case 1150:base = 5150; break;
	case 1160:base = 5150; break;
	case 1170:base = 5150; break;
	case 3000:base = 5000; break;
	case 3010:base = 5010; break;
	case 3020:base = 5020; break;
	case 3030:base = 5030; break;
	case 3040:base = 5040; break;
	case 3050:base = 5050; break;
	case 3060:base = 5060; break;
	case 3070:base = 5070; break;
	case 3080:base = 5080; break;
	case 3090:base = 5090; break;
	case 3100:base = 5100; break;
	case 3110:base = 5100; break;
	case 3120:base = 5110; break;
	case 3130:base = 5110; break;
	case 3140:base = 5120; break;
	case 3150:base = 5120; break;
	case 3160:base = 5130; break;
	case 3170:base = 5130; break;
	case 3180:base = 5140; break;
	case 3190:base = 5140; break;
	case 3200:base = 5150; break;
	case 3210:base = 5150; break;
	}
	return base + rest;
}

// Deferred weapon attach queue/process methods
void CActorInstance::QueuePendingWeaponAttach(DWORD dwItemIndex, DWORD dwParentPartIndex, DWORD dwPartIndex)
{
	// Avoid resetting retries if same request is already pending
	if (m_bPendingWeaponAttach
		&& m_PendingWeapon.dwItemIndex == dwItemIndex
		&& m_PendingWeapon.dwParentPartIndex == dwParentPartIndex
		&& m_PendingWeapon.dwPartIndex == dwPartIndex)
	{
		return;
	}

	m_PendingWeapon.dwItemIndex = dwItemIndex;
	m_PendingWeapon.dwParentPartIndex = dwParentPartIndex;
	m_PendingWeapon.dwPartIndex = dwPartIndex;
	m_PendingWeapon.dwRetryCount = 0;
	m_bPendingWeaponAttach = true;

#ifdef ENABLE_CSHIELD_DEBUG
	TraceError("[PENDING] QueueWeaponAttach item=%u parent=%u part=%u", dwItemIndex, dwParentPartIndex, dwPartIndex);
#endif
}

bool CActorInstance::ProcessPendingWeaponAttach()
{
	if (!m_bPendingWeaponAttach)
		return false;

	// Increment retry counter and attempt
	++m_PendingWeapon.dwRetryCount;

	AttachWeapon(m_PendingWeapon.dwItemIndex, m_PendingWeapon.dwParentPartIndex, m_PendingWeapon.dwPartIndex);

	// Heuristic: if model/bone is resolvable now, clear pending
	const char* szBoneName = nullptr;
	if (GetAttachingBoneName(m_PendingWeapon.dwPartIndex, &szBoneName) && szBoneName)
	{
		int iBoneIndex = -1;
		if (FindBoneIndex(m_PendingWeapon.dwPartIndex, szBoneName, &iBoneIndex))
		{
#ifdef ENABLE_CSHIELD_DEBUG
			TraceError("[PENDING] Weapon attach ready item=%u part=%u after %u retries", m_PendingWeapon.dwItemIndex, m_PendingWeapon.dwPartIndex, m_PendingWeapon.dwRetryCount);
#endif
			m_bPendingWeaponAttach = false;
			return true;
		}
	}

	// Safety cap
	if (m_PendingWeapon.dwRetryCount > 200)
	{
#ifdef ENABLE_CSHIELD_DEBUG
		TraceError("[PENDING] Giving up weapon attach item=%u part=%u", m_PendingWeapon.dwItemIndex, m_PendingWeapon.dwPartIndex);
#endif
		m_bPendingWeaponAttach = false;
	}

	return true;
}

also search for :
void CActorInstance::AttachWeapon(DWORD dwItemIndex, DWORD dwParentPartIndex, DWORD dwPartIndex)
{
	if (dwPartIndex >= CRaceData::PART_MAX_NUM)
		return;

	m_adwPartItemID[dwPartIndex] = dwItemIndex;

	if (USE_VIETNAM_CONVERT_WEAPON_VNUM)
		dwItemIndex = Vietnam_ConvertWeaponVnum(dwItemIndex);

	CItemData* pItemData;
	if (!CItemManager::Instance().GetItemDataPointer(dwItemIndex, &pItemData))
	{
		RegisterModelThing(dwPartIndex, NULL);
		SetModelInstance(dwPartIndex, dwPartIndex, 0);

		RegisterModelThing(CRaceData::PART_WEAPON_LEFT, NULL);
		SetModelInstance(CRaceData::PART_WEAPON_LEFT, CRaceData::PART_WEAPON_LEFT, 0);

		RefreshActorInstance();
		return;
	}

	__DestroyWeaponTrace();
	// 양손무기(자객 이도류) 왼손,오른손 모두에 장착.
	DWORD dwWeaponType = pItemData->GetWeaponType();
#ifdef ENABLE_WEAPON_COSTUME_SYSTEM
	if (pItemData->GetType() == CItemData::ITEM_TYPE_COSTUME)
	{
		DWORD typeDec = pItemData->GetValue(3);
		if (__IsRightHandWeapon(typeDec))
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON, pItemData);
		if (__IsLeftHandWeapon(typeDec))
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON_LEFT, pItemData);
	}
	else
	{
		if (m_eRace == CRaceData::RACE_WOLFMAN_M)
		{
			const char* szAttachingBoneName = "equip_right_weapon";
			if (dwWeaponType != CItemData::WEAPON_CLAW)
				szAttachingBoneName = "equip_right";
			m_pkCurRaceData->ChangeAttachingBoneName(CRaceData::PART_WEAPON, szAttachingBoneName);
		}

		if (__IsRightHandWeapon(dwWeaponType))
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON, pItemData);
		if (__IsLeftHandWeapon(dwWeaponType))
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON_LEFT, pItemData);
	}
#else
	if (__IsRightHandWeapon(dwWeaponType))
		AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON, pItemData);
	if (__IsLeftHandWeapon(dwWeaponType))
		AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON_LEFT, pItemData);
#endif
//#ifdef ENABLE_INBUILD_ANIMATION
//	if (CGrannyLODController* pLODController = m_LODControllerVector[dwPartIndex])
//	{
//		if (CGrannyModelInstance* pWeaponModelInstance = pLODController->GetModelInstance())
//		{
//			CGraphicThing* pItemGraphicThing = pItemData->GetModelThing();
//			if (CGrannyMotion* pItemMotion = pItemGraphicThing->GetMotionPointer(0))
//			{
//				pWeaponModelInstance->SetMotionPointer(pItemMotion);
//			}
//		}
//	}
//#endif
}

replace with :
void CActorInstance::AttachWeapon(DWORD dwItemIndex, DWORD dwParentPartIndex, DWORD dwPartIndex)
{
	if (dwPartIndex >= CRaceData::PART_MAX_NUM)
		return;

	// Track chosen item per part
	m_adwPartItemID[dwPartIndex] = dwItemIndex;

	if (USE_VIETNAM_CONVERT_WEAPON_VNUM)
		dwItemIndex = Vietnam_ConvertWeaponVnum(dwItemIndex);

	CItemData* pItemData;
	if (!CItemManager::Instance().GetItemDataPointer(dwItemIndex, &pItemData))
	{
		// Clear both hands on invalid item
		RegisterModelThing(dwPartIndex, NULL);
		SetModelInstance(dwPartIndex, dwPartIndex, 0);

		RegisterModelThing(CRaceData::PART_WEAPON_LEFT, NULL);
		SetModelInstance(CRaceData::PART_WEAPON_LEFT, CRaceData::PART_WEAPON_LEFT, 0);

		RefreshActorInstance();
		return;
	}

	// Defer when race/model context isn't ready yet
	if (!m_pkCurRaceData)
	{
		QueuePendingWeaponAttach(dwItemIndex, dwParentPartIndex, dwPartIndex);
		return;
	}

	__DestroyWeaponTrace();

	// Handle both hands when necessary
	DWORD dwWeaponType = pItemData->GetWeaponType();
#ifdef ENABLE_WEAPON_COSTUME_SYSTEM
	if (pItemData->GetType() == CItemData::ITEM_TYPE_COSTUME)
	{
		DWORD typeDec = pItemData->GetValue(3);
		if (__IsRightHandWeapon(typeDec))
		{
			m_adwPartItemID[CRaceData::PART_WEAPON] = dwItemIndex;
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON, pItemData);
		}
		if (__IsLeftHandWeapon(typeDec))
		{
			m_adwPartItemID[CRaceData::PART_WEAPON_LEFT] = dwItemIndex;
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON_LEFT, pItemData);
		}
	}
	else
	{
		if (m_eRace == CRaceData::RACE_WOLFMAN_M)
		{
			const char* szAttachingBoneName = "equip_right_weapon";
			if (dwWeaponType != CItemData::WEAPON_CLAW)
				szAttachingBoneName = "equip_right";
			m_pkCurRaceData->ChangeAttachingBoneName(CRaceData::PART_WEAPON, szAttachingBoneName);
		}

		if (__IsRightHandWeapon(dwWeaponType))
		{
			m_adwPartItemID[CRaceData::PART_WEAPON] = dwItemIndex;
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON, pItemData);
		}
		if (__IsLeftHandWeapon(dwWeaponType))
		{
			m_adwPartItemID[CRaceData::PART_WEAPON_LEFT] = dwItemIndex;
			AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON_LEFT, pItemData);
		}
	}
#else
	if (__IsRightHandWeapon(dwWeaponType))
	{
		m_adwPartItemID[CRaceData::PART_WEAPON] = dwItemIndex;
		AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON, pItemData);
	}
	if (__IsLeftHandWeapon(dwWeaponType))
	{
		m_adwPartItemID[CRaceData::PART_WEAPON_LEFT] = dwItemIndex;
		AttachWeapon(dwParentPartIndex, CRaceData::PART_WEAPON_LEFT, pItemData);
	}
#endif
	// In-build animation handling intentionally unchanged (commented)
}

also search for :
void CActorInstance::AttachWeapon(DWORD dwParentPartIndex, DWORD dwPartIndex, CItemData* pItemData)
{
	//assert(m_pkCurRaceData);
	if (!pItemData)
		return;

	const char* szBoneName;
	if (!GetAttachingBoneName(dwPartIndex, &szBoneName))
		return;

	// NOTE : (이도류처리)단도일 경우 형태가 다른 것으로 얻는다. 없을 경우 디폴트를 리턴
	if (CRaceData::PART_WEAPON_LEFT == dwPartIndex)
	{
		RegisterModelThing(dwPartIndex, pItemData->GetSubModelThing());
	}
	else
	{
		RegisterModelThing(dwPartIndex, pItemData->GetModelThing());
	}

	for (DWORD i = 0; i < pItemData->GetLODModelThingCount(); ++i)
	{
		CGraphicThing* pThing;

		if (!pItemData->GetLODModelThingPointer(i, &pThing))
			continue;

		RegisterLODThing(dwPartIndex, pThing);
	}

	SetModelInstance(dwPartIndex, dwPartIndex, 0);
	AttachModelInstance(dwParentPartIndex, szBoneName, dwPartIndex);

	// 20041208.myevan.무기스펙큘러(값옷은 SetShape에서 직접 해준다.)
	if (USE_WEAPON_SPECULAR)
	{
		SMaterialData kMaterialData;
		kMaterialData.pImage = NULL;
		kMaterialData.isSpecularEnable = TRUE;
		kMaterialData.fSpecularPower = pItemData->GetSpecularPowerf();
		kMaterialData.bSphereMapIndex = 1;
		SetMaterialData(dwPartIndex, NULL, kMaterialData);
	}

	// Weapon Trace
#ifdef ENABLE_WEAPON_COSTUME_SYSTEM
	if (pItemData->GetType() == CItemData::ITEM_TYPE_COSTUME)
	{
		DWORD typeDec = pItemData->GetValue(3);
		if (__IsWeaponTrace(typeDec))
		{
			CWeaponTrace* pWeaponTrace = CWeaponTrace::New();
			pWeaponTrace->SetWeaponInstance(this, dwPartIndex, szBoneName);
			m_WeaponTraceVector.push_back(pWeaponTrace);
		}
	}
	else
	{
		if (__IsWeaponTrace(pItemData->GetWeaponType()))
		{
			CWeaponTrace* pWeaponTrace = CWeaponTrace::New();
			pWeaponTrace->SetWeaponInstance(this, dwPartIndex, szBoneName);
			m_WeaponTraceVector.push_back(pWeaponTrace);
		}
	}
#else
	if (__IsWeaponTrace(pItemData->GetWeaponType()))
	{
		CWeaponTrace* pWeaponTrace = CWeaponTrace::New();
		pWeaponTrace->SetWeaponInstance(this, dwPartIndex, szBoneName);
		m_WeaponTraceVector.push_back(pWeaponTrace);
	}
#endif
}
replace with :

void CActorInstance::AttachWeapon(DWORD dwParentPartIndex, DWORD dwPartIndex, CItemData* pItemData)
{
	if (!pItemData)
		return;

	// Race/model context not ready yet - defer
	if (!m_pkCurRaceData)
	{
		DWORD vnum = m_adwPartItemID[dwPartIndex];
		if (vnum)
			QueuePendingWeaponAttach(vnum, dwParentPartIndex, dwPartIndex);
		return;
	}

	const char* szBoneName;
	if (!GetAttachingBoneName(dwPartIndex, &szBoneName) || !szBoneName)
	{
		DWORD vnum = m_adwPartItemID[dwPartIndex];
		if (vnum)
			QueuePendingWeaponAttach(vnum, dwParentPartIndex, dwPartIndex);
		return;
	}

	// Choose model thing for the requested part, ensure it's valid
	CGraphicThing* pModelThing = (CRaceData::PART_WEAPON_LEFT == dwPartIndex)
		? pItemData->GetSubModelThing()
		: pItemData->GetModelThing();

	if (!pModelThing)
	{
		DWORD vnum = m_adwPartItemID[dwPartIndex];
		if (vnum)
			QueuePendingWeaponAttach(vnum, dwParentPartIndex, dwPartIndex);
		return;
	}

	RegisterModelThing(dwPartIndex, pModelThing);

	for (DWORD i = 0; i < pItemData->GetLODModelThingCount(); ++i)
	{
		CGraphicThing* pThing;
		if (!pItemData->GetLODModelThingPointer(i, &pThing))
			continue;

		RegisterLODThing(dwPartIndex, pThing);
	}

	// If SetModelInstance fails (resource not resolved yet), retry later
	if (!SetModelInstance(dwPartIndex, dwPartIndex, 0))
	{
		DWORD vnum = m_adwPartItemID[dwPartIndex];
		if (vnum)
			QueuePendingWeaponAttach(vnum, dwParentPartIndex, dwPartIndex);
		return;
	}

	AttachModelInstance(dwParentPartIndex, szBoneName, dwPartIndex);

	// Weapon specular (costumes handled in SetShape)
	if (USE_WEAPON_SPECULAR)
	{
		SMaterialData kMaterialData;
		kMaterialData.pImage = NULL;
		kMaterialData.isSpecularEnable = TRUE;
		kMaterialData.fSpecularPower = pItemData->GetSpecularPowerf();
		kMaterialData.bSphereMapIndex = 1;
		SetMaterialData(dwPartIndex, NULL, kMaterialData);
	}

	// Weapon Trace
#ifdef ENABLE_WEAPON_COSTUME_SYSTEM
	if (pItemData->GetType() == CItemData::ITEM_TYPE_COSTUME)
	{
		DWORD typeDec = pItemData->GetValue(3);
		if (__IsWeaponTrace(typeDec))
		{
			CWeaponTrace* pWeaponTrace = CWeaponTrace::New();
			pWeaponTrace->SetWeaponInstance(this, dwPartIndex, szBoneName);
			m_WeaponTraceVector.push_back(pWeaponTrace);
		}
	}
	else
	{
		if (__IsWeaponTrace(pItemData->GetWeaponType()))
		{
			CWeaponTrace* pWeaponTrace = CWeaponTrace::New();
			pWeaponTrace->SetWeaponInstance(this, dwPartIndex, szBoneName);
			m_WeaponTraceVector.push_back(pWeaponTrace);
		}
	}
#else
	if (__IsWeaponTrace(pItemData->GetWeaponType()))
	{
		CWeaponTrace* pWeaponTrace = CWeaponTrace::New();
		pWeaponTrace->SetWeaponInstance(this, dwPartIndex, szBoneName);
		m_WeaponTraceVector.push_back(pWeaponTrace);
	}
#endif
}


Process Deferred Attach Each Frame
Source-client/UserInterface/InstanceBase.cpp

search for : 

void CInstanceBase::Update()
{
	++ms_dwUpdateCounter;

	StateProcess();
replace with :
void CInstanceBase::Update()
{
	++ms_dwUpdateCounter;

	// Attempt any deferred weapon attachment (model/bones may not be ready right after spawn/warp)
	m_GraphicThingInstance.ProcessPendingWeaponAttach();

	StateProcess();


And last Source-client\GameLib\ActorInstance.cpp :

in void CActorInstance::__Initialize() search for or add in the last of the void : 
#ifdef ENABLE_SKILL_COLOR_SYSTEM
	memset(m_dwSkillColor, 0, sizeof(m_dwSkillColor));
#endif
#if defined(ENABLE_NPC_WEAR_ITEM)
	m_dwRealRaceIndex = 0;
#endif
replace with :

#ifdef ENABLE_SKILL_COLOR_SYSTEM
	memset(m_dwSkillColor, 0, sizeof(m_dwSkillColor));
#endif
#if defined(ENABLE_NPC_WEAR_ITEM)
	m_dwRealRaceIndex = 0;
#endif

	// Initialize deferred weapon attachment state
	m_bPendingWeaponAttach = false;
	m_PendingWeapon = SPendingWeapon();
}


Effect After Fix

✅ No more random warp crash.

✅ No more null bone reference.

✅ Weapon attaches reliably as soon as model data is ready.

✅ Tested over 100+ warp transitions under stress.
✅ Zero crashes.

Credits

Special thanks to:

Doofy (CShield) for support and verification

Everyone who helped reproduce the issue reliably.

FAQ

Q: Is this caused by CShield?
A: No. CShield just makes the race condition timing more visible. The underlying bug is in the client attach pipeline itself.

Q: Should every client apply this fix?
A: Yes. If your client equips weapons and warps, this affects you.

If anyone wants I can also provide:

A build with debug traces to test in your environment

Just reply here.

Done.
This finally eliminates one of the oldest silent crash sources in the client.
Hope it helps someone else.
Enjoy. 💙

  • Metin2 Dev 3
  • Love 3

───────────────────────────────────────────
— Development & Research —
Metin2 Systems • Client/Server • Reverse Engineering

Discord:  saudidos

If I helped you, consider leaving a like. ✦
───────────────────────────────────────────
 

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.