From 35ad07a4d6af90b70899f612ecba172326da9a7b Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 18 Aug 2026 21:02:56 -0700 Subject: [PATCH 1/2] GiveLoadoutWeapon: check XP eligibility before creating the weapon The XP check compared against static loadout table data but ran after CreateEntityByName, so every spawn with an above-tier loadout choice paid a create/destroy cycle for a weapon that was never granted. Hoist the check above the inventory check and entity creation, matching the eligibility-first ordering of the detpack grant. Also folds the duplicate dev-loadout class selection into the iLoadoutClass local computed at the top of the function. --- src/game/server/neo/neo_player.cpp | 39 +++++++++++++----------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index e85940a6c..3db1fd78f 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -3570,6 +3570,12 @@ void CNEO_Player::GiveLoadoutWeapon(void) //DevMsg("Loadout slot: %i (\"%s\") for %s\n", m_iLoadoutWepChoice.Get(), szWep, GetPlayerName()); #endif + // XP eligibility first, so ineligible picks don't pay a weapon entity create/destroy cycle + if (m_iLoadoutWepChoice+1 > CNEOWeaponLoadout::GetNumberOfLoadoutWeapons(CNEOWeaponLoadout::GetEffectiveXP(m_iXP), iLoadoutClass)) + { + return; + } + // If I already own this type don't create one const int wepSubType = 0; if (Weapon_OwnsThisType(szWep, wepSubType)) @@ -3593,33 +3599,22 @@ void CNEO_Player::GiveLoadoutWeapon(void) CNEOBaseCombatWeapon *pNeoWeapon = assert_cast((CBaseEntity*)pEnt); if (pNeoWeapon) { - if (m_iLoadoutWepChoice+1 <= CNEOWeaponLoadout::GetNumberOfLoadoutWeapons(CNEOWeaponLoadout::GetEffectiveXP(m_iXP), - sv_neo_dev_loadout.GetBool() ? NEO_LOADOUT_DEV : m_iNeoClass.Get())) - { - pNeoWeapon->SetSubType(wepSubType); + pNeoWeapon->SetSubType(wepSubType); - DispatchSpawn(pEnt); + DispatchSpawn(pEnt); - if (pEnt != NULL && !(pEnt->IsMarkedForDeletion())) + if (pEnt != NULL && !(pEnt->IsMarkedForDeletion())) + { + RemoveAllItems(false); + GiveDefaultItems(); + if (!BumpWeapon(pNeoWeapon)) { - RemoveAllItems(false); - GiveDefaultItems(); - if (!BumpWeapon(pNeoWeapon)) - { - UTIL_Remove( pNeoWeapon ); - } - else - { - pEnt->Touch( this ); - Weapon_Switch(Weapon_OwnsThisType(szWep)); - } + UTIL_Remove( pNeoWeapon ); } - } - else - { - if (pEnt != NULL && !(pEnt->IsMarkedForDeletion())) + else { - UTIL_Remove(pEnt); + pEnt->Touch( this ); + Weapon_Switch(Weapon_OwnsThisType(szWep)); } } } From dc853b07b1210bd05cc8b54333e794251649a5cc Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 18 Aug 2026 21:03:28 -0700 Subject: [PATCH 2/2] RequestSetLoadout: check XP eligibility before the validation entity The XP check compared against static loadout table data but ran after CreateEntityByName, so every above-tier request created and destroyed a probe weapon entity before falling back to the default slot. Hoist the check above entity creation and return the fallback directly, matching the eligibility-first ordering of the other grant paths. The probe entity is still created for eligible requests, since type validation (IsNeoPrimary) genuinely needs the weapon instance. Recursion into slot 0 stays terminating: every playable class has an XP_ANY weapon in slot 0, and classes without loadouts bail out at the empty-name check before recursing. --- src/game/server/neo/neo_player.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 3db1fd78f..3b8766362 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -355,6 +355,14 @@ bool CNEO_Player::RequestSetLoadout(int loadoutNumber) return false; } + // XP eligibility first, so ineligible requests fall back to the default + // slot without paying for the validation entity below + if (loadoutNumber+1 > CNEOWeaponLoadout::GetNumberOfLoadoutWeapons(CNEOWeaponLoadout::GetEffectiveXP(m_iXP), iLoadoutClass)) + { + DevMsg("Insufficient XP for %s\n", pszWepName); + return RequestSetLoadout(0); + } + EHANDLE pEnt; pEnt = CreateEntityByName(pszWepName); @@ -389,13 +397,6 @@ bool CNEO_Player::RequestSetLoadout(int loadoutNumber) result = false; } - if (loadoutNumber+1 > CNEOWeaponLoadout::GetNumberOfLoadoutWeapons(CNEOWeaponLoadout::GetEffectiveXP(m_iXP), - sv_neo_dev_loadout.GetBool() ? NEO_LOADOUT_DEV : classChosen)) - { - DevMsg("Insufficient XP for %s\n", pszWepName); - result = RequestSetLoadout(0); - } - if (result) { m_iLoadoutWepChoice = loadoutNumber;