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:
rowea 2026-05-30 20:56:18 +01:00
parent 44cb9a4837
commit d2e77350e6
6 changed files with 1138 additions and 0 deletions

1
.gitignore vendored
View File

@ -37,3 +37,4 @@ waf3*/
.vscode/
.depproj/
source-engine.sln
/source-engine-runtime/

View File

@ -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"
}
}

View File

@ -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"

View 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" );
}

View 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
View 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.