Crafter.Debug/tooling/AGENTS.consumer.md
Jorijn van der Graaf 8e7f8c5f3b
fix(tooling): build first, then start the app, then list it
The notes had agents start `crafter-build <flag> -r` in the background
and run `crafter-debug ls --wait 60s` straight after. That raced the
build: the first agent to follow them ran the plain build's stub, which
said "no debug build yet" and exited, since the client `ls --wait` needs
is one of the things the debug build compiles, and it polled by hand
until the build was done.

The race came from the order, so the order changes: build with the flag
in the foreground, which ends once the app and the client are built,
then start the app with -r in the background, then `ls --wait`, which
now only waits for what it is for, the app registering. A launcher that
polls for the build instead was tried and dropped: a shell loop that
reruns itself each second, and waits out its whole --wait when no build
is coming at all.

The consumer notes, the client's guide and the README say so, and the
stub and the not-yet-built launcher name the step that was skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:24:35 +01:00

4.3 KiB

Crafter.Debug

This app can be driven while it runs: call its commands, read and set its state, pause and step it, wait for conditions, read its log, take screenshots. It is only in debug builds, under this project's debug flag (the notes above name it, usually --debug-api).

Driving it

  • Build with the debug flag first, in the foreground: crafter-build <debug flag>. That build also compiles the client ./build/crafter-debug runs. The first one takes a few minutes.
  • Then start the app in the background, since it does not exit: crafter-build <debug flag> -r.
  • From the project directory, use ./build/crafter-debug:
    • ls --wait 60s until it is listed; status, cmds, help <command>, get '*'
    • call <command> key=value ..., set <path> <value>
    • pause --when '<condition>' then wait --paused; step 3, resume
    • wait '<condition>', wait --frames 30, wait --event <Name>
    • trace <path> --frames 120, log --level warn, log --follow (under Monitor)
    • screenshot: open the PNG it prints with Read
    • quit when done
  • ./build/crafter-debug guide is the full reference. Errors name the next step.
  • Exit codes: 2 usage, 3 no instance, 4 app and client from different versions, 5 timed out, 6 the app answered an error.
  • Requests run when the app calls Debug::Pump(), once per frame; status shows when it last did.
  • Stopping the background -r task can leave the app running. Stop it with quit, or quit --force if its loop is stuck.
  • Run long waits in the background, and chain short steps with &&.
  • debugger prints how to attach lldb or gdb for breakpoints and memory.

Coding with it

Missing a command or watch for the task at hand? Add it. It is debug-only code.

  • Where it goes:
    • The hook points sit in main, under the project's debug define, or in a shim module: Debug::Session debug({ .name = "..." }); after ThreadPool::Start(), and Debug::Pump() once per frame on the app's thread.
    • Registrations go in debug-only sources, which project.cpp adds only when the flag is on.
  • Pause and step hold only what is gated on them. In a frame loop: while (acc >= dt && Debug::Advance()) { Tick(); acc -= dt; }. In a fixed-step loop: Debug::Paused() and Debug::TakeSteps().
  • Registering (import Crafter.Debug;, namespace Crafter::Debug):
struct SpawnArgs { Debug::Required<std::string> kind; float x = 0; float y = 0; };   // fields become key=value
Debug::Command("World_Spawn", "Spawn an entity; returns its id.",
    [](const SpawnArgs& a) -> Debug::Result<std::int32_t> {
        if (!world) return Debug::Error{ "NoWorld", "no world loaded; call Level_Load first" };
        return world->Spawn(*a.kind, a.x, a.y);
    });
Debug::Watch("physics.gravity", gravity, { .min = -50, .max = 0, .help = "m/s^2" });   // get and set
Debug::Watch("scene", [] { return SceneName(); });                                       // read-only
Debug::Sample("solver.residual", residual);                    // hot code, any thread; `trace --history`
Debug::Check(mass > 0, "physics", "body {} has mass {}", id, mass);   // a soft assert; also Log, LogWarn, LogError, Emit
  • Rules:
    • Threading: handlers, getters and setters run on the app's thread inside Pump, so they read state without locks, but must not block. For a result that takes frames (a GPU readback, a network round trip), return Debug::Async<T> with a poller.
    • Reuse the app's own paths: call the same code the UI or game uses, rather than re-implementing it in the command, because a re-implemented command drifts from the real behaviour.
    • Errors are Debug::Error{ "Code", "message" }, with the message saying what to do next.
    • Names: commands are Area_Verb; watches are dotted paths.
    • Per-object state goes through a Debug::Group member, Debug::Group debug{ std::format("fort{}", slot) }, which prefixes paths and unregisters with its owner. It can't be moved, so keep such objects in std::unique_ptr or std::deque.
    • Types: arguments and results are aggregates (public fields with default initializers), scoped enums, strings, numbers or std containers. For anything else, specialize Debug::Codec<T>.
    • Screenshots: Debug::SetCapture makes them work. Crafter.Debug's README has the Crafter.Graphics version.