A remote debugger for SourcePawn plugins running on a live game server (CS:S, TF2, L4D2 -- any Source 1 game running SourceMod). Debug directly from VS Code — set breakpoints, inspect variables, step through code, and use the Debug Console REPL without modifying your plugin code or leaving the editor.
Key Insight: The debugger works like a traditional C++ debugger — you see source code, see where execution stops, and can inspect the full call stack and variable state at each frame.
This doc covers what the debugger does, how to use it, and how to build it. For detailed usage instructions, see usage.md.
- Install the extension from the VS Code Marketplace (search: "SourcePawn Debugger")
- Configure a launch config in
.vscode/launch.json:{ "version": "0.2.0", "configurations": [ { "type": "sp-debugger", "request": "launch", "name": "Debug SourcePawn Plugin", "host": "your.server.ip", "port": 8080, "plugin": "${file}" } ] } - Deploy the C++ extension to your server (copy
.sofiles toaddons/sourcemod/extensions/) - Set breakpoints by clicking the gutter in the editor
- Start debugging with F5 and interact with the server
See usage.md for detailed instructions on all debugger features.
This project consists of two parts:
- C++ Extension (
sp-console-debugger/) — A SourceMod extension that runs on the game server and implements the DAP (Debug Adapter Protocol) server over TCP - VS Code Extension (
vscode/) — A TypeScript adapter that translates VS Code DAP requests to the protocol and manages the socket connection
See architecture.md for the full technical design.
The SourcePawn VM is single-threaded and NOT thread-safe. When a breakpoint fires, the entire game server freezes. This is not a bug—it's inherent to running a debugger in-process on the main game thread. The non-freezing alternative is the snapshot logpoint feature ({@all}, {@locals}, {@args}), which captures variable snapshots and continues without pausing.
For production servers, use snapshot logpoints. For development/testing, use normal breakpoints with a dedicated test server.
| Feature | Status | Notes |
|---|---|---|
| Breakpoints | ✓ Full | Normal, conditional, hit-count, logpoints, snapshot logpoints |
| Call Stack | ✓ Full | Multi-frame inspection with clickable source links |
| Variables | ✓ Full | Locals, arguments, globals, array expansion, typed rendering |
| Stepping | ✓ Full | Step Over/Into/Out + async pause |
| Evaluation | ✓ Full | Hover, watch expressions, Debug Console REPL |
| Variable Edit | ✓ Full | Inline set (including single array elements) |
| Data Breakpoints | ✓ Full | Watchpoints on global/static variables |
| Memory Inspection | ✓ Full | Hex viewer via readMemory |
| Debug Console REPL | ✓ Full | Type commands (bt, p, x, break, etc.) while paused |
| Function Profiler | ✓ Full | Non-pausing capture of the debugged plugin → flame chart with per-line heat, per-call min/p95/max, and a Perfetto timeline export (needs the patched VM) |
Developer's Machine (VS Code)
┌──────────────────────────────────────┐
│ VS Code Extension (TypeScript) │
│ ├─ TcpConnection (socket framing) │
│ ├─ DebugSessionManager (state) │
│ └─ debugAdapter.ts (DAP translator) │
└──────────────────┬───────────────────┘
│ DAP over TCP
│ Content-Length framing
│ 10.0.0.5:8080
▼
Game Server Network
│
▼
┌──────────────────────────────────────┐
│ Game Server (CS:S, TF2, ...) │
│ ├─ Main Thread (SourcePawn VM) │
│ │ └─ Plugin executes │
│ │ OnDebugBreak() → parks │
│ │ VM thread in condition │
│ │ variable │
│ └─ TCP Thread │
│ └─ DAPHandlers (process cmds) │
│ RequestRegistry (match seq) │
│ VariableHandleRegistry │
└──────────────────────────────────────┘
Key Insight: The C++ extension listens on a TCP port (default 8080) and speaks the exact DAP wire protocol that VS Code expects. The VS Code extension just forwards requests and collects responses. No custom protocol translation needed.
-
usage.md — How to use every debugger feature (for plugin developers)
- Installation and setup
- Breakpoints, stepping, variable inspection
- Snapshot logpoints (the non-freezing way to observe a live server)
- Debug Console REPL
- Troubleshooting common issues
-
architecture.md — Deep technical design (for maintainers)
- Component architecture and responsibilities
- Threading model and synchronization
- DAP command flow and state management
- Symbol resolution and variable rendering
- Handle registry (scope encoding)
-
protocol.md — Wire protocol and framing (for protocol maintainers)
- Content-Length framing (byte-accurate, UTF-8 handling)
- DAP message format and validation
- Request/response correlation (seq/request_seq)
- Event ordering constraints (continued before SetRunmode)
- TCP connection lifecycle
-
troubleshooting.md — Known issues and solutions
- The freeze model (why it happens, how to avoid it)
- Transport anomalies (buffer re-sync, parse errors, orphan responses)
- Path mapping and source discovery
- Timeout configurations
- Reconnection behavior
-
Testing — CI regression tests and dev tools
- Regression test suite and mock environment setup
- Unit suite (
bun test) + DAP integration test + headless dev tools - Mock mode and remote mode (real server)
Built from the repo root. The sp-console-debugger/ source tree is not
committed — it is reconstructed from the pinned upstream commit plus our patch
(the "Fetch Debugger Source" VS Code task does this automatically; see
updating-upstream.md for the manual commands).
# 1. Reconstruct sp-console-debugger/ if missing (fetch task or manual commands)
# 2. Configure + build:
.venv/bin/python configure.py --enable-optimize --targets=x86,x86_64 \
--sm-path=../sourcemod
.venv/bin/ambuild objdir
# Output: objdir/package/addons/sourcemod/extensions/{,x64/}sp-debugger.ext.soRequires:
- SourceMod source tree (with the
debug_api_symbolsSourcePawn branch) at../sourcemod - Python venv with ambuild at
.venv/
cd vscode
bun run compile
# Output: out/Requires: bun at ~/.bun/bin/bun
-
Copy the
.sofiles:scp objdir/package/addons/sourcemod/extensions/sp-debugger.ext.so \ user@server:/home/srcds/csgo/addons/sourcemod/extensions/ scp objdir/package/addons/sourcemod/extensions/x64/sp-debugger.ext.so \ user@server:/home/srcds/csgo/addons/sourcemod/extensions/x64/The config file is not copied manually — the extension writes a default
addons/sourcemod/configs/console-debugger.cfgon first load. Edit it later only if you need a non-default port or verbose logging. -
Full game-server restart required (critical):
# Option A: Stop and start the process pkill -f srcds # or your server stop command # Wait for graceful shutdown, then start the server again # Option B: In-game command (if available) _restart
Do NOT use
sm exts loadorsm exts reload— these load the extension "late" (after plugins are already running), which skips the critical VM initialization step. Use only after a full process restart. -
Verify it's loaded:
sm exts listShould show
[01] SourceMod Console Debugger [RUNNING]
The C++ extension writes a self-documenting addons/sourcemod/configs/console-debugger.cfg
the first time it loads and reads it at every startup thereafter. An existing
file is never overwritten, so your edits are preserved across updates:
"ConsoleDebugger"
{
"bind" "127.0.0.1"
"token" ""
"max_clients" "4"
"port" "8080"
"debug_log" "0"
}| Key | Default | Meaning |
|---|---|---|
bind |
127.0.0.1 |
Address the DAP server listens on. |
token |
(empty) | Shared secret the debugger client must present. |
max_clients |
4 |
Concurrent debugger connections allowed (1–64). |
port |
8080 |
TCP port (1–65535). |
debug_log |
0 |
Verbose transport logging. |
Deleting the file regenerates the documented default on the next load. The VS Code launch config must use the same port, token and server address.
A debug session is not a read-only view: over that socket a client can read and write the debugged plugin's memory, and a breakpoint holds the game thread until the client resumes it. An open, unauthenticated debug port is therefore equivalent to handing out control of the plugins.
So the defaults are deliberately closed:
bindis127.0.0.1, reachable only from the machine itself.- Exposing it further requires a token: with a non-loopback
bindand an emptytoken, the extension logs an error and does not open the port at all. - With a token set, every request except
initialize/disconnectis refused until the client presents it (inlaunch/attach/initialize), and a client that never authenticates is dropped after 15 seconds.
Debugging a remote server is best done over an SSH tunnel, which keeps the port closed to everyone else and needs no token:
ssh -L 8080:127.0.0.1:8080 user@gameserver
# then point launch.json at "host": "127.0.0.1"If you would rather expose the port directly, set both keys and put the same
secret in launch.json:
"bind" "0.0.0.0"
"token" "a-long-random-string"GNU General Public License v3.0. See LICENSE for details.
Original extension by Peace-Maker. Rewritten for DAP (Debug Adapter Protocol) support.
Questions? Read the troubleshooting guide or the architecture docs. This is a complex system with multiple threads, TCP framing, and SourcePawn VM internals — some knowledge of each layer helps.
{ "type": "sp-debugger", "request": "launch", "host": "your.server.ip", "port": 8080, "token": "a-long-random-string", "plugin": "${file}" }