DebugTools Walkthrough¶
Difficulty: Advanced Concepts: HTTP server, background threads, in-game console, virtual input, P/Invoke window management
DebugTools is the most infrastructure-heavy mod in the framework. It provides a 70+ endpoint HTTP debug server, runtime introspection tools, an in-game console, virtual input injection, inventory/equipment/world control, and window management — enabling full headless/remote game control and automated testing.
What It Does¶
- HTTP Debug Server: REST API on
localhost:7878with 70+ endpoints for game state queries, inventory/equipment control, world manipulation, NPC management, and runtime introspection - Runtime Introspection: Browse any type via reflection, read/write fields, evaluate property paths, trace method calls, and watch field changes — all at runtime via HTTP
- In-Game Console (Ctrl+`): Command system with history, tab completion, and output scrolling
- Virtual Input: Programmatic game actions (movement, attacks, inventory) via trigger injection
- Inventory & Equipment Control: Set any inventory slot, equip armor/accessories/pets, select hotbar, modify chest contents
- World Manipulation: Place/fill tiles and liquids, toggle hardmode/blood moon/events, set boss progression, trigger invasions, teleport
- NPC Control: List, spawn, kill, and reposition NPCs
- Snapshots: Save and restore game state for repeatable testing
- Window Management: Hide/show game and console windows for headless operation
- System Health: Mod health checks, Harmony patch audit, EventLog, FPS/memory diagnostics, network stats
Architecture¶
DebugTools is a single mod that combines functionality from several subsystems. Each subsystem initializes independently and can fail without crashing the others:
Mod.Initialize()
├── WindowManager.Initialize() ← P/Invoke window handles
├── DebugHttpServer.Start() ← Background listener thread (70+ endpoints)
├── RuntimeIntrospection.Init() ← Reflection browser, tracing, watching
├── MainThreadDispatcher.Init() ← Thread-safe game-thread execution
├── ConsoleUI.Initialize() ← In-game console with Ctrl+` keybind
├── VirtualInputManager.Init() ← Trigger injection state
└── VirtualInputPatches.Apply() ← Harmony patches for input pipeline
Key Concepts¶
1. Config-Driven Feature Toggles¶
The mod uses a class-based config with three boolean options (saved to core/configs/debug-tools.client.json):
public class DebugToolsConfig : ModConfig
{
[Client] public bool Enabled { get; set; } = true;
[Client] public bool HttpServer { get; set; } = true;
[Client] public bool StartHidden { get; set; } = false;
}
In Initialize(), the mod checks these before starting subsystems:
public void Initialize(ModContext context)
{
bool httpEnabled = context.Config.Get("httpServer", true);
bool startHidden = context.Config.Get("startHidden", false);
WindowManager.Initialize(_log);
if (httpEnabled)
DebugHttpServer.Start(_log);
ConsoleUI.Initialize(context, _log);
}
This lets users disable the HTTP server without disabling the console, or start hidden for automated testing.
2. Background Thread HTTP Server¶
The HTTP server runs on a background thread using HttpListener, keeping the game thread free:
public static void Start(ILogger log)
{
_listener = new HttpListener();
_listener.Prefixes.Add("http://localhost:7878/");
_listener.Start();
_listenerThread = new Thread(ListenLoop) { IsBackground = true };
_listenerThread.Start();
}
private static void ListenLoop()
{
while (_running)
{
var context = _listener.GetContext();
ThreadPool.QueueUserWorkItem(_ => HandleRequest(context));
}
}
Each request is dispatched to the thread pool, so slow requests don't block the listener. The _running flag (volatile) enables clean shutdown.
CSRF protection: The server rejects requests with an Origin header, blocking browser-originated requests while allowing curl and scripts.
3. Route Dispatch¶
Routes are dispatched with a simple switch on the URL path:
private static void HandleRequest(HttpListenerContext ctx)
{
string path = ctx.Request.Url.AbsolutePath;
switch (path)
{
case "/api/status": HandleStatus(ctx); break;
case "/api/player": HandlePlayer(ctx); break;
case "/api/input/key": HandleKeyInput(ctx); break;
// ... 70+ endpoints
}
}
Game state reads use reflection (thread-safe because Terraria's static fields are stable references). Command execution uses a lock to serialize output capture.
4. In-Game Console with Event Subscriptions¶
The console subscribes to framework events for update and draw:
public static void Initialize(ModContext context, ILogger log)
{
// Update logic runs every frame
FrameEvents.OnPreUpdate += OnUpdate;
// Draw callback registered with UIRenderer
UIRenderer.RegisterPanelDraw("debug-console", DrawConsole, priority: 100);
// Capture command output
CommandRegistry.OnOutput += OnCommandOutput;
CommandRegistry.OnClearOutput += OnClearOutput;
}
When the console opens, it blocks keyboard input from reaching the game:
This prevents typing in the console from moving your character or triggering keybinds.
5. Virtual Input: Action-to-Trigger Mapping¶
Virtual input works by injecting into Terraria's trigger system (not raw keyboard state). Each "action" maps to a trigger name:
private static readonly Dictionary<string, string> ActionToTrigger = new Dictionary<string, string>
{
{ "move_left", "Left" },
{ "move_right", "Right" },
{ "jump", "Jump" },
{ "attack", "MouseLeft" },
// ... 28+ actions
};
A Harmony postfix on PlayerInput.UpdateInput() injects active triggers each frame:
[HarmonyPostfix]
static void UpdateInput_Postfix()
{
foreach (var trigger in VirtualInputManager.GetActiveTriggers())
{
PlayerInput.Triggers.Current.KeyStatus[trigger] = true;
}
}
Actions use reference counting so multiple overlapping actions on the same trigger don't conflict.
6. Window Management via P/Invoke¶
Window control uses Win32 APIs to hide/show both the game and console windows:
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("kernel32.dll")]
private static extern IntPtr GetConsoleWindow();
private const int SW_HIDE = 0;
private const int SW_SHOW = 5;
The console handle is available immediately (GetConsoleWindow()), but the game window handle requires reflection through XNA and is only available after Main.Initialize() completes:
public static void OnGameReady()
{
// Game window handle via direct access (Main.instance is public)
if (Main.instance?.Window != null)
_gameHandle = Main.instance.Window.Handle;
if (_startHidden)
Hide();
}
7. Graceful Cleanup¶
Each subsystem cleans up independently, wrapped in try/catch:
public void Unload()
{
try { ConsoleUI.Unload(); } catch { }
try { DebugHttpServer.Stop(); } catch { }
try { WindowManager.RestoreIfHidden(); } catch { }
try { VirtualInputManager.ReleaseAll(); } catch { }
}
This ensures that a failure in one subsystem (e.g., HTTP server already stopped) doesn't prevent window restoration or input cleanup.
Debug Commands¶
The mod registers commands accessible via console or HTTP:
| Command | Description |
|---|---|
menu.state |
Show current menu screen and available characters/worlds |
menu.select <target> |
Navigate menus (singleplayer, character_N, world_N, play, back, title) |
menu.back |
Return to title screen |
menu.enter [char] [world] |
Enter a world by index |
debug-tools.echo <text> |
Print text to console |
HTTP Endpoints (Summary)¶
| Category | Endpoints |
|---|---|
| Status & System | /api/status, /api/commands, /api/execute, /api/mods, /api/capabilities, /api/logs, /api/diagnostics, /api/health, /api/harmony, /api/net/stats |
| Game State (read) | /api/player, /api/world, /api/state/surroundings, /api/state/inventory, /api/state/entities, /api/state/tiles, /api/state/tiles/raw, /api/state/ui, /api/npcs, /api/projectiles |
| Inventory & Equip | /api/inventory/set, /api/equip, /api/hotbar/select, /api/chest/set |
| Player Actions | /api/player/give, /api/teleport, /api/player/buff, /api/save |
| World & Tiles | /api/tiles/set, /api/tiles/fill, /api/world/set, /api/progression/set, /api/event |
| NPC Control | /api/npcs/kill, /api/npcs/set_position, /api/spawn/npc |
| Snapshots | /api/snapshot/list, /api/snapshot/save, /api/snapshot/restore |
| Virtual Input | /api/input/key, /api/input/mouse, /api/input/action, /api/input/release_all, /api/input/actions, /api/input/state, /api/input/log |
| Menu Navigation | /api/menu/state, /api/menu/navigate, /api/menu/enter_world, /api/menu/join_world, /api/menu/exit_world, /api/menu/wait |
| Mod Actions | /api/mod-action, /api/keybind, /api/chat/send, /api/screenshot |
| Introspection | /api/reflect/type, /api/reflect/field, /api/reflect/instance, /api/eval, /api/trace/add, /api/trace/remove, /api/trace/log, /api/watch/add, /api/watch/remove, /api/watch/log |
| Config | /api/config, /api/config/{id}, /api/config/{id}/set, /api/config/{id}/reload, /api/config/{id}/reset |
| Window Control | /api/window/show, /api/window/hide, /api/window/state |
Lessons Learned¶
- Background threads need clean shutdown: Use a volatile flag, catch
HttpListenerExceptionon close - Game window isn't ready at init: Use
OnGameReady()lifecycle hook for anything requiringMain.instance - Trigger injection, not raw keyboard: Terraria overwrites keyboard state each frame; inject into the trigger system instead
- Wrap subsystem cleanup in try/catch: One failure shouldn't prevent others from cleaning up
- CSRF matters even on localhost: Reject
Originheaders to prevent malicious websites from controlling your game
For more on reflection patterns, see Tested Patterns.