From ea07be9335782393dcd3d79344935272e20bc07a Mon Sep 17 00:00:00 2001 From: SuperCake Date: Thu, 30 Jul 2026 23:24:06 +0200 Subject: [PATCH 1/3] feat(player): logic for spectating in order --- src/game/server/swarm/asw_player.cpp | 84 +++++++++++++++++++++++++++- src/game/server/swarm/asw_player.h | 6 ++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/game/server/swarm/asw_player.cpp b/src/game/server/swarm/asw_player.cpp index 2f43f2825..039f5f167 100644 --- a/src/game/server/swarm/asw_player.cpp +++ b/src/game/server/swarm/asw_player.cpp @@ -57,6 +57,7 @@ #include "missionchooser/iasw_mission_chooser_source.h" #include "rd_vgui_vscript_shared.h" #include "rd_crafting_defs.h" +#include // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -2173,8 +2174,89 @@ HSCRIPT CASW_Player::ScriptGetNPC() const return ToHScript( GetNPC() ); } +bool CASW_Player::CompareSpectatingPriority( CASW_Marine* pM1, CASW_Marine* pM2 ) const +{ + // Assign worst priority if no marine or no profile index + const int iProfile1 = pM1 ? pM1->GetMarineProfile()->m_ProfileIndex : INT_MAX; + const int iProfile2 = pM2 ? pM2->GetMarineProfile()->m_ProfileIndex : INT_MAX; + + const int iPriority1 = ( iProfile1 >= 0 && iProfile1 < ASW_NUM_MARINE_PROFILES ) ? + m_iSpectatingPrioMapping[iProfile1] : INT_MAX; + const int iPriority2 = ( iProfile2 >= 0 && iProfile2 < ASW_NUM_MARINE_PROFILES ) ? + m_iSpectatingPrioMapping[iProfile2] : INT_MAX; + + return iPriority1 < iPriority2; +} + +void CASW_Player::SpectateNextMarineInOrder() +{ + CASW_Game_Resource* pGameResource = ASWGameResource(); + if ( !pGameResource ) + return; + + int nMarinesSorted = 0; + CASW_Marine* pMarinesSorted[ASW_MAX_MARINE_RESOURCES] = { NULL }; + + // Gather all valid marines + for ( int i = 0; i < pGameResource->GetMaxMarineResources(); i++ ) + { + CASW_Marine_Resource* pMR = pGameResource->GetMarineResource( i ); + CASW_Marine *pMarine = pMR ? pMR->GetMarineEntity() : NULL; + if ( pMarine && pMarine->IsAlive() && pMarine->GetHealth() > 0 ) + { + pMarinesSorted[nMarinesSorted] = pMarine; + nMarinesSorted++; + } + } + + std::sort( + pMarinesSorted, + pMarinesSorted + nMarinesSorted, + [this](CASW_Marine* a, CASW_Marine* b) { + return CompareSpectatingPriority(a, b); + } + ); + + //Msg("CASW_Player::SpectateNextMarineInOrder\n"); + + //Msg(" set first guy as our first\n"); + CASW_Marine *pFirst = pMarinesSorted[0]; + + // loop through all valid marines + for ( int i = 0; i < nMarinesSorted; i++ ) + { + //Msg("Checking pMR %d\n", i); + CASW_Marine *pMarine = pMarinesSorted[i]; // Alive marine + + if (GetSpectatingNPC() == NULL) // if we're not spectating anything yet, then spectate the first one we find + { + //Msg(" We're not spectating anyone, so we're gonna spec this dude\n"); + SetSpectatingNPC(pMarine); + break; + } + if (GetSpectatingNPC() == pMarine) // if we're spectating this one, then clear it, so the next one we find will get set + { + //Msg(" we're spectating this dude, so clearing our current spectator\n"); + SetSpectatingNPC(NULL); + } + } + //Msg("end\n"); + // if we're still not spectating anything but we found at least marine, then that means we were spectating the last one in the list and need to set this + if (GetSpectatingNPC() == NULL && pFirst) + { + //Msg(" but we're still not speccing anyone and we have a first set, so speccing that dude\n"); + SetSpectatingNPC(pFirst); + } +} + void CASW_Player::SpectateNextMarine() { + if ( m_bSpectatingInOrder ) + { + SpectateNextMarineInOrder(); + return; + } + CASW_Game_Resource* pGameResource = ASWGameResource(); if (!pGameResource) return; @@ -2209,7 +2291,7 @@ void CASW_Player::SpectateNextMarine() { //Msg(" we're spectating this dude, so clearing our current spectator\n"); SetSpectatingNPC(NULL); - } + } } //Msg("end\n"); // if we're still not spectating anything but we found at least marine, then that means we were spectating the last one in the list and need to set this diff --git a/src/game/server/swarm/asw_player.h b/src/game/server/swarm/asw_player.h index fc8aa428e..d79432fac 100644 --- a/src/game/server/swarm/asw_player.h +++ b/src/game/server/swarm/asw_player.h @@ -87,6 +87,12 @@ class CASW_Player : public CBaseMultiplayerPlayer, public IASWPlayerAnimStateHel virtual bool ASWAnim_CanMove(); // spectating +private: + bool m_bSpectatingInOrder = false; + int m_iSpectatingPrioMapping[ASW_NUM_MARINE_PROFILES]; // profile -> priority; smaller number is higher priority +public: + bool CompareSpectatingPriority( CASW_Marine* pM1, CASW_Marine* pM2 ) const; + void SpectateNextMarineInOrder(); void SpectateNextMarine(); void SetSpectatingNPC( CASW_Inhabitable_NPC *pSpectating ); CASW_Inhabitable_NPC *GetSpectatingNPC() const; From 185a3735870073ff7c1da9acbd879ef38f71f6ef Mon Sep 17 00:00:00 2001 From: SuperCake Date: Thu, 30 Jul 2026 23:28:03 +0200 Subject: [PATCH 2/3] feat(player): set priority for spectating in order --- src/game/server/swarm/asw_player.cpp | 22 ++++++++++++++++++++++ src/game/server/swarm/asw_player.h | 2 ++ 2 files changed, 24 insertions(+) diff --git a/src/game/server/swarm/asw_player.cpp b/src/game/server/swarm/asw_player.cpp index 039f5f167..d1b17989f 100644 --- a/src/game/server/swarm/asw_player.cpp +++ b/src/game/server/swarm/asw_player.cpp @@ -2174,6 +2174,28 @@ HSCRIPT CASW_Player::ScriptGetNPC() const return ToHScript( GetNPC() ); } +// Can have some (not all) marine profiles; rest will have worst priority +void CASW_Player::SetSpectatingOrder( const int* iProfiles, int nProfiles ) +{ + // Prefill with worst priority in case a profile is not in the list or is invalid + for ( int i = 0; i < ASW_NUM_MARINE_PROFILES; i++ ) + { + m_iSpectatingPrioMapping[i] = INT_MAX; + } + + nProfiles = MIN( nProfiles, ASW_NUM_MARINE_PROFILES ); + for ( int i = 0; i < nProfiles; i++ ) + { + const int iProfile = iProfiles[i]; + if ( iProfile >= 0 && iProfile < ASW_NUM_MARINE_PROFILES ) + { + m_iSpectatingPrioMapping[iProfile] = i; + } + } + + m_bSpectatingInOrder = true; +} + bool CASW_Player::CompareSpectatingPriority( CASW_Marine* pM1, CASW_Marine* pM2 ) const { // Assign worst priority if no marine or no profile index diff --git a/src/game/server/swarm/asw_player.h b/src/game/server/swarm/asw_player.h index d79432fac..af6d394f0 100644 --- a/src/game/server/swarm/asw_player.h +++ b/src/game/server/swarm/asw_player.h @@ -91,6 +91,8 @@ class CASW_Player : public CBaseMultiplayerPlayer, public IASWPlayerAnimStateHel bool m_bSpectatingInOrder = false; int m_iSpectatingPrioMapping[ASW_NUM_MARINE_PROFILES]; // profile -> priority; smaller number is higher priority public: + void UnsetSpectatingOrder() { m_bSpectatingInOrder = false; } + void SetSpectatingOrder( const int* iProfiles, int nProfiles ); bool CompareSpectatingPriority( CASW_Marine* pM1, CASW_Marine* pM2 ) const; void SpectateNextMarineInOrder(); void SpectateNextMarine(); From 1bc2b76bbeaf264e4dd568444843328c2bc70e4b Mon Sep 17 00:00:00 2001 From: SuperCake Date: Thu, 6 Aug 2026 17:58:00 +0200 Subject: [PATCH 3/3] feat(player): add rd_spectate_order and rd_set_spectate_order and parser str -> int[] --- src/game/client/swarm/c_asw_player.cpp | 32 ++++++++++++++ src/game/server/swarm/asw_player.cpp | 34 ++++++++++++++ src/game/shared/swarm/asw_player_shared.cpp | 49 +++++++++++++++++++++ src/game/shared/swarm/asw_player_shared.h | 7 +++ 4 files changed, 122 insertions(+) diff --git a/src/game/client/swarm/c_asw_player.cpp b/src/game/client/swarm/c_asw_player.cpp index 072a54471..2b91c917a 100644 --- a/src/game/client/swarm/c_asw_player.cpp +++ b/src/game/client/swarm/c_asw_player.cpp @@ -149,6 +149,9 @@ ConVar asw_turret_fog_end( "asw_turret_fog_end", "1200", 0, "Fog end distance fo ConVar rd_force_spectate_marine( "rd_force_spectate_marine", "-1", FCVAR_DONTRECORD, "spectate this marine resource index if it exists", true, -1, true, ASW_MAX_MARINE_RESOURCES ); +static void set_rd_spectate_order( IConVar *pConVar = NULL, const char *pOldValue = NULL, float flOldValue = 0.0f ); +ConVar rd_spectate_order( "rd_spectate_order", "", FCVAR_ARCHIVE, "Prioritize this marines in spectate order. Space separated list. Example: \"4 1 7 3 0\".", set_rd_spectate_order ); + extern ConVar asw_allow_detach; extern ConVar asw_stim_cam_time; extern ConVar asw_rts_controls; @@ -1516,6 +1519,9 @@ void C_ASW_Player::OnDataChanged( DataUpdateType_t updateType ) } } + // send our rd_spectate_order to the server + set_rd_spectate_order(); + // tell other players that we're fully connected engine->ServerCmd( "cl_fullyjoined\n" ); } @@ -2999,3 +3005,29 @@ CSteamID C_ASW_Player::GetSteamID() CSteamID invalid; return invalid; } + +static void set_rd_spectate_order( IConVar *pConVar, const char *pOldValue, float flOldValue ) +{ + const char *pNewValue = pConVar ? ConVarRef( pConVar ).GetString() : rd_spectate_order.GetString(); + + int iProfiles[ASW_NUM_MARINE_PROFILES]; + const int nProfiles = parseSpectateOrder( pNewValue, iProfiles ); + + // Failed parsing + if ( nProfiles == -1 ) + { + if ( pConVar ) + { + ConVarRef( pConVar ).SetValue( pOldValue ); + } + return; + } + + if ( engine->IsInGame() ) + { + const int iBufLen = 30 + ASW_NUM_MARINE_PROFILES * 3; + char szbuf[iBufLen]; + Q_snprintf( szbuf, iBufLen, "rd_set_spectate_order \"%s\"\n", pNewValue ); + engine->ClientCmd( szbuf ); + } +} diff --git a/src/game/server/swarm/asw_player.cpp b/src/game/server/swarm/asw_player.cpp index d1b17989f..cafbd14c6 100644 --- a/src/game/server/swarm/asw_player.cpp +++ b/src/game/server/swarm/asw_player.cpp @@ -2174,6 +2174,40 @@ HSCRIPT CASW_Player::ScriptGetNPC() const return ToHScript( GetNPC() ); } +CON_COMMAND_F( rd_set_spectate_order, "Prioritize this marines in spectate order. Example: \"4 1 7 3 0\". \"\" for unset.", FCVAR_HIDDEN ) +{ + CASW_Player *pPlayer = ToASW_Player( UTIL_GetCommandClient() ); + if ( !pPlayer ) + { + Warning( "%s: Not a Player (not connected to a server?)\n", args[0] ); + return; + } + + if ( args.ArgC() != 2 ) + { + Warning( "%s: Expected quoted space separated list. Example: \"4 1 7 3 0\". \"\" for unset.\n", args[0] ); + return; + } + + int iProfiles[ASW_NUM_MARINE_PROFILES]; + const int nProfiles = parseSpectateOrder( args[1], iProfiles ); + + // Failed parsing + if ( nProfiles == -1 ) + { + return; + } + + // Empty string + if ( nProfiles == 0 ) + { + pPlayer->UnsetSpectatingOrder(); + return; + } + + pPlayer->SetSpectatingOrder( iProfiles, nProfiles ); +} + // Can have some (not all) marine profiles; rest will have worst priority void CASW_Player::SetSpectatingOrder( const int* iProfiles, int nProfiles ) { diff --git a/src/game/shared/swarm/asw_player_shared.cpp b/src/game/shared/swarm/asw_player_shared.cpp index 5fa82027d..fa98ed77d 100644 --- a/src/game/shared/swarm/asw_player_shared.cpp +++ b/src/game/shared/swarm/asw_player_shared.cpp @@ -1452,3 +1452,52 @@ DLL_EXPORT ASW_Controls_t GetASWControlsForPlayer( CASW_Player *pPlayer ) return pPlayer->GetASWControls(); } + +int parseSpectateOrder( const char *szInput, int *iProfiles, const int nProfilesMax ) +{ + Assert( nProfilesMax > 0 && nProfilesMax <= ASW_NUM_MARINE_PROFILES ); + + if ( Q_strlen( szInput ) == 0 ) + { + return 0; + } + + CSplitString szProfiles( szInput, " " ); + const int nProfiles = szProfiles.Count(); + + if ( nProfiles > nProfilesMax ) + { + Warning( "parse specate order: Too many profiles (%d > %d)\n", nProfiles, nProfilesMax ); + return -1; + } + + for ( int i = 0; i < nProfiles; i++ ) + { + const int iProfile = atoi( szProfiles[i] ); + + if ( iProfile == 0 && szProfiles[i][0] != '0' ) + { + Warning( "parse specate order: Not a number (#%d: %s)\n", i + 1, szProfiles[i] ); + return -1; + } + + if ( iProfile < 0 || iProfile >= nProfilesMax ) + { + Warning( "parse specate order: Incorrect profile number (#%d: %s)\n", i + 1, szProfiles[i] ); + return -1; + } + + for ( int j = 0; j < i; j++ ) + { + if ( iProfiles[j] == iProfile ) + { + Warning( "parse specate order: Duplicate profile number (#%d: %d)\n", i + 1, iProfile ); + return -1; + } + } + + iProfiles[i] = iProfile; + } + + return nProfiles; +} diff --git a/src/game/shared/swarm/asw_player_shared.h b/src/game/shared/swarm/asw_player_shared.h index be7c8b740..2a3f32a89 100644 --- a/src/game/shared/swarm/asw_player_shared.h +++ b/src/game/shared/swarm/asw_player_shared.h @@ -12,6 +12,8 @@ #pragma once #endif +#include "asw_shareddefs.h" + // Shared header file for players #if defined( CLIENT_DLL ) #define CASW_Player C_ASW_Player @@ -57,4 +59,9 @@ enum CASW_Earned_XP_t ASW_NUM_XP_TYPES }; +// Parse space separated profile numbers into iProfiles +// @returns Number of parsed profiles +// @returns -1 if parsing failed +int parseSpectateOrder( const char *szInput, int *iProfiles, const int nProfilesMax = ASW_NUM_MARINE_PROFILES ); + #endif // ASW_PLAYER_SHARED_H