Skip to content

Commit

Permalink
allow weapons to share menu slots
Browse files Browse the repository at this point in the history
A hacky solution that involves lying to the client. When a player picks up a weapon that shares a slot with other weapons, the client will be told that they have every possible weapon for the slot. User messages then overwrite the client's knowledge of other weapons that can use the slot, preventing missing/wrong icons.

If a player attempts to hold multiple weapons that share a slot, the held weapon will be dropped to make room for the newly collected one.

also precaching custom weapons used in the map cfg.
  • Loading branch information
wootguy committed Jan 5, 2025
1 parent 3de7e45 commit 7a44991
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 110 deletions.
2 changes: 1 addition & 1 deletion cl_dll/hl/hl_baseentity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ Vector CBasePlayer :: AutoaimDeflection( Vector &vecSrc, float flDist, float flD
void CBasePlayer :: ResetAutoaim( ) { }
void CBasePlayer :: SetCustomDecalFrames( int nFrames ) { }
int CBasePlayer :: GetCustomDecalFrames( void ) { return -1; }
void CBasePlayer::DropPlayerItem ( char *pszItemName ) { }
void CBasePlayer::DropPlayerItem ( const char *pszItemName ) { }
BOOL CBasePlayer::HasPlayerItem( CBasePlayerItem *pCheckItem ) { return FALSE; }
BOOL CBasePlayer :: SwitchWeapon( CBasePlayerItem *pWeapon ) { return FALSE; }
Vector CBasePlayer :: GetGunPosition( void ) { return g_vecZero; }
Expand Down
125 changes: 49 additions & 76 deletions dlls/hooks/hlds_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -558,81 +558,6 @@ void ServerDeactivate( void )

#include "lagcomp.h"

void PrecacheWeapons() {
if (g_mapWeapons.find("weapon_shotgun") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_shotgun");
UTIL_PrecacheOther("ammo_buckshot");
}
if (g_mapWeapons.find("weapon_crowbar") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_crowbar");
}
if (g_mapWeapons.find("weapon_9mmhandgun") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_9mmhandgun");
UTIL_PrecacheOther("ammo_9mmclip");
}
if (g_mapWeapons.find("weapon_9mmAR") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_9mmAR");
UTIL_PrecacheOther("ammo_9mmAR");
UTIL_PrecacheOther("ammo_ARgrenades");
}
if (g_mapWeapons.find("weapon_357") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_357");
UTIL_PrecacheOther("ammo_357");
}
if (g_mapWeapons.find("weapon_gauss") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_gauss");
UTIL_PrecacheOther("ammo_gaussclip");
}
if (g_mapWeapons.find("weapon_rpg") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_rpg");
UTIL_PrecacheOther("ammo_rpgclip");
}
if (g_mapWeapons.find("weapon_crossbow") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_crossbow");
UTIL_PrecacheOther("ammo_crossbow");
}
if (g_mapWeapons.find("weapon_egon") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_egon");
UTIL_PrecacheOther("ammo_gaussclip");
}
if (g_mapWeapons.find("weapon_tripmine") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_tripmine");
}
if (g_mapWeapons.find("weapon_satchel") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_satchel");
}
if (g_mapWeapons.find("weapon_handgrenade") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_handgrenade");
}
if (g_mapWeapons.find("weapon_snark") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_snark");
}
if (g_mapWeapons.find("weapon_hornetgun") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_hornetgun");
}
if (g_mapWeapons.find("weapon_grapple") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_grapple");
}
if (g_mapWeapons.find("weapon_pipewrench") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_pipewrench");
}
if (g_mapWeapons.find("weapon_displacer") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_displacer");
}
if (g_mapWeapons.find("weapon_shockrifle") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_shockrifle");
}
if (g_mapWeapons.find("weapon_sporelauncher") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_sporelauncher");
}
if (g_mapWeapons.find("weapon_medkit") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_medkit");
}
if (g_mapWeapons.find("weapon_inventory") != g_mapWeapons.end()) {
UTIL_PrecacheOther("weapon_inventory");
}
}

void PrecacheTextureSounds() {
if (g_textureStats.tex_concrete) {
PRECACHE_FOOTSTEP_SOUNDS(g_stepSoundsConcrete)
Expand Down Expand Up @@ -690,6 +615,41 @@ void PrecacheTextureSounds() {
}
}

int g_weaponSlotMasks[MAX_WEAPONS];

void MarkWeaponSlotConflicts() {
for (int i = 0; i < MAX_WEAPONS; i++)
{
ItemInfo& II = CBasePlayerItem::ItemInfoArray[i];

if (!II.iId)
continue;

int mask = 1 << II.iId;

for (int i = 0; i < MAX_WEAPONS; i++)
{
ItemInfo& II2 = CBasePlayerItem::ItemInfoArray[i];

if (!II2.iId)
continue;

// when multiple weapons share a slot, the client may not show any weapon in the menu
// if only one weapon is held from that slot (depending on weapon ID order). For example,
// if picking up ID 1, the menu slot is filled as expected, but then immediately removed
// because ID 2 isn't held, which shares the same slot (see WeaponsResource::DropWeapon).
// So, the client will be told that it holds every possible weapon for that slot, if any
// any weapon is held for that slot. A network message controls which weapon is rendered
// in the shared slot.
if (II.iSlot == II2.iSlot && II.iPosition == II2.iPosition) {
mask |= 1 << II2.iId;
}
}

g_weaponSlotMasks[II.iId] = mask;
}
}

void ServerActivate( edict_t *pEdictList, int edictCount, int clientMax )
{
int i;
Expand Down Expand Up @@ -732,9 +692,14 @@ void ServerActivate( edict_t *pEdictList, int edictCount, int clientMax )

AddMapPluginEquipment();

PrecacheWeapons();
for (std::string wepName : g_mapWeapons) {
UTIL_PrecacheOther(wepName.c_str());
}

PrecacheTextureSounds();

MarkWeaponSlotConflicts();

if (mp_antiblock.value)
PRECACHE_SOUND_ENT(NULL, "weapons/xbow_hitbod2.wav");

Expand Down Expand Up @@ -1866,6 +1831,14 @@ void UpdateClientData ( const edict_t *ent, int sendweapons, struct clientdata_s
cd->watertype = pev->watertype;
cd->weapons = pev->weapons;

for (int i = 0; i < MAX_WEAPONS; i++) {
if (pev->weapons & (1 << i)) {
// weapons that share slots occupy multiple bits so that the menu renders correctly
// (client may think the slot is empty if only one weapon is held from a shared slot)
cd->weapons |= g_weaponSlotMasks[i];
}
}

if (pl->m_fakeSuit) {
cd->weapons |= 1 << WEAPON_SUIT;
}
Expand Down
109 changes: 89 additions & 20 deletions dlls/player/CBasePlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,7 @@ void CBasePlayer::TabulateWeapons(void) {
CBasePlayerItem* pPlayerItem = (CBasePlayerItem*)m_rgpPlayerItems[i].GetEntity();

while (pPlayerItem) {
pev->weapons |= (1 << pPlayerItem->m_iId);
pev->weapons |= g_weaponSlotMasks[pPlayerItem->m_iId];
pPlayerItem = (CBasePlayerItem*)pPlayerItem->m_pNext.GetEntity();
}
}
Expand Down Expand Up @@ -4241,6 +4241,8 @@ int CBasePlayer::AddPlayerItem( CBasePlayerItem *pItem )
SwitchWeapon( pItem );
}

ResolveWeaponSlotConflict(pItem->m_iId);

return TRUE;
}
else if (gEvilImpulse101)
Expand Down Expand Up @@ -4657,32 +4659,38 @@ void CBasePlayer :: UpdateClientData( void )

for (i = 0; i < MAX_WEAPONS; i++)
{
ItemInfo& II = CBasePlayerItem::ItemInfoArray[i];
ItemInfo* II = &CBasePlayerItem::ItemInfoArray[i];

int conflictId = GetCurrentIdForConflictedSlot(i);
if (conflictId != -1 && conflictId != i) {
// all weapon infos that share a slot must point to currently held weapon in the shared slot
// or else the client won't render the menu correctly
II = &CBasePlayerItem::ItemInfoArray[conflictId];
}

if ( !II.iId )
if ( !II->iId )
continue;

const char *pszName;
if (!II.pszName)
if (!II->pszName)
pszName = "Empty";
else
pszName = II.pszName;

MESSAGE_BEGIN( MSG_ONE, gmsgWeaponList, NULL, pev );
WRITE_STRING(pszName); // string weapon name
WRITE_BYTE(GetAmmoIndex(II.pszAmmo1)); // byte Ammo Type
WRITE_BYTE(II.iMaxAmmo1); // byte Max Ammo 1
WRITE_BYTE(GetAmmoIndex(II.pszAmmo2)); // byte Ammo2 Type
WRITE_BYTE(II.iMaxAmmo2); // byte Max Ammo 2
WRITE_BYTE(II.iSlot); // byte bucket
WRITE_BYTE(II.iPosition); // byte bucket pos
WRITE_BYTE(II.iId); // byte id (bit index into pev->weapons)
WRITE_BYTE(II.iFlags); // byte Flags
pszName = II->pszName;

MESSAGE_BEGIN(MSG_ONE, gmsgWeaponList, NULL, pev);
WRITE_STRING(II->pszName); // string weapon name
WRITE_BYTE(GetAmmoIndex(II->pszAmmo1)); // byte Ammo Type
WRITE_BYTE(II->iMaxAmmo1); // byte Max Ammo 1
WRITE_BYTE(GetAmmoIndex(II->pszAmmo2)); // byte Ammo2 Type
WRITE_BYTE(II->iMaxAmmo2); // byte Max Ammo 2
WRITE_BYTE(II->iSlot); // byte bucket
WRITE_BYTE(II->iPosition); // byte bucket pos
WRITE_BYTE(i); // byte id (bit index into pev->weapons)
WRITE_BYTE(II->iFlags); // byte Flags
MESSAGE_END();
}
}


SendAmmoUpdate();

// Update all the items
Expand Down Expand Up @@ -5140,7 +5148,7 @@ void CBasePlayer::CleanupWeaponboxes(void)
// DropPlayerItem - drop the named item, or if no name,
// the active item.
//=========================================================
void CBasePlayer::DropPlayerItem ( char *pszItemName )
void CBasePlayer::DropPlayerItem ( const char *pszItemName )
{
if ( !g_pGameRules->IsMultiplayer() )
{
Expand Down Expand Up @@ -5169,10 +5177,13 @@ void CBasePlayer::DropPlayerItem ( char *pszItemName )

while ( pWeapon )
{
ItemInfo info;
pWeapon->GetItemInfo(&info);

if ( pszItemName )
{
// try to match by name.
if ( !strcmp( pszItemName, STRING( pWeapon->pev->classname ) ) )
// try to match by classname or name.
if ( !strcmp( pszItemName, STRING( pWeapon->pev->classname ) ) || !strcmp(pszItemName, info.pszName))
{
// match!
break;
Expand Down Expand Up @@ -6331,4 +6342,62 @@ void CBasePlayer::LoadScore() {
m_iDeaths = 0;
m_scoreMultiplier = 1.0f;
}
}

void CBasePlayer::ResolveWeaponSlotConflict(int wepId) {
int mask = g_weaponSlotMasks[wepId];

if (count_bits_set(mask) <= 1) {
return; // impossible for there to be a conflict
}

ItemInfo& II = CBasePlayerItem::ItemInfoArray[wepId];

for (int i = 0; i < MAX_WEAPONS; i++) {
int bit = (1 << i);
if (mask & bit) {
if ((pev->weapons & bit) && i != wepId) {
// player is already holding a weapon that fills this slot.
// Drop this held weapon because the player won't be able to choose which
// weapon they want from that slot.
ItemInfo& dropInfo = CBasePlayerItem::ItemInfoArray[i];
DropPlayerItem(dropInfo.pszName);
}

// redirect all item info to the weapon with the given ID
MESSAGE_BEGIN(MSG_ONE, gmsgWeaponList, NULL, pev);
WRITE_STRING(II.pszName); // string weapon name
WRITE_BYTE(GetAmmoIndex(II.pszAmmo1)); // byte Ammo Type
WRITE_BYTE(II.iMaxAmmo1); // byte Max Ammo 1
WRITE_BYTE(GetAmmoIndex(II.pszAmmo2)); // byte Ammo2 Type
WRITE_BYTE(II.iMaxAmmo2); // byte Max Ammo 2
WRITE_BYTE(II.iSlot); // byte bucket
WRITE_BYTE(II.iPosition); // byte bucket pos
WRITE_BYTE(i); // byte id (bit index into pev->weapons)
WRITE_BYTE(II.iFlags); // byte Flags
MESSAGE_END();
ALERT(at_console, "acktually, ID %d is now for %s\n", i, II.pszName);
}
}
}

int CBasePlayer::GetCurrentIdForConflictedSlot(int wepId) {
int mask = g_weaponSlotMasks[wepId];

ItemInfo& II = CBasePlayerItem::ItemInfoArray[wepId];

if (count_bits_set(mask) <= 1) {
return wepId; // impossible for there to be a conflict
}

for (int i = 0; i < MAX_WEAPONS; i++) {
int bit = (1 << i);
if (mask & bit) {
if ((pev->weapons & bit) && i != wepId) {
return i;
}
}
}

return -1;
}
11 changes: 10 additions & 1 deletion dlls/player/CBasePlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ class EXPORT CBasePlayer : public CBaseMonster
void AddPointsToTeam( int score, BOOL bAllowNegativeScore );
BOOL AddPlayerItem( CBasePlayerItem *pItem );
BOOL RemovePlayerItem( CBasePlayerItem *pItem );
void DropPlayerItem ( char *pszItemName );
void DropPlayerItem ( const char *pszItemName );
void DropAmmo(bool secondary);
BOOL HasPlayerItem( CBasePlayerItem *pCheckItem );
CBasePlayerItem* GetNamedPlayerItem(const char* pszItemName);
Expand Down Expand Up @@ -535,6 +535,15 @@ class EXPORT CBasePlayer : public CBaseMonster

// load score from global state, or initialize to 0
void LoadScore();

// tell the client which weapon belongs in a slot which multiple weapons can fill
void ResolveWeaponSlotConflict(int wepId);

// if a weapon slot can be filled by multiple weapons, this returns the weapon ID
// that is currently held in that slot. If no weapon is filling the slot
// that queryWepId fills, then -1 is returned.
// queryWepId can be any weapon that fills the slot in question
int GetCurrentIdForConflictedSlot(int queryWepId);

// for sven-style monster info
//void UpdateMonsterInfo();
Expand Down
11 changes: 11 additions & 0 deletions dlls/util/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3255,3 +3255,14 @@ const char* cstr(string_t s) {
return STRING(s);
}

uint32_t count_bits_set(uint32_t v) {
// https://graphics.stanford.edu/~seander/bithacks.html
uint32_t c; // c accumulates the total bits set in v

for (c = 0; v; c++) {
v &= v - 1; // clear the least significant bit set
}

return c;
}

5 changes: 4 additions & 1 deletion dlls/util/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ extern EXPORT globalvars_t *gpGlobals;

extern std::unordered_set<std::string> g_weaponNames; // names given by weapons (may have a prefix: "hlcoop/weapon_grapple")
extern std::unordered_set<std::string> g_weaponClassnames; // valid weapon classnames
extern int g_weaponSlotMasks[MAX_WEAPONS]; // for handling slot conflict

extern int g_serveractive; // 1 if ServerActivate was called (no longer safe to precache)
extern int g_edictsinit; // 1 if all edicts were allocated so that relocations can begin
Expand Down Expand Up @@ -869,6 +870,8 @@ EXPORT bool folderExists(const std::string& path);

EXPORT uint64_t getFreeSpace(const std::string& path);

EXPORT uint32_t count_bits_set(uint32_t v);

EXPORT void UTIL_BeamFollow(int entindex, int modelIdx, int life, int width, RGBA color, int msgMode=MSG_BROADCAST, const float* msgOrigin=NULL, edict_t* targetEnt=NULL);
EXPORT void UTIL_Fizz(int eidx, int modelIdx, uint8_t density, int msgMode=MSG_BROADCAST, const float* msgOrigin=NULL, edict_t* targetEnt=NULL);
EXPORT void UTIL_ELight(int entindex, int attachment, Vector origin, float radius, RGBA color, int life, float decay, int msgMode=MSG_BROADCAST, const float* msgOrigin=NULL, edict_t* targetEnt=NULL);
Expand All @@ -879,4 +882,4 @@ EXPORT void UTIL_KillBeam(int entindex, int msgMode = MSG_BROADCAST, const float
EXPORT void UTIL_BSPDecal(int entindex, Vector origin, int decalIdx, int msgMode=MSG_BROADCAST, const float* msgOrigin=NULL, edict_t* targetEnt=NULL);
EXPORT void UTIL_PlayerDecal(int entindex, int playernum, Vector origin, int decalIdx, int msgMode = MSG_BROADCAST, const float* msgOrigin = NULL, edict_t* targetEnt = NULL);
EXPORT void UTIL_GunshotDecal(int entindex, Vector origin, int decalIdx, int msgMode = MSG_BROADCAST, const float* msgOrigin = NULL, edict_t* targetEnt = NULL);
EXPORT void UTIL_Decal(int entindex, Vector origin, int decalIdx, int msgMode = MSG_BROADCAST, const float* msgOrigin = NULL, edict_t* targetEnt = NULL);
EXPORT void UTIL_Decal(int entindex, Vector origin, int decalIdx, int msgMode = MSG_BROADCAST, const float* msgOrigin = NULL, edict_t* targetEnt = NULL);
1 change: 1 addition & 0 deletions dlls/weapon/CGlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ void CGlock::Precache( void )
PRECACHE_SOUND ("weapons/pl_gun3.wav");//handgun

UTIL_PrecacheOther("ammo_9mm");
UTIL_PrecacheOther("ammo_9mmclip");

PrecacheEvents();
}
Expand Down
Loading

0 comments on commit 7a44991

Please sign in to comment.