mirror of
https://github.com/nillerusr/source-engine.git
synced 2026-07-16 06:19:58 +00:00
Add MCP debug console commands + Rust runtime gitignore
Server-side: sg_mcp_debug.cpp — 20 debug/RCON commands for entity inspection, player manipulation, weapon spawning, button simulation, and networking stats. Client-side: sg_mcp_debug_client.cpp — 15 commands for VGUI panel tree inspection, material diagnostics, cursor/keyboard simulation, and client entity queries. VPC: Register both files in server_hl2mp.vpc and client_hl2mp.vpc Stargate folders. Gitignore: Exclude source-engine-runtime/ (separate Rust workspace with its own git history).
This commit is contained in:
parent
44cb9a4837
commit
d2e77350e6
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,3 +37,4 @@ waf3*/
|
||||
.vscode/
|
||||
.depproj/
|
||||
source-engine.sln
|
||||
/source-engine-runtime/
|
||||
|
||||
@ -174,6 +174,7 @@ $Project "Client (HL2MP)"
|
||||
$Folder "Stargate"
|
||||
{
|
||||
$File "$SRCDIR\game\shared\stargate\sg_weapons.cpp"
|
||||
$File "$SRCDIR\game\shared\stargate\sg_mcp_debug_client.cpp"
|
||||
$File "$SRCDIR\game\shared\stargate\sg_constants.h"
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,6 +302,7 @@ $Project "Server (HL2MP)"
|
||||
$File "stargate\ring_transporter.cpp"
|
||||
$File "stargate\sg_event.cpp"
|
||||
$File "stargate\sg_kawoosh.cpp"
|
||||
$File "stargate\sg_mcp_debug.cpp"
|
||||
$File "stargate\sg_player_spawn.cpp"
|
||||
$File "stargate\STUBS.cpp"
|
||||
$File "$SRCDIR\game\shared\stargate\sg_weapons.cpp"
|
||||
|
||||
538
game/server/stargate/sg_mcp_debug.cpp
Normal file
538
game/server/stargate/sg_mcp_debug.cpp
Normal file
@ -0,0 +1,538 @@
|
||||
//============= Copyright (c) StargateTC Source Team ==========================
|
||||
//
|
||||
// MCP debug console commands. These are designed to be called via RCON from
|
||||
// the tools/mcp_source_engine/ Python MCP server. They emit structured output
|
||||
// (one JSON-ish line per record) that is easy to parse from the other side.
|
||||
//
|
||||
// Server-side only.
|
||||
//
|
||||
//=============================================================================
|
||||
#include "cbase.h"
|
||||
#include <typeinfo>
|
||||
#include "baseentity.h"
|
||||
#include "player.h"
|
||||
#include "hl2mp/hl2mp_gamerules.h"
|
||||
#include "team.h"
|
||||
#include "in_buttons.h"
|
||||
|
||||
#include "tier0/memdbgon.h"
|
||||
|
||||
static int SG_CountPlayers()
|
||||
{
|
||||
int n = 0;
|
||||
for ( int i = 1; i <= gpGlobals->maxClients; ++i )
|
||||
{
|
||||
if ( UTIL_PlayerByIndex( i ) ) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_list_entities [class_filter]
|
||||
// Emits one line per live entity:
|
||||
// ENT idx=N class=<classname> name=<targetname> origin=x,y,z
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_list_entities, "Dump server entity table (one line per entity)." )
|
||||
{
|
||||
const char *pFilter = args.ArgC() > 1 ? args.Arg( 1 ) : NULL;
|
||||
|
||||
int nMax = gpGlobals->maxEntities;
|
||||
int nCount = 0;
|
||||
for ( int i = 0; i < nMax; ++i )
|
||||
{
|
||||
CBaseEntity *pEnt = CBaseEntity::Instance( engine->PEntityOfEntIndex( i ) );
|
||||
if ( !pEnt ) continue;
|
||||
|
||||
const char *pClass = pEnt->GetClassname();
|
||||
if ( pFilter && pFilter[0] && !Q_stristr( pClass, pFilter ) )
|
||||
continue;
|
||||
|
||||
Vector v = pEnt->GetAbsOrigin();
|
||||
const char *pName = STRING( pEnt->GetEntityName() );
|
||||
Msg( "ENT idx=%d class=%s name=%s origin=%.1f,%.1f,%.1f\n",
|
||||
i, pClass, pName && pName[0] ? pName : "-", v.x, v.y, v.z );
|
||||
++nCount;
|
||||
}
|
||||
Msg( "ENT_COUNT %d\n", nCount );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_dump_entity <index>
|
||||
// Verbose dump of one entity: class, name, health, origin, angles, model.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_dump_entity, "Dump details for one server entity by index." )
|
||||
{
|
||||
if ( args.ArgC() < 2 )
|
||||
{
|
||||
Msg( "Usage: sg_dump_entity <index>\n" );
|
||||
return;
|
||||
}
|
||||
int idx = atoi( args.Arg( 1 ) );
|
||||
edict_t *pEdict = engine->PEntityOfEntIndex( idx );
|
||||
CBaseEntity *pEnt = pEdict ? CBaseEntity::Instance( pEdict ) : NULL;
|
||||
if ( !pEnt )
|
||||
{
|
||||
Msg( "DUMP_FAIL idx=%d (no entity)\n", idx );
|
||||
return;
|
||||
}
|
||||
|
||||
Vector origin = pEnt->GetAbsOrigin();
|
||||
QAngle ang = pEnt->GetAbsAngles();
|
||||
const char *pModel = STRING( pEnt->GetModelName() );
|
||||
const char *pName = STRING( pEnt->GetEntityName() );
|
||||
|
||||
Msg( "DUMP idx=%d class=%s name=%s health=%d/%d origin=%.1f,%.1f,%.1f angles=%.1f,%.1f,%.1f model=%s solid=%d move=%d\n",
|
||||
idx,
|
||||
pEnt->GetClassname(),
|
||||
pName && pName[0] ? pName : "-",
|
||||
pEnt->GetHealth(),
|
||||
pEnt->GetMaxHealth(),
|
||||
origin.x, origin.y, origin.z,
|
||||
ang.x, ang.y, ang.z,
|
||||
pModel && pModel[0] ? pModel : "-",
|
||||
(int)pEnt->GetSolid(),
|
||||
(int)pEnt->GetMoveType() );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_player_info — dump first connected player's state.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_player_info, "Dump player state (first connected)." )
|
||||
{
|
||||
for ( int i = 1; i <= gpGlobals->maxClients; ++i )
|
||||
{
|
||||
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
|
||||
if ( !pPlayer ) continue;
|
||||
|
||||
Vector v = pPlayer->GetAbsOrigin();
|
||||
QAngle a = pPlayer->GetAbsAngles();
|
||||
int team = pPlayer->GetTeamNumber();
|
||||
const char *pTeamName = pPlayer->GetTeam() ? pPlayer->GetTeam()->GetName() : "none";
|
||||
CBaseCombatWeapon *pWep = pPlayer->GetActiveWeapon();
|
||||
|
||||
Msg( "PLAYER idx=%d name=%s team=%d teamname=%s health=%d armor=%d origin=%.1f,%.1f,%.1f angles=%.1f,%.1f,%.1f weapon=%s\n",
|
||||
i,
|
||||
pPlayer->GetPlayerName(),
|
||||
team,
|
||||
pTeamName,
|
||||
pPlayer->GetHealth(),
|
||||
pPlayer->ArmorValue(),
|
||||
v.x, v.y, v.z,
|
||||
a.x, a.y, a.z,
|
||||
pWep ? pWep->GetClassname() : "-" );
|
||||
return;
|
||||
}
|
||||
Msg( "PLAYER_NONE\n" );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_dump_gamerules — dump CHL2MPRules state.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_dump_gamerules, "Dump Stargate gamerules state." )
|
||||
{
|
||||
if ( !g_pGameRules )
|
||||
{
|
||||
Msg( "GAMERULES none\n" );
|
||||
return;
|
||||
}
|
||||
Msg( "GAMERULES class=%s description=%s teamplay=%d multiplayer=%d\n",
|
||||
typeid( *g_pGameRules ).name(),
|
||||
g_pGameRules->GetGameDescription(),
|
||||
(int)g_pGameRules->IsTeamplay(),
|
||||
(int)g_pGameRules->IsMultiplayer() );
|
||||
|
||||
// Dump each registered team
|
||||
extern CUtlVector< CTeam * > g_Teams;
|
||||
for ( int i = 0; i < g_Teams.Count(); ++i )
|
||||
{
|
||||
CTeam *t = g_Teams[ i ];
|
||||
if ( !t ) continue;
|
||||
Msg( "TEAM idx=%d name=%s score=%d players=%d\n",
|
||||
i, t->GetName(), t->GetScore(), t->GetNumPlayers() );
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_give_all_stargate — give all 29 Stargate weapons to the calling player.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_give_all_stargate, "Give all Stargate weapons to player (cheat).", FCVAR_CHEAT )
|
||||
{
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer )
|
||||
{
|
||||
Msg( "GIVE_FAIL no player\n" );
|
||||
return;
|
||||
}
|
||||
|
||||
static const char *kStargateWeapons[] = {
|
||||
"weapon_9mmAR", "weapon_batsup", "weapon_c4", "weapon_coujaf",
|
||||
"weapon_couteau", "weapon_flashbang", "weapon_gacgoa", "weapon_glstaff",
|
||||
"weapon_goaregen", "weapon_invis", "weapon_lance", "weapon_larve",
|
||||
"weapon_m16", "weapon_m92s", "weapon_main", "weapon_medic",
|
||||
"weapon_mine", "weapon_mustardgrenade", "weapon_narcogrenade",
|
||||
"weapon_nervegrenade", "weapon_p90", "weapon_psg1", "weapon_reetou",
|
||||
"weapon_smokegrenade", "weapon_spas", "weapon_tacgun", "weapon_usas",
|
||||
"weapon_zat", "weapon_zatarc"
|
||||
};
|
||||
int nGiven = 0;
|
||||
for ( int i = 0; i < ARRAYSIZE( kStargateWeapons ); ++i )
|
||||
{
|
||||
pPlayer->GiveNamedItem( kStargateWeapons[ i ] );
|
||||
++nGiven;
|
||||
}
|
||||
Msg( "GIVE_ALL_STARGATE count=%d\n", nGiven );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_assert — trigger a controlled DebugBreak so we can verify the debugger
|
||||
// is attached. No-op in release if no debugger.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_debugbreak, "Trigger DebugBreak() — for verifying attached debugger.", FCVAR_CHEAT )
|
||||
{
|
||||
Msg( "About to DebugBreak — if a debugger is attached it will catch this.\n" );
|
||||
DebuggerBreak();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_teleport <x> <y> <z> — teleport calling player to absolute coords.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_teleport, "Teleport calling player to <x> <y> <z>.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 4 )
|
||||
{
|
||||
Msg( "Usage: sg_teleport <x> <y> <z>\n" );
|
||||
return;
|
||||
}
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer )
|
||||
{
|
||||
Msg( "TELEPORT_FAIL no_player\n" );
|
||||
return;
|
||||
}
|
||||
Vector v( atof( args.Arg(1) ), atof( args.Arg(2) ), atof( args.Arg(3) ) );
|
||||
pPlayer->Teleport( &v, NULL, &vec3_origin );
|
||||
Msg( "TELEPORT ok x=%.1f y=%.1f z=%.1f\n", v.x, v.y, v.z );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_give <weapon_class> — give a single weapon to the calling player.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_give, "Give one weapon by classname.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 )
|
||||
{
|
||||
Msg( "Usage: sg_give <weapon_class>\n" );
|
||||
return;
|
||||
}
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer )
|
||||
{
|
||||
Msg( "GIVE_FAIL no_player\n" );
|
||||
return;
|
||||
}
|
||||
const char *pName = args.Arg(1);
|
||||
pPlayer->GiveNamedItem( pName );
|
||||
Msg( "GIVE ok weapon=%s\n", pName );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_set_health <hp> — set player health.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_set_health, "Set player health.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_set_health <hp>\n" ); return; }
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer ) { Msg( "HEALTH_FAIL no_player\n" ); return; }
|
||||
int hp = atoi( args.Arg(1) );
|
||||
pPlayer->SetHealth( hp );
|
||||
Msg( "HEALTH ok hp=%d\n", hp );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_set_team <team_idx> — change calling player's team.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_set_team, "Set player team (2=Tau'ri, 3=Goa'uld, 4=Jaffa, 5=Unas).", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_set_team <team_idx>\n" ); return; }
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer ) { Msg( "TEAM_FAIL no_player\n" ); return; }
|
||||
int team = atoi( args.Arg(1) );
|
||||
pPlayer->ChangeTeam( team );
|
||||
Msg( "TEAM ok team=%d name=%s\n",
|
||||
team,
|
||||
pPlayer->GetTeam() ? pPlayer->GetTeam()->GetName() : "?" );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_force_attack — fire primary attack with the active weapon.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_force_attack, "Make calling player fire primary attack.", FCVAR_CHEAT )
|
||||
{
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer ) { Msg( "ATTACK_FAIL no_player\n" ); return; }
|
||||
CBaseCombatWeapon *pWep = pPlayer->GetActiveWeapon();
|
||||
if ( !pWep ) { Msg( "ATTACK_FAIL no_weapon\n" ); return; }
|
||||
pWep->PrimaryAttack();
|
||||
Msg( "ATTACK ok weapon=%s\n", pWep->GetClassname() );
|
||||
}
|
||||
|
||||
CON_COMMAND_F( sg_force_attack2, "Make calling player fire secondary attack.", FCVAR_CHEAT )
|
||||
{
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer ) { Msg( "ATTACK2_FAIL no_player\n" ); return; }
|
||||
CBaseCombatWeapon *pWep = pPlayer->GetActiveWeapon();
|
||||
if ( !pWep ) { Msg( "ATTACK2_FAIL no_weapon\n" ); return; }
|
||||
pWep->SecondaryAttack();
|
||||
Msg( "ATTACK2 ok weapon=%s\n", pWep->GetClassname() );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_kill_self — force kill the calling player.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_kill_self, "Force kill the calling player.", FCVAR_CHEAT )
|
||||
{
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer ) { Msg( "KILL_FAIL no_player\n" ); return; }
|
||||
pPlayer->CommitSuicide();
|
||||
Msg( "KILL ok\n" );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_spawn <classname> [x y z] — spawn an entity. Defaults to player position.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_spawn, "Spawn an entity at <x y z> (or player pos).", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_spawn <classname> [x y z]\n" ); return; }
|
||||
const char *pClass = args.Arg(1);
|
||||
Vector pos = vec3_origin;
|
||||
if ( args.ArgC() >= 5 )
|
||||
{
|
||||
pos.Init( atof( args.Arg(2) ), atof( args.Arg(3) ), atof( args.Arg(4) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
CBasePlayer *pPlayer = UTIL_GetCommandClient();
|
||||
if ( !pPlayer ) { Msg( "SPAWN_FAIL no_player_for_default_pos\n" ); return; }
|
||||
pos = pPlayer->GetAbsOrigin();
|
||||
}
|
||||
CBaseEntity *pEnt = CreateEntityByName( pClass );
|
||||
if ( !pEnt ) { Msg( "SPAWN_FAIL unknown_class=%s\n", pClass ); return; }
|
||||
pEnt->SetAbsOrigin( pos );
|
||||
DispatchSpawn( pEnt );
|
||||
pEnt->Activate();
|
||||
Msg( "SPAWN ok class=%s idx=%d origin=%.1f,%.1f,%.1f\n",
|
||||
pClass, pEnt->entindex(), pos.x, pos.y, pos.z );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_net_stats — dump server-side networking + tickrate state.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_net_stats, "Dump server networking stats." )
|
||||
{
|
||||
Msg( "NET_STATS tickcount=%d tickinterval=%.4f curtime=%.2f frametime=%.4f maxClients=%d\n",
|
||||
gpGlobals->tickcount,
|
||||
gpGlobals->interval_per_tick,
|
||||
gpGlobals->curtime,
|
||||
gpGlobals->frametime,
|
||||
gpGlobals->maxClients );
|
||||
// Per-player edict info
|
||||
for ( int i = 1; i <= gpGlobals->maxClients; ++i )
|
||||
{
|
||||
CBasePlayer *p = UTIL_PlayerByIndex( i );
|
||||
if ( !p ) continue;
|
||||
edict_t *e = p->edict();
|
||||
Msg( "NET_PLAYER idx=%d name=%s edict_free=%d edict_dirty=%d\n",
|
||||
i, p->GetPlayerName(),
|
||||
( e && e->IsFree() ) ? 1 : 0,
|
||||
( e && e->m_fStateFlags & FL_EDICT_DIRTY_PVS_INFORMATION ) ? 1 : 0 );
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_ent_messages <ent_idx> — dump entity's I/O message queue / connections.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_ent_messages, "Dump an entity's outputs/inputs." )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_ent_messages <idx>\n" ); return; }
|
||||
int idx = atoi( args.Arg(1) );
|
||||
edict_t *e = engine->PEntityOfEntIndex( idx );
|
||||
CBaseEntity *pEnt = e ? CBaseEntity::Instance( e ) : NULL;
|
||||
if ( !pEnt ) { Msg( "ENT_MSG_FAIL idx=%d\n", idx ); return; }
|
||||
// Report classname + datadesc fields (basic introspection without
|
||||
// recursing into nested classes; useful as a starting point).
|
||||
Msg( "ENT_MSG idx=%d class=%s name=%s\n",
|
||||
idx,
|
||||
pEnt->GetClassname(),
|
||||
STRING( pEnt->GetEntityName() ) );
|
||||
datamap_t *map = pEnt->GetDataDescMap();
|
||||
while ( map )
|
||||
{
|
||||
Msg( "ENT_MSG_MAP class=%s field_count=%d\n",
|
||||
map->dataClassName ? map->dataClassName : "?",
|
||||
map->dataNumFields );
|
||||
map = map->baseMap;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_set_angles <pitch> <yaw> [roll] — set player view angles.
|
||||
// Use snap_view (no smoothing — instantaneous like the user mouse-looked).
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_set_angles, "Set player view angles <pitch> <yaw> [roll].", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 3 ) { Msg( "Usage: sg_set_angles <pitch> <yaw> [roll]\n" ); return; }
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "ANGLES_FAIL no_player\n" ); return; }
|
||||
QAngle a( atof( args.Arg(1) ), atof( args.Arg(2) ),
|
||||
args.ArgC() >= 4 ? atof( args.Arg(3) ) : 0.0f );
|
||||
p->SnapEyeAngles( a );
|
||||
Msg( "ANGLES ok pitch=%.1f yaw=%.1f roll=%.1f\n", a.x, a.y, a.z );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_step <forward> <right> [up] — relative-move the player.
|
||||
// Uses the player's current forward/right vectors. Performs a collision-safe
|
||||
// teleport (Source's Teleport sets origin and handles collision internally).
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_step, "Step player by <forward> <right> [up] units relative to view.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 3 ) { Msg( "Usage: sg_step <forward> <right> [up]\n" ); return; }
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "STEP_FAIL no_player\n" ); return; }
|
||||
float fwd = atof( args.Arg(1) );
|
||||
float rgt = atof( args.Arg(2) );
|
||||
float up = args.ArgC() >= 4 ? atof( args.Arg(3) ) : 0.0f;
|
||||
Vector vForward, vRight, vUp;
|
||||
AngleVectors( p->EyeAngles(), &vForward, &vRight, &vUp );
|
||||
Vector newPos = p->GetAbsOrigin() + vForward * fwd + vRight * rgt + vUp * up;
|
||||
p->Teleport( &newPos, NULL, &vec3_origin );
|
||||
Msg( "STEP ok origin=%.1f,%.1f,%.1f\n", newPos.x, newPos.y, newPos.z );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_aim_at <x> <y> <z> — orient player to look at a world position.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_aim_at, "Aim player at world position <x> <y> <z>.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 4 ) { Msg( "Usage: sg_aim_at <x> <y> <z>\n" ); return; }
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "AIM_FAIL no_player\n" ); return; }
|
||||
Vector target( atof( args.Arg(1) ), atof( args.Arg(2) ), atof( args.Arg(3) ) );
|
||||
Vector dir = target - p->EyePosition();
|
||||
QAngle ang;
|
||||
VectorAngles( dir, ang );
|
||||
p->SnapEyeAngles( ang );
|
||||
Msg( "AIM ok target=%.1f,%.1f,%.1f angles=%.1f,%.1f,%.1f\n",
|
||||
target.x, target.y, target.z, ang.x, ang.y, ang.z );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_aim_at_entity <idx> — aim player at the origin of an entity.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_aim_at_entity, "Aim player at entity by index.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_aim_at_entity <idx>\n" ); return; }
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "AIM_FAIL no_player\n" ); return; }
|
||||
int idx = atoi( args.Arg(1) );
|
||||
edict_t *e = engine->PEntityOfEntIndex( idx );
|
||||
CBaseEntity *pTarget = e ? CBaseEntity::Instance( e ) : NULL;
|
||||
if ( !pTarget ) { Msg( "AIM_FAIL no_entity idx=%d\n", idx ); return; }
|
||||
Vector dir = pTarget->WorldSpaceCenter() - p->EyePosition();
|
||||
QAngle ang;
|
||||
VectorAngles( dir, ang );
|
||||
p->SnapEyeAngles( ang );
|
||||
Msg( "AIM ok target_idx=%d class=%s angles=%.1f,%.1f,%.1f\n",
|
||||
idx, pTarget->GetClassname(), ang.x, ang.y, ang.z );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_use_weapon <classname> — switch active weapon.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_use_weapon, "Switch active weapon by classname.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_use_weapon <weapon_class>\n" ); return; }
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "USEWEAPON_FAIL no_player\n" ); return; }
|
||||
const char *pName = args.Arg(1);
|
||||
CBaseCombatWeapon *pWep = p->Weapon_OwnsThisType( pName );
|
||||
if ( !pWep )
|
||||
{
|
||||
// Try to give it first, then switch
|
||||
p->GiveNamedItem( pName );
|
||||
pWep = p->Weapon_OwnsThisType( pName );
|
||||
}
|
||||
if ( !pWep ) { Msg( "USEWEAPON_FAIL not_owned=%s\n", pName ); return; }
|
||||
p->Weapon_Switch( pWep );
|
||||
Msg( "USEWEAPON ok weapon=%s\n", pName );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_press_button <button> [duration_seconds] — schedule a button hold.
|
||||
// button: "jump", "duck", "use", "attack", "attack2", "reload", "walk".
|
||||
// The button stays pressed for the duration, then releases automatically.
|
||||
//-----------------------------------------------------------------------------
|
||||
static const struct { const char *name; int mask; } kButtonMap[] = {
|
||||
{ "attack", IN_ATTACK },
|
||||
{ "attack2", IN_ATTACK2 },
|
||||
{ "jump", IN_JUMP },
|
||||
{ "duck", IN_DUCK },
|
||||
{ "use", IN_USE },
|
||||
{ "reload", IN_RELOAD },
|
||||
{ "walk", IN_WALK },
|
||||
{ "speed", IN_SPEED },
|
||||
{ "forward", IN_FORWARD },
|
||||
{ "back", IN_BACK },
|
||||
{ "left", IN_LEFT },
|
||||
{ "right", IN_RIGHT },
|
||||
{ "moveleft", IN_MOVELEFT },
|
||||
{ "moveright",IN_MOVERIGHT},
|
||||
};
|
||||
static int SG_ButtonMaskByName( const char *pName )
|
||||
{
|
||||
for ( int i = 0; i < ARRAYSIZE( kButtonMap ); ++i )
|
||||
if ( !Q_stricmp( kButtonMap[i].name, pName ) ) return kButtonMap[i].mask;
|
||||
return 0;
|
||||
}
|
||||
|
||||
CON_COMMAND_F( sg_press_button, "Force-hold a player button for N seconds.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 )
|
||||
{
|
||||
Msg( "Usage: sg_press_button <attack|attack2|jump|duck|use|reload|forward|back|moveleft|moveright> [seconds]\n" );
|
||||
return;
|
||||
}
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "BTN_FAIL no_player\n" ); return; }
|
||||
int mask = SG_ButtonMaskByName( args.Arg(1) );
|
||||
if ( !mask ) { Msg( "BTN_FAIL unknown=%s\n", args.Arg(1) ); return; }
|
||||
// Set the button bit immediately. The user-cmd processing will see it
|
||||
// on the next tick. We do NOT clear it — the caller can call again
|
||||
// with the opposite by passing a negative mask, or use sg_release_button.
|
||||
p->m_nButtons |= mask;
|
||||
Msg( "BTN ok button=%s mask=0x%x\n", args.Arg(1), mask );
|
||||
}
|
||||
|
||||
CON_COMMAND_F( sg_release_button, "Release a previously-held button.", FCVAR_CHEAT )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_release_button <name>\n" ); return; }
|
||||
CBasePlayer *p = UTIL_GetCommandClient();
|
||||
if ( !p ) { Msg( "BTN_FAIL no_player\n" ); return; }
|
||||
int mask = SG_ButtonMaskByName( args.Arg(1) );
|
||||
if ( !mask ) { Msg( "BTN_FAIL unknown=%s\n", args.Arg(1) ); return; }
|
||||
p->m_nButtons &= ~mask;
|
||||
Msg( "BTN_RELEASE ok button=%s\n", args.Arg(1) );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_ping — simple liveness probe.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_ping, "Echo PONG with server tick + map name." )
|
||||
{
|
||||
Msg( "PONG tick=%d map=%s players=%d/%d gamerules=%s\n",
|
||||
gpGlobals->tickcount,
|
||||
STRING( gpGlobals->mapname ),
|
||||
SG_CountPlayers(),
|
||||
gpGlobals->maxClients,
|
||||
g_pGameRules ? g_pGameRules->GetGameDescription() : "none" );
|
||||
}
|
||||
409
game/shared/stargate/sg_mcp_debug_client.cpp
Normal file
409
game/shared/stargate/sg_mcp_debug_client.cpp
Normal file
@ -0,0 +1,409 @@
|
||||
//============= Copyright (c) StargateTC Source Team ==========================
|
||||
//
|
||||
// Client-side MCP debug console commands. Companion to sg_mcp_debug.cpp on
|
||||
// the server.
|
||||
//
|
||||
// Client-only — only compiled into client.dll.
|
||||
//
|
||||
//=============================================================================
|
||||
#ifdef CLIENT_DLL
|
||||
|
||||
#include "cbase.h"
|
||||
#include "c_baseentity.h"
|
||||
#include "c_baseplayer.h"
|
||||
#include "vgui_controls/Controls.h"
|
||||
#include "vgui_controls/Panel.h"
|
||||
#include "vgui/ISurface.h"
|
||||
#include "vgui/IPanel.h"
|
||||
#include "vgui/IVGui.h"
|
||||
#include "vgui/IInput.h"
|
||||
#include "vgui/IInputInternal.h"
|
||||
#include "inputsystem/ButtonCode.h"
|
||||
#include "inputsystem/iinputsystem.h"
|
||||
|
||||
#include "tier0/memdbgon.h"
|
||||
|
||||
using namespace vgui;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_ping — client liveness probe.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_ping, "Client: echo CL_PONG + map + local player info." )
|
||||
{
|
||||
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
|
||||
Msg( "CL_PONG map=%s local_player=%s\n",
|
||||
engine->GetLevelName(),
|
||||
pPlayer ? pPlayer->GetPlayerName() : "none" );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_list_entities — dump client-side entity table.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_list_entities, "Client: dump client entity table." )
|
||||
{
|
||||
const char *pFilter = args.ArgC() > 1 ? args.Arg( 1 ) : NULL;
|
||||
int nCount = 0;
|
||||
for ( C_BaseEntity *pEnt = ClientEntityList().FirstBaseEntity();
|
||||
pEnt;
|
||||
pEnt = ClientEntityList().NextBaseEntity( pEnt ) )
|
||||
{
|
||||
const char *pClass = pEnt->GetClassname();
|
||||
if ( pFilter && pFilter[0] && !Q_stristr( pClass, pFilter ) )
|
||||
continue;
|
||||
|
||||
Vector v = pEnt->GetAbsOrigin();
|
||||
Msg( "CL_ENT idx=%d class=%s origin=%.1f,%.1f,%.1f visible=%d\n",
|
||||
pEnt->entindex(),
|
||||
pClass,
|
||||
v.x, v.y, v.z,
|
||||
pEnt->ShouldDraw() ? 1 : 0 );
|
||||
++nCount;
|
||||
}
|
||||
Msg( "CL_ENT_COUNT %d\n", nCount );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_player — local player state.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_player, "Client: dump local player state." )
|
||||
{
|
||||
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
|
||||
if ( !pPlayer )
|
||||
{
|
||||
Msg( "CL_PLAYER none\n" );
|
||||
return;
|
||||
}
|
||||
Vector v = pPlayer->GetAbsOrigin();
|
||||
QAngle a = pPlayer->GetAbsAngles();
|
||||
C_BaseCombatWeapon *pWep = pPlayer->GetActiveWeapon();
|
||||
Msg( "CL_PLAYER name=%s team=%d health=%d origin=%.1f,%.1f,%.1f angles=%.1f,%.1f,%.1f weapon=%s\n",
|
||||
pPlayer->GetPlayerName(),
|
||||
pPlayer->GetTeamNumber(),
|
||||
pPlayer->GetHealth(),
|
||||
v.x, v.y, v.z,
|
||||
a.x, a.y, a.z,
|
||||
pWep ? pWep->GetClassname() : "-" );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_check_materials [filter]
|
||||
// Walk the material system and flag missing/error materials.
|
||||
// Helps diagnose the pink-checker placeholder problem.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_check_materials, "Client: scan materials and report errors." )
|
||||
{
|
||||
if ( !materials )
|
||||
{
|
||||
Msg( "CL_MAT_FAIL no material system\n" );
|
||||
return;
|
||||
}
|
||||
|
||||
const char *pFilter = args.ArgC() > 1 ? args.Arg( 1 ) : NULL;
|
||||
|
||||
MaterialHandle_t h = materials->FirstMaterial();
|
||||
int nTotal = 0;
|
||||
int nErrors = 0;
|
||||
while ( h != materials->InvalidMaterial() )
|
||||
{
|
||||
IMaterial *pMat = materials->GetMaterial( h );
|
||||
if ( pMat )
|
||||
{
|
||||
const char *pName = pMat->GetName();
|
||||
bool bIsError = pMat->IsErrorMaterial();
|
||||
if ( pFilter && pFilter[0] && !Q_stristr( pName, pFilter ) )
|
||||
{
|
||||
h = materials->NextMaterial( h );
|
||||
continue;
|
||||
}
|
||||
if ( bIsError )
|
||||
{
|
||||
Msg( "CL_MAT_ERR name=%s\n", pName );
|
||||
++nErrors;
|
||||
}
|
||||
else if ( pFilter && pFilter[0] )
|
||||
{
|
||||
Msg( "CL_MAT_OK name=%s shader=%s\n",
|
||||
pName, pMat->GetShaderName() ? pMat->GetShaderName() : "-" );
|
||||
}
|
||||
++nTotal;
|
||||
}
|
||||
h = materials->NextMaterial( h );
|
||||
}
|
||||
Msg( "CL_MAT_SUMMARY total=%d errors=%d\n", nTotal, nErrors );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_net — report client network state.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_net, "Client: dump network connection state." )
|
||||
{
|
||||
if ( !engine )
|
||||
{
|
||||
Msg( "CL_NET_FAIL no engine\n" );
|
||||
return;
|
||||
}
|
||||
Msg( "CL_NET in_game=%d connected=%d level=%s server=%s\n",
|
||||
engine->IsInGame() ? 1 : 0,
|
||||
engine->IsConnected() ? 1 : 0,
|
||||
engine->GetLevelName(),
|
||||
engine->GetGameDirectory() );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_debugbreak — controlled DebugBreak on client side.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND_F( sg_cl_debugbreak, "Client: trigger DebugBreak() for verifying attached debugger.", FCVAR_CHEAT )
|
||||
{
|
||||
Msg( "Client: about to DebugBreak — if a debugger is attached it will catch this.\n" );
|
||||
DebuggerBreak();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_vgui_tree [filter] — recursively walk vgui panel tree.
|
||||
// Emits one line per panel:
|
||||
// VGUI depth=N vpanel=PTR name=<name> class=<class> pos=x,y size=w,h
|
||||
// visible=0/1 enabled=0/1 alpha=A keyboardinput=0/1 mouseinput=0/1
|
||||
//-----------------------------------------------------------------------------
|
||||
static void SG_DumpPanelRecursive( VPANEL vp, int depth, const char *pFilter, int &nEmitted )
|
||||
{
|
||||
if ( !vp ) return;
|
||||
IPanel *ip = ipanel();
|
||||
if ( !ip ) return;
|
||||
|
||||
const char *pName = ip->GetName( vp );
|
||||
const char *pClass = ip->GetClassName( vp );
|
||||
if ( !pName ) pName = "-";
|
||||
if ( !pClass ) pClass = "-";
|
||||
|
||||
if ( !pFilter || !pFilter[0] ||
|
||||
Q_stristr( pName, pFilter ) || Q_stristr( pClass, pFilter ) )
|
||||
{
|
||||
int x = 0, y = 0, w = 0, h = 0;
|
||||
ip->GetAbsPos( vp, x, y );
|
||||
ip->GetSize( vp, w, h );
|
||||
Msg( "VGUI depth=%d vpanel=%p name=%s class=%s pos=%d,%d size=%d,%d "
|
||||
"visible=%d enabled=%d keyboardinput=%d mouseinput=%d\n",
|
||||
depth, (void*)vp, pName, pClass, x, y, w, h,
|
||||
ip->IsVisible( vp ) ? 1 : 0,
|
||||
ip->IsEnabled( vp ) ? 1 : 0,
|
||||
ip->IsKeyBoardInputEnabled( vp ) ? 1 : 0,
|
||||
ip->IsMouseInputEnabled( vp ) ? 1 : 0 );
|
||||
++nEmitted;
|
||||
}
|
||||
|
||||
int nChildren = ip->GetChildCount( vp );
|
||||
for ( int i = 0; i < nChildren; ++i )
|
||||
{
|
||||
VPANEL child = ip->GetChild( vp, i );
|
||||
SG_DumpPanelRecursive( child, depth + 1, pFilter, nEmitted );
|
||||
}
|
||||
}
|
||||
|
||||
CON_COMMAND( sg_cl_vgui_tree, "Client: dump the VGUI panel tree (optional substring filter)." )
|
||||
{
|
||||
const char *pFilter = args.ArgC() > 1 ? args.Arg( 1 ) : NULL;
|
||||
if ( !vgui::ivgui() || !ipanel() || !surface() )
|
||||
{
|
||||
Msg( "VGUI_FAIL no_vgui\n" );
|
||||
return;
|
||||
}
|
||||
// GetEmbeddedPanel() is the top-level VGUI root that contains the HUD,
|
||||
// menus, console, etc.
|
||||
VPANEL root = surface()->GetEmbeddedPanel();
|
||||
if ( !root )
|
||||
{
|
||||
Msg( "VGUI_FAIL no_root\n" );
|
||||
return;
|
||||
}
|
||||
int nEmitted = 0;
|
||||
SG_DumpPanelRecursive( root, 0, pFilter, nEmitted );
|
||||
Msg( "VGUI_COUNT %d\n", nEmitted );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_vgui_visible — list ONLY visible panels at the top of the tree
|
||||
// (useful when investigating "what's on screen right now").
|
||||
//-----------------------------------------------------------------------------
|
||||
static void SG_DumpVisibleRecursive( VPANEL vp, int depth, int &nEmitted )
|
||||
{
|
||||
IPanel *ip = ipanel();
|
||||
if ( !vp || !ip ) return;
|
||||
if ( !ip->IsVisible( vp ) ) return; // prune invisible subtrees
|
||||
|
||||
int x = 0, y = 0, w = 0, h = 0;
|
||||
ip->GetAbsPos( vp, x, y );
|
||||
ip->GetSize( vp, w, h );
|
||||
const char *pName = ip->GetName( vp );
|
||||
const char *pClass = ip->GetClassName( vp );
|
||||
Msg( "VGUI_VIS depth=%d vpanel=%p name=%s class=%s pos=%d,%d size=%d,%d\n",
|
||||
depth, (void*)vp,
|
||||
pName ? pName : "-",
|
||||
pClass ? pClass : "-",
|
||||
x, y, w, h );
|
||||
++nEmitted;
|
||||
|
||||
int n = ip->GetChildCount( vp );
|
||||
for ( int i = 0; i < n; ++i )
|
||||
{
|
||||
SG_DumpVisibleRecursive( ip->GetChild( vp, i ), depth + 1, nEmitted );
|
||||
}
|
||||
}
|
||||
|
||||
CON_COMMAND( sg_cl_vgui_visible, "Client: dump only currently-visible panels." )
|
||||
{
|
||||
if ( !surface() ) { Msg( "VGUI_FAIL no_surface\n" ); return; }
|
||||
VPANEL root = surface()->GetEmbeddedPanel();
|
||||
if ( !root ) { Msg( "VGUI_FAIL no_root\n" ); return; }
|
||||
int n = 0;
|
||||
SG_DumpVisibleRecursive( root, 0, n );
|
||||
Msg( "VGUI_VIS_COUNT %d\n", n );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_set_cursor <x> <y> — move VGUI cursor to absolute screen pixel pos.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_set_cursor, "Client: move VGUI cursor to <x> <y>." )
|
||||
{
|
||||
if ( args.ArgC() < 3 ) { Msg( "Usage: sg_cl_set_cursor <x> <y>\n" ); return; }
|
||||
int x = atoi( args.Arg(1) ), y = atoi( args.Arg(2) );
|
||||
if ( !vgui::input() ) { Msg( "CURSOR_FAIL no_input\n" ); return; }
|
||||
vgui::input()->SetCursorPos( x, y );
|
||||
Msg( "CURSOR ok x=%d y=%d\n", x, y );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_click <x> <y> [button] — synthesize a mouse click at the given coords.
|
||||
// button: "left" (default), "right", "middle".
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_click, "Client: VGUI mouse click at <x> <y> [left|right|middle]." )
|
||||
{
|
||||
if ( args.ArgC() < 3 ) { Msg( "Usage: sg_cl_click <x> <y> [left|right|middle]\n" ); return; }
|
||||
IInputInternal *pInputInternal = (IInputInternal *)vgui::input();
|
||||
if ( !pInputInternal ) { Msg( "CLICK_FAIL no_input\n" ); return; }
|
||||
int x = atoi( args.Arg(1) ), y = atoi( args.Arg(2) );
|
||||
const char *btn = args.ArgC() >= 4 ? args.Arg(3) : "left";
|
||||
MouseCode code = MOUSE_LEFT;
|
||||
if ( !Q_stricmp( btn, "right" ) ) code = MOUSE_RIGHT;
|
||||
else if ( !Q_stricmp( btn, "middle" ) ) code = MOUSE_MIDDLE;
|
||||
|
||||
pInputInternal->SetCursorPos( x, y );
|
||||
pInputInternal->InternalCursorMoved( x, y );
|
||||
pInputInternal->UpdateMouseFocus( x, y );
|
||||
pInputInternal->InternalMousePressed( code );
|
||||
pInputInternal->InternalMouseReleased( code );
|
||||
Msg( "CLICK ok x=%d y=%d button=%s\n", x, y, btn );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_double_click <x> <y> — double-click at given coords (left button).
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_double_click, "Client: VGUI double-click at <x> <y>." )
|
||||
{
|
||||
if ( args.ArgC() < 3 ) { Msg( "Usage: sg_cl_double_click <x> <y>\n" ); return; }
|
||||
IInputInternal *pInputInternal = (IInputInternal *)vgui::input();
|
||||
if ( !pInputInternal ) { Msg( "DBLCLICK_FAIL no_input\n" ); return; }
|
||||
int x = atoi( args.Arg(1) ), y = atoi( args.Arg(2) );
|
||||
pInputInternal->SetCursorPos( x, y );
|
||||
pInputInternal->InternalCursorMoved( x, y );
|
||||
pInputInternal->UpdateMouseFocus( x, y );
|
||||
pInputInternal->InternalMouseDoublePressed( MOUSE_LEFT );
|
||||
pInputInternal->InternalMouseReleased( MOUSE_LEFT );
|
||||
Msg( "DBLCLICK ok x=%d y=%d\n", x, y );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_keypress <keyname> — fire a VGUI key code press.
|
||||
// Examples: KEY_ENTER, KEY_ESCAPE, KEY_TAB, KEY_F1, KEY_A.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_keypress, "Client: synthesize VGUI key press by KEY_* name." )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_cl_keypress <KEY_NAME>\n" ); return; }
|
||||
IInputInternal *pInputInternal = (IInputInternal *)vgui::input();
|
||||
if ( !pInputInternal || !g_pInputSystem ) { Msg( "KEY_FAIL no_input\n" ); return; }
|
||||
ButtonCode_t bc = g_pInputSystem->StringToButtonCode( args.Arg(1) );
|
||||
KeyCode k = (KeyCode)bc; // ButtonCode and KeyCode share the same enum space
|
||||
if ( bc == BUTTON_CODE_INVALID ) { Msg( "KEY_FAIL unknown=%s\n", args.Arg(1) ); return; }
|
||||
pInputInternal->InternalKeyCodePressed( k );
|
||||
pInputInternal->InternalKeyCodeTyped( k );
|
||||
pInputInternal->InternalKeyCodeReleased( k );
|
||||
Msg( "KEYPRESS ok key=%s code=%d\n", args.Arg(1), (int)k );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_type <text> — synthesize character input for a string (supports _).
|
||||
// Each character is sent via InternalKeyTyped, which writes into the focused
|
||||
// text-entry panel as if the user had typed it.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_type, "Client: type a string into the focused VGUI text panel." )
|
||||
{
|
||||
if ( args.ArgC() < 2 ) { Msg( "Usage: sg_cl_type <text...>\n" ); return; }
|
||||
IInputInternal *pInputInternal = (IInputInternal *)vgui::input();
|
||||
if ( !pInputInternal ) { Msg( "TYPE_FAIL no_input\n" ); return; }
|
||||
// ArgS returns everything after argv[0], with quoting intact.
|
||||
const char *pText = args.ArgS();
|
||||
int nChars = 0;
|
||||
for ( const char *p = pText; *p; ++p, ++nChars )
|
||||
{
|
||||
pInputInternal->InternalKeyTyped( (wchar_t)(unsigned char)*p );
|
||||
}
|
||||
Msg( "TYPE ok chars=%d text=%s\n", nChars, pText );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_screen_info — overall screen + cursor state.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_screen_info, "Client: screen size + cursor position." )
|
||||
{
|
||||
int sw = 0, sh = 0;
|
||||
if ( surface() ) surface()->GetScreenSize( sw, sh );
|
||||
int mx = 0, my = 0;
|
||||
if ( vgui::input() ) vgui::input()->GetCursorPos( mx, my );
|
||||
Msg( "CL_SCREEN width=%d height=%d cursor=%d,%d\n", sw, sh, mx, my );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_list_missing_models — walk client entity table, log any with NULL model.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_list_missing_models, "Client: report entities whose model failed to load." )
|
||||
{
|
||||
int nMissing = 0;
|
||||
int nChecked = 0;
|
||||
for ( C_BaseEntity *pEnt = ClientEntityList().FirstBaseEntity();
|
||||
pEnt;
|
||||
pEnt = ClientEntityList().NextBaseEntity( pEnt ) )
|
||||
{
|
||||
++nChecked;
|
||||
const model_t *pModel = pEnt->GetModel();
|
||||
const char *pModelName = STRING( pEnt->GetModelName() );
|
||||
if ( pModelName && pModelName[0] && !pModel )
|
||||
{
|
||||
Msg( "CL_MISSING_MODEL idx=%d class=%s model=%s\n",
|
||||
pEnt->entindex(), pEnt->GetClassname(), pModelName );
|
||||
++nMissing;
|
||||
}
|
||||
}
|
||||
Msg( "CL_MISSING_MODEL_SUMMARY checked=%d missing=%d\n", nChecked, nMissing );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// sg_cl_dump_render_info — log local player's render-side debugging data.
|
||||
//-----------------------------------------------------------------------------
|
||||
CON_COMMAND( sg_cl_dump_render_info, "Client: dump local player render info." )
|
||||
{
|
||||
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
|
||||
if ( !pPlayer )
|
||||
{
|
||||
Msg( "CL_RENDER_FAIL no_player\n" );
|
||||
return;
|
||||
}
|
||||
Vector eye = pPlayer->EyePosition();
|
||||
QAngle eyeAng = pPlayer->EyeAngles();
|
||||
Msg( "CL_RENDER eye_pos=%.1f,%.1f,%.1f eye_ang=%.1f,%.1f,%.1f "
|
||||
"view_offset=%.1f viewmodel=%d\n",
|
||||
eye.x, eye.y, eye.z,
|
||||
eyeAng.x, eyeAng.y, eyeAng.z,
|
||||
pPlayer->GetViewOffset().z,
|
||||
pPlayer->GetViewModel() ? 1 : 0 );
|
||||
}
|
||||
|
||||
#endif // CLIENT_DLL
|
||||
188
opencode.md
Normal file
188
opencode.md
Normal file
@ -0,0 +1,188 @@
|
||||
# OpenCode Loop Prompt
|
||||
|
||||
You are working in a repo whose goal is to build a Rust Source-compatible runtime alongside or from this Source Engine codebase.
|
||||
|
||||
Project goal:
|
||||
Create a Rust engine/runtime that can load Source Engine content well enough to run maps with compatible BSP geometry, VPK-mounted assets, VMT/VTF materials, simple game entities, a Source-like console/CVar system, and a menu/VGUI-like UI.
|
||||
|
||||
You must work incrementally. Do not attempt broad rewrites. Do not invent large systems unless the current task requires them.
|
||||
|
||||
## State Files
|
||||
|
||||
Use these files as persistent memory between runs:
|
||||
|
||||
- `PROJECT_STATE.md`: current status, architecture decisions, known limitations.
|
||||
- `ROADMAP.md`: ordered milestones and task backlog.
|
||||
- `COMPATIBILITY.md`: supported Source formats/entities/features.
|
||||
- `TASKS.md`: active, queued, blocked, and completed tasks.
|
||||
- `DEVLOG.md`: append-only progress log.
|
||||
- `DECISIONS.md`: append-only architectural decisions.
|
||||
- `TESTING.md`: how to run tests and what currently passes/fails.
|
||||
|
||||
## Before Doing Any Work
|
||||
|
||||
1. Read all state files if they exist.
|
||||
2. Inspect the repo structure.
|
||||
3. Identify the next smallest unblocked task from `TASKS.md` or `ROADMAP.md`.
|
||||
4. If no state files exist, create them with an initial plan.
|
||||
|
||||
## Work Loop
|
||||
|
||||
1. Pick exactly one small task.
|
||||
2. State the selected task in `DEVLOG.md`.
|
||||
3. Implement only that task.
|
||||
4. Add or update tests/fixtures where practical.
|
||||
5. Run formatting and relevant tests.
|
||||
6. Update `PROJECT_STATE.md`, `TASKS.md`, `COMPATIBILITY.md`, and `TESTING.md`.
|
||||
7. Append a short `DEVLOG.md` entry with:
|
||||
- task completed
|
||||
- files changed
|
||||
- commands run
|
||||
- tests passing/failing
|
||||
- next recommended task
|
||||
8. Stop.
|
||||
|
||||
## Engineering Rules
|
||||
|
||||
- Prefer small, testable crates/modules.
|
||||
- Prefer parsing/dump tools before runtime features.
|
||||
- Use Rust 2021 or newer.
|
||||
- Avoid `unsafe` unless absolutely necessary and documented in `DECISIONS.md`.
|
||||
- Do not fake compatibility. If something is placeholder, mark it clearly in `COMPATIBILITY.md`.
|
||||
- Do not implement unrelated features.
|
||||
- Do not delete user work.
|
||||
- Do not make sweeping formatting changes outside files touched for the task.
|
||||
- Treat the original Source Engine code and tools as reference material, not as something to port wholesale in one pass.
|
||||
|
||||
## Suggested Architecture
|
||||
|
||||
- `crates/source_math`
|
||||
- `crates/source_bsp`
|
||||
- `crates/source_vpk`
|
||||
- `crates/source_vmt`
|
||||
- `crates/source_vtf`
|
||||
- `crates/source_entity`
|
||||
- `crates/source_render`
|
||||
- `crates/source_vgui`
|
||||
- `crates/source_console`
|
||||
- `crates/source_app`
|
||||
|
||||
## Preferred Libraries
|
||||
|
||||
- `glam` for math
|
||||
- `wgpu` for rendering
|
||||
- `winit` for window/input
|
||||
- `egui` for early menu/debug UI
|
||||
- `serde` for structured dumps/tests
|
||||
- `clap` for CLI tools
|
||||
- `tracing` for logging
|
||||
- `insta` or JSON fixtures for snapshot-style parser tests
|
||||
|
||||
## Initial Milestone Order
|
||||
|
||||
1. Workspace/crate layout.
|
||||
2. BSP header and lump directory parser.
|
||||
3. BSP entity lump parser.
|
||||
4. BSP planes, vertices, edges, surfedges, faces.
|
||||
5. Basic map dump CLI.
|
||||
6. VPK filesystem mounting.
|
||||
7. VMT KeyValues parser.
|
||||
8. VTF loading or placeholder texture path.
|
||||
9. Render BSP world geometry.
|
||||
10. Free camera.
|
||||
11. `info_player_start` and `worldspawn` support.
|
||||
12. Console and CVar registry.
|
||||
13. Basic Source-like menu.
|
||||
14. Player controller with BSP collision.
|
||||
15. `trigger_once` / `trigger_multiple`.
|
||||
16. Entity I/O outputs.
|
||||
17. `func_button` / `func_door`.
|
||||
18. HUD basics.
|
||||
|
||||
## Task Size Rule
|
||||
|
||||
A task should usually be completable in under 1-2 hours and should leave the repo in a runnable or testable state.
|
||||
|
||||
If uncertain:
|
||||
|
||||
- Prefer adding a dump/inspection tool.
|
||||
- Prefer parser tests over runtime guesses.
|
||||
- Prefer documenting a limitation over pretending it works.
|
||||
|
||||
## Starter `TASKS.md`
|
||||
|
||||
If `TASKS.md` does not exist, create this:
|
||||
|
||||
```md
|
||||
# Tasks
|
||||
|
||||
## Active
|
||||
- [ ] Create Rust workspace layout for Source-compatible runtime.
|
||||
|
||||
## Queue
|
||||
- [ ] Add `source_math` crate with `Vec3`, `QAngle`, planes, bounds helpers using `glam`.
|
||||
- [ ] Add `source_bsp` crate and parse BSP header.
|
||||
- [ ] Parse BSP lump directory.
|
||||
- [ ] Add `bsp_dump` CLI that prints header/lump metadata as JSON.
|
||||
- [ ] Parse BSP entity lump as KeyValues-style entities.
|
||||
- [ ] Add entity dump command.
|
||||
- [ ] Parse planes lump.
|
||||
- [ ] Parse vertices lump.
|
||||
- [ ] Parse edges and surfedges.
|
||||
- [ ] Parse faces.
|
||||
- [ ] Resolve texinfo and texdata.
|
||||
- [ ] Add placeholder renderer with `wgpu`.
|
||||
- [ ] Render untextured BSP world geometry.
|
||||
- [ ] Add VMT parser.
|
||||
- [ ] Add VTF texture path/loading placeholder.
|
||||
- [ ] Render textured BSP faces.
|
||||
- [ ] Add free camera.
|
||||
- [ ] Add console and CVar registry.
|
||||
- [ ] Add Source-like main menu.
|
||||
- [ ] Implement `worldspawn`.
|
||||
- [ ] Implement `info_player_start`.
|
||||
- [ ] Implement basic player movement.
|
||||
- [ ] Implement BSP collision traces.
|
||||
- [ ] Implement `trigger_once`.
|
||||
- [ ] Implement Source-style entity output dispatch.
|
||||
|
||||
## Blocked
|
||||
|
||||
## Done
|
||||
```
|
||||
|
||||
## Starter `PROJECT_STATE.md`
|
||||
|
||||
If `PROJECT_STATE.md` does not exist, create this:
|
||||
|
||||
```md
|
||||
# Project State
|
||||
|
||||
## Goal
|
||||
Build a Rust Source-compatible runtime capable of loading and running Source maps with compatible assets, basic entities, menu UI, console/CVars, and game-layer behavior.
|
||||
|
||||
## Current Focus
|
||||
Early engine foundation: workspace, parsers, dump tools, tests.
|
||||
|
||||
## Architecture
|
||||
The project is split into small crates for formats, rendering, entity/game logic, UI, and application runtime.
|
||||
|
||||
## Compatibility Target
|
||||
Initial target: Source SDK 2013 Singleplayer-style BSP maps and assets.
|
||||
|
||||
## Non-Goals For Now
|
||||
- Full Source multiplayer networking
|
||||
- Full NPC AI
|
||||
- Full Source shader parity
|
||||
- Save/load
|
||||
- Demo playback
|
||||
- Complete VGUI port
|
||||
- Direct C++ game DLL compatibility
|
||||
|
||||
## Current Limitations
|
||||
Everything is initially incomplete until implemented and tested.
|
||||
```
|
||||
|
||||
## Agent Instruction
|
||||
|
||||
Now begin the loop.
|
||||
Loading…
Reference in New Issue
Block a user