feat(registry): commands, watches, samples, the timeline and conditions
The app side of Crafter.Debug, minus the session:
- Command(name, description, handler): the handler takes nothing or
one argument struct and returns void, a value, Result<T> or
Async<T>. Arguments are decoded before it runs, so a mistyped call
is answered with every mistake and never reaches it; a throwing
handler becomes an Exception error instead of unwinding into the
frame loop. This replaces 3DForts' three-file pattern per verb.
- Watch(path, variable | getter [, setter]) with bounds, help and
read-only, and Sample(path, value) from any thread with a
frame-stamped history. Paths resolve into values ("bodies[1].pos.y")
and set the same way, writing a member writes the whole value back.
Unknown paths and commands get a suggestion.
- Group: registrations under a prefix that end with their owner. Not
movable, so neither is its owner: its watches refer to the owner's
members, and a move would leave them pointing at the old object.
- Log, LogWarn, LogError, Check and Emit on one frame-stamped
timeline, with source locations captured without macros. A check
failing every frame is counted every time but stored sparsely, and
the session's listener hears every failure.
- Condition: what `wait` and `pause --when` evaluate each frame. A
condition that cannot be decided reports why instead of being false,
which would make a wait hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:40:59 +01:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
// What an app offers the agent: commands it can call, watches it can read and
|
|
|
|
|
// set, and samples it can trace.
|
|
|
|
|
//
|
|
|
|
|
// struct AimArgs { Debug::Required<std::int32_t> slot; float yaw = 0; float pitch = 0; };
|
|
|
|
|
// Debug::Command("Player_SetAim", "Aim the slot's turret, in degrees.",
|
|
|
|
|
// [](const AimArgs& a) -> Debug::Result<> { ... });
|
|
|
|
|
// Debug::Watch("physics.gravity", Physics::gravity, { .min = -50, .max = 0, .help = "m/s^2" });
|
|
|
|
|
// Debug::Sample("xpbd.residual", residual);
|
|
|
|
|
//
|
|
|
|
|
// A command handler takes nothing, or one argument type that has a Codec (an
|
|
|
|
|
// aggregate, usually, or Value for raw JSON), and returns void, a value with
|
|
|
|
|
// a Codec, Result<T> or Async<T>. Arguments are decoded before the handler
|
|
|
|
|
// runs, so a mistyped call is answered with every mistake in it and never
|
|
|
|
|
// reaches the handler. Free functions register for the program's lifetime;
|
|
|
|
|
// a Group registers under a prefix and unregisters with its owner.
|
|
|
|
|
//
|
|
|
|
|
// Thread safety: registering, unregistering and listing are safe from any
|
|
|
|
|
// thread. Handlers, getters and setters are only ever called by the session,
|
|
|
|
|
// on the thread that calls Pump, so they read app state without locking.
|
|
|
|
|
// Sample may be called from any thread.
|
|
|
|
|
export module Crafter.Debug:Registry;
|
|
|
|
|
import std;
|
|
|
|
|
import Crafter.Debug.Core;
|
|
|
|
|
|
|
|
|
|
export namespace Crafter::Debug {
|
|
|
|
|
struct WatchOptions {
|
|
|
|
|
// Bounds a set must respect, for arithmetic watches.
|
|
|
|
|
std::optional<double> min;
|
|
|
|
|
std::optional<double> max;
|
|
|
|
|
// Shown by `crafter-debug help` and `get`: units, meaning.
|
|
|
|
|
std::string help;
|
|
|
|
|
// A watch of a variable is writable unless this is set.
|
|
|
|
|
bool readOnly = false;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The type-erased registry. Not for app code: the templates below fill it.
|
|
|
|
|
export namespace Crafter::Debug::Detail {
|
|
|
|
|
// What calling a handler produced: a result now, or a poller that the
|
|
|
|
|
// session calls once per Pump until it yields (the handler returned
|
|
|
|
|
// an Async).
|
|
|
|
|
struct CommandOutcome {
|
|
|
|
|
std::optional<Result<Value>> done;
|
|
|
|
|
std::function<std::optional<Result<Value>>()> pending;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct CommandEntry {
|
|
|
|
|
std::string name;
|
|
|
|
|
std::string description;
|
|
|
|
|
// Describe() of the argument type, or null for a command that takes none.
|
|
|
|
|
Value args;
|
|
|
|
|
// Describe() of the result type, or null for one that returns nothing.
|
|
|
|
|
Value returns;
|
|
|
|
|
std::function<CommandOutcome(const Value& params)> invoke;
|
|
|
|
|
std::uint64_t owner = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct WatchEntry {
|
|
|
|
|
std::string path;
|
|
|
|
|
Value type;
|
|
|
|
|
std::string help;
|
|
|
|
|
std::function<Value()> get;
|
|
|
|
|
// Empty for a read-only watch. Returns an error message, or nothing.
|
|
|
|
|
std::function<std::optional<std::string>(const Value& value)> set;
|
|
|
|
|
std::uint64_t owner = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct SamplePoint {
|
|
|
|
|
std::uint64_t frame;
|
|
|
|
|
Value value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A path resolved to a value: from a watch's getter or a sample's
|
|
|
|
|
// latest point, then into it along whatever the path names past the
|
|
|
|
|
// watch ("bodies[3].pos" against the watch "bodies").
|
|
|
|
|
struct Resolved {
|
|
|
|
|
Value value;
|
|
|
|
|
std::string error;
|
|
|
|
|
bool Ok() const { return error.empty(); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class Registry {
|
|
|
|
|
public:
|
|
|
|
|
static Registry& Instance();
|
|
|
|
|
|
|
|
|
|
std::uint64_t NewOwner();
|
|
|
|
|
void AddCommand(CommandEntry entry);
|
|
|
|
|
void AddWatch(WatchEntry entry);
|
|
|
|
|
void AddSample(std::string_view path, Value value, std::uint64_t frame, std::uint64_t owner);
|
|
|
|
|
void RemoveOwner(std::uint64_t owner);
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<const CommandEntry> FindCommand(std::string_view name) const;
|
|
|
|
|
// Sorted by name; `glob` filters ("*" any run, "?" one character).
|
|
|
|
|
std::vector<std::shared_ptr<const CommandEntry>> Commands(std::string_view glob = "*") const;
|
|
|
|
|
std::vector<std::shared_ptr<const WatchEntry>> Watches(std::string_view glob = "*") const;
|
|
|
|
|
std::vector<std::string> SamplePaths(std::string_view glob = "*") const;
|
|
|
|
|
std::vector<SamplePoint> SampleHistory(std::string_view path) const;
|
|
|
|
|
|
|
|
|
|
// Call on the thread that calls Pump: it runs getters.
|
|
|
|
|
Resolved Resolve(std::string_view path) const;
|
|
|
|
|
// Sets the watch the path names, or a part of its value: "fort1.core.pos.x"
|
|
|
|
|
// reads the watch "fort1.core.pos", replaces x and writes it back.
|
feat(session): serve requests without blocking a thread, pause and step
The app's end of the connection, independent of the transport, which
reaches it through Detail::Exchange: one request's way back, safe from
any thread and never blocking.
- What needs only the session's own state (hello, status, list, log,
pause, resume) is answered on the transport thread. What touches the
app (calls, get, set, trace, screenshot) queues for the next Pump and
runs on the app's thread between frames. Waits and steps reply from
the Pump that sees them satisfied, events from the thread that emits
them. No request ever holds a thread while it waits.
- A request whose timeout passes before Pump is answered Timeout and
is guaranteed not to have run, so an agent never wonders whether a
command went through; an Async result arrives on the Pump it is
ready; requests left when the Session ends are answered ShuttingDown.
- Advance() for frame loops and Paused()/TakeSteps() for fixed-step
loops hold what the app gates on them; pausing an app that gates
nothing is refused instead of silently doing nothing. pause --when
stops the frame its condition holds; pause --on-check stops before
the next Advance in the frame a check fails, optionally raising
SIGTRAP for an attached debugger.
- A wait that times out says what its condition's paths were; a log
follower gets matching entries in order, from the backlog on, and is
dropped when it cannot keep up.
- Request parameters go through Codec, so a mistyped request gets the
same did-you-mean replies a mistyped command does. The session sets
PR_SET_PTRACER_ANY so a debugger the agent starts can attach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:53:15 +01:00
|
|
|
// Returns why not (UnknownPath, ReadOnly or InvalidParams), or
|
|
|
|
|
// nothing. Call on the Pump thread.
|
|
|
|
|
std::optional<Error> Set(std::string_view path, const Value& value) const;
|
feat(registry): commands, watches, samples, the timeline and conditions
The app side of Crafter.Debug, minus the session:
- Command(name, description, handler): the handler takes nothing or
one argument struct and returns void, a value, Result<T> or
Async<T>. Arguments are decoded before it runs, so a mistyped call
is answered with every mistake and never reaches it; a throwing
handler becomes an Exception error instead of unwinding into the
frame loop. This replaces 3DForts' three-file pattern per verb.
- Watch(path, variable | getter [, setter]) with bounds, help and
read-only, and Sample(path, value) from any thread with a
frame-stamped history. Paths resolve into values ("bodies[1].pos.y")
and set the same way, writing a member writes the whole value back.
Unknown paths and commands get a suggestion.
- Group: registrations under a prefix that end with their owner. Not
movable, so neither is its owner: its watches refer to the owner's
members, and a move would leave them pointing at the old object.
- Log, LogWarn, LogError, Check and Emit on one frame-stamped
timeline, with source locations captured without macros. A check
failing every frame is counted every time but stored sparsely, and
the session's listener hears every failure.
- Condition: what `wait` and `pause --when` evaluate each frame. A
condition that cannot be decided reports why instead of being false,
which would make a wait hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:40:59 +01:00
|
|
|
|
|
|
|
|
// The closest known command, watch or sample name to `name`, for
|
|
|
|
|
// "did you mean" in errors.
|
|
|
|
|
std::optional<std::string> SuggestCommand(std::string_view name) const;
|
|
|
|
|
std::optional<std::string> SuggestPath(std::string_view path) const;
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
mutable std::mutex mutex;
|
|
|
|
|
std::map<std::string, std::shared_ptr<const CommandEntry>, std::less<>> commands;
|
|
|
|
|
std::map<std::string, std::shared_ptr<const WatchEntry>, std::less<>> watches;
|
|
|
|
|
struct Series {
|
|
|
|
|
std::deque<SamplePoint> points;
|
|
|
|
|
std::uint64_t owner = 0;
|
|
|
|
|
};
|
|
|
|
|
std::map<std::string, Series, std::less<>> samples;
|
|
|
|
|
std::atomic<std::uint64_t> nextOwner{ 1 };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// "*" matches any run of characters, "?" any one.
|
|
|
|
|
bool GlobMatch(std::string_view glob, std::string_view text);
|
|
|
|
|
|
|
|
|
|
// The frame Sample and the timeline stamp what they record with; the
|
|
|
|
|
// session sets it each Pump.
|
|
|
|
|
std::uint64_t CurrentFrame();
|
|
|
|
|
void SetFrame(std::uint64_t frame);
|
|
|
|
|
|
|
|
|
|
template<class>
|
|
|
|
|
struct FunctionTraits;
|
|
|
|
|
|
|
|
|
|
template<class R, class... A>
|
|
|
|
|
struct FunctionTraits<std::function<R(A...)>> {
|
|
|
|
|
using Return = R;
|
|
|
|
|
static constexpr std::size_t arity = sizeof...(A);
|
|
|
|
|
template<std::size_t I>
|
|
|
|
|
using Arg = std::tuple_element_t<I, std::tuple<A...>>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The value type a handler's return produces: void, T, Result<T>, Async<T>.
|
|
|
|
|
template<class R>
|
|
|
|
|
struct Produced {
|
|
|
|
|
using Type = R;
|
|
|
|
|
};
|
|
|
|
|
template<class T>
|
|
|
|
|
struct Produced<Result<T>> {
|
|
|
|
|
using Type = T;
|
|
|
|
|
};
|
|
|
|
|
template<class T>
|
|
|
|
|
struct Produced<Async<T>> {
|
|
|
|
|
using Type = T;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template<class R>
|
|
|
|
|
inline constexpr bool isAsync = false;
|
|
|
|
|
template<class T>
|
|
|
|
|
inline constexpr bool isAsync<Async<T>> = true;
|
|
|
|
|
|
|
|
|
|
template<class R>
|
|
|
|
|
inline constexpr bool isResult = false;
|
|
|
|
|
template<class T>
|
|
|
|
|
inline constexpr bool isResult<Result<T>> = true;
|
|
|
|
|
|
|
|
|
|
inline CommandOutcome Fail(std::string code, std::string message) {
|
|
|
|
|
return CommandOutcome{ .done = Result<Value>(Error{ std::move(code), std::move(message) }), .pending = {} };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class T>
|
|
|
|
|
Result<Value> ToValue(Result<T>&& result) {
|
|
|
|
|
if (!result.Ok()) return result.GetError();
|
|
|
|
|
if constexpr (std::is_void_v<T>) {
|
|
|
|
|
return Value();
|
|
|
|
|
} else {
|
|
|
|
|
return Debug::Encode(result.Get());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calls the handler through `call`, turning whatever it returns, or
|
|
|
|
|
// throws, into an outcome. A handler's exception must not unwind into
|
|
|
|
|
// the app's frame loop.
|
|
|
|
|
template<class Call>
|
|
|
|
|
CommandOutcome Run(Call&& call) {
|
|
|
|
|
using R = std::invoke_result_t<Call&>;
|
|
|
|
|
try {
|
|
|
|
|
if constexpr (std::is_void_v<R>) {
|
|
|
|
|
call();
|
|
|
|
|
return CommandOutcome{ .done = Result<Value>(Value()), .pending = {} };
|
|
|
|
|
} else if constexpr (isAsync<R>) {
|
|
|
|
|
using T = typename Produced<R>::Type;
|
|
|
|
|
return CommandOutcome{
|
|
|
|
|
.done = std::nullopt,
|
|
|
|
|
.pending = [async = call()]() mutable -> std::optional<Result<Value>> {
|
|
|
|
|
std::optional<Result<T>> polled = async.Poll();
|
|
|
|
|
if (!polled) return std::nullopt;
|
|
|
|
|
return ToValue(std::move(*polled));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
} else if constexpr (isResult<R>) {
|
|
|
|
|
return CommandOutcome{ .done = ToValue(call()), .pending = {} };
|
|
|
|
|
} else {
|
|
|
|
|
return CommandOutcome{ .done = Result<Value>(Debug::Encode(call())), .pending = {} };
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::exception& error) {
|
|
|
|
|
return Fail("Exception", std::format("the handler threw: {}", error.what()));
|
|
|
|
|
} catch (...) {
|
|
|
|
|
return Fail("Exception", "the handler threw something that is not a std::exception");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class F>
|
|
|
|
|
CommandEntry MakeCommand(std::string name, std::string_view description, F handler, std::uint64_t owner) {
|
|
|
|
|
using Traits = FunctionTraits<decltype(std::function(handler))>;
|
|
|
|
|
static_assert(Traits::arity <= 1, "a command handler takes no parameters, or one argument struct");
|
|
|
|
|
using R = typename Traits::Return;
|
|
|
|
|
using P = typename Produced<R>::Type;
|
|
|
|
|
CommandEntry entry;
|
|
|
|
|
entry.name = std::move(name);
|
|
|
|
|
entry.description = std::string(description);
|
|
|
|
|
entry.owner = owner;
|
|
|
|
|
if constexpr (!std::is_void_v<P>) entry.returns = Debug::Describe<std::remove_cvref_t<P>>();
|
|
|
|
|
if constexpr (Traits::arity == 0) {
|
|
|
|
|
entry.invoke = [handler = std::move(handler)](const Value& params) mutable -> CommandOutcome {
|
|
|
|
|
if (!params.IsNull() && !(params.IsObject() && params.Size() == 0)) {
|
|
|
|
|
return Fail("InvalidParams", "this command takes no arguments");
|
|
|
|
|
}
|
|
|
|
|
return Run([&] { return handler(); });
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
using Args = std::remove_cvref_t<typename Traits::template Arg<0>>;
|
|
|
|
|
entry.args = Debug::Describe<Args>();
|
|
|
|
|
entry.invoke = [handler = std::move(handler)](const Value& params) mutable -> CommandOutcome {
|
|
|
|
|
Args args{};
|
|
|
|
|
DecodeContext context;
|
|
|
|
|
if constexpr (Aggregate<Args>) {
|
|
|
|
|
Debug::Decode(params.IsNull() ? Value::MakeObject() : params, args, context);
|
|
|
|
|
} else {
|
|
|
|
|
Debug::Decode(params, args, context);
|
|
|
|
|
}
|
|
|
|
|
if (!context.Ok()) return Fail("InvalidParams", context.Report());
|
|
|
|
|
return Run([&] { return handler(args); });
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class T>
|
|
|
|
|
std::optional<std::string> CheckBounds(const T& value, const WatchOptions& options) {
|
|
|
|
|
if constexpr (std::is_arithmetic_v<T> && !std::is_same_v<T, bool>) {
|
|
|
|
|
const double number = static_cast<double>(value);
|
|
|
|
|
if ((options.min && number < *options.min) || (options.max && number > *options.max)) {
|
|
|
|
|
return std::format("{} is outside {}..{}", number, options.min ? std::format("{}", *options.min) : std::string(),
|
|
|
|
|
options.max ? std::format("{}", *options.max) : std::string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class T>
|
|
|
|
|
Value WatchType(const WatchOptions& options) {
|
|
|
|
|
Value type = Debug::Describe<T>();
|
|
|
|
|
if (options.min) type.Set("minimum", Value(*options.min));
|
|
|
|
|
if (options.max) type.Set("maximum", Value(*options.max));
|
|
|
|
|
return type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class T>
|
|
|
|
|
WatchEntry MakeVariableWatch(std::string path, T& variable, const WatchOptions& options, std::uint64_t owner) {
|
|
|
|
|
WatchEntry entry;
|
|
|
|
|
entry.path = std::move(path);
|
|
|
|
|
entry.type = WatchType<T>(options);
|
|
|
|
|
entry.help = options.help;
|
|
|
|
|
entry.owner = owner;
|
|
|
|
|
entry.get = [&variable] { return Debug::Encode(variable); };
|
|
|
|
|
if constexpr (!std::is_const_v<T>) {
|
|
|
|
|
if (!options.readOnly) {
|
|
|
|
|
entry.set = [&variable, options](const Value& value) -> std::optional<std::string> {
|
|
|
|
|
T decoded = variable;
|
|
|
|
|
DecodeContext context;
|
|
|
|
|
Debug::Decode(value, decoded, context);
|
|
|
|
|
if (!context.Ok()) return context.Report();
|
|
|
|
|
if (auto bounds = CheckBounds(decoded, options)) return bounds;
|
|
|
|
|
variable = std::move(decoded);
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class G, class S>
|
|
|
|
|
WatchEntry MakeComputedWatch(std::string path, G getter, std::optional<S> setter, const WatchOptions& options, std::uint64_t owner) {
|
|
|
|
|
using T = std::remove_cvref_t<std::invoke_result_t<G&>>;
|
|
|
|
|
WatchEntry entry;
|
|
|
|
|
entry.path = std::move(path);
|
|
|
|
|
entry.type = WatchType<T>(options);
|
|
|
|
|
entry.help = options.help;
|
|
|
|
|
entry.owner = owner;
|
|
|
|
|
entry.get = [getter]() mutable { return Debug::Encode(getter()); };
|
|
|
|
|
if (setter && !options.readOnly) {
|
|
|
|
|
entry.set = [getter, setter = std::move(*setter), options](const Value& value) mutable -> std::optional<std::string> {
|
|
|
|
|
T decoded = getter();
|
|
|
|
|
DecodeContext context;
|
|
|
|
|
Debug::Decode(value, decoded, context);
|
|
|
|
|
if (!context.Ok()) return context.Report();
|
|
|
|
|
if (auto bounds = CheckBounds(decoded, options)) return bounds;
|
|
|
|
|
using SR = std::invoke_result_t<S&, const T&>;
|
|
|
|
|
if constexpr (std::is_void_v<SR>) {
|
|
|
|
|
setter(decoded);
|
|
|
|
|
} else {
|
|
|
|
|
static_assert(std::is_same_v<SR, Result<>>, "a watch setter returns void or Debug::Result<>");
|
|
|
|
|
Result<> result = setter(decoded);
|
|
|
|
|
if (!result.Ok()) return std::format("{}: {}", result.GetError().code, result.GetError().message);
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string Joined(std::string_view prefix, std::string_view name);
|
|
|
|
|
|
|
|
|
|
struct NoSetter {
|
|
|
|
|
template<class T>
|
|
|
|
|
void operator()(const T&) const {}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export namespace Crafter::Debug {
|
|
|
|
|
// Registers a command for the program's lifetime. See the comment at the top.
|
|
|
|
|
template<class F>
|
|
|
|
|
void Command(std::string_view name, std::string_view description, F&& handler) {
|
|
|
|
|
Detail::Registry::Instance().AddCommand(Detail::MakeCommand(std::string(name), description, std::forward<F>(handler), 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Watches a variable: `get` reads it and `set` writes it, on the Pump
|
|
|
|
|
// thread. The variable must outlive the program, or use a Group.
|
|
|
|
|
template<class T>
|
|
|
|
|
requires (!std::invocable<T&>)
|
|
|
|
|
void Watch(std::string_view path, T& variable, WatchOptions options = {}) {
|
|
|
|
|
Detail::Registry::Instance().AddWatch(Detail::MakeVariableWatch(std::string(path), variable, options, 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Watches what a getter returns; read-only.
|
|
|
|
|
template<class G>
|
|
|
|
|
requires std::invocable<G&>
|
|
|
|
|
void Watch(std::string_view path, G getter, WatchOptions options = {}) {
|
|
|
|
|
Detail::Registry::Instance().AddWatch(Detail::MakeComputedWatch(std::string(path), std::move(getter), std::optional<Detail::NoSetter>(), options, 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Watches a getter and writes through a setter taking the getter's type
|
|
|
|
|
// and returning void or Result<>.
|
|
|
|
|
template<class G, class S>
|
|
|
|
|
requires std::invocable<G&>
|
|
|
|
|
void Watch(std::string_view path, G getter, S setter, WatchOptions options = {}) {
|
|
|
|
|
Detail::Registry::Instance().AddWatch(Detail::MakeComputedWatch(std::string(path), std::move(getter), std::optional<S>(std::move(setter)), options, 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Records a value under `path`, stamped with the current frame, from any
|
|
|
|
|
// thread: for locals in hot code that no watch can reach. `get` reads the
|
|
|
|
|
// latest, `trace` the history.
|
|
|
|
|
template<class T>
|
|
|
|
|
void Sample(std::string_view path, const T& value) {
|
|
|
|
|
Detail::Registry::Instance().AddSample(path, Debug::Encode(value), Detail::CurrentFrame(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Registrations under a prefix that end with the Group: hold one as a
|
|
|
|
|
// member of what the commands and watches reach into.
|
|
|
|
|
//
|
|
|
|
|
// class Fort {
|
|
|
|
|
// Core core;
|
|
|
|
|
// Debug::Group debug;
|
|
|
|
|
// public:
|
|
|
|
|
// explicit Fort(std::int32_t slot) : debug(std::format("fort{}", slot)) {
|
|
|
|
|
// debug.Watch("core.hp", core.hp);
|
|
|
|
|
// }
|
|
|
|
|
// };
|
|
|
|
|
//
|
|
|
|
|
// Neither copyable nor movable, and so neither is a class that holds one:
|
|
|
|
|
// its watches refer to the members they were given, and a moved object
|
|
|
|
|
// would leave them pointing at the old one. Keep such objects in place
|
|
|
|
|
// (std::unique_ptr, std::deque, std::list) rather than in a std::vector.
|
|
|
|
|
class Group {
|
|
|
|
|
public:
|
|
|
|
|
explicit Group(std::string prefix);
|
|
|
|
|
~Group();
|
|
|
|
|
Group(const Group&) = delete;
|
|
|
|
|
Group& operator=(const Group&) = delete;
|
|
|
|
|
|
|
|
|
|
const std::string& Prefix() const { return prefix; }
|
|
|
|
|
|
|
|
|
|
template<class F>
|
|
|
|
|
void Command(std::string_view name, std::string_view description, F&& handler) {
|
|
|
|
|
Detail::Registry::Instance().AddCommand(Detail::MakeCommand(Detail::Joined(prefix, name), description, std::forward<F>(handler), owner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class T>
|
|
|
|
|
requires (!std::invocable<T&>)
|
|
|
|
|
void Watch(std::string_view path, T& variable, WatchOptions options = {}) {
|
|
|
|
|
Detail::Registry::Instance().AddWatch(Detail::MakeVariableWatch(Detail::Joined(prefix, path), variable, options, owner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class G>
|
|
|
|
|
requires std::invocable<G&>
|
|
|
|
|
void Watch(std::string_view path, G getter, WatchOptions options = {}) {
|
|
|
|
|
Detail::Registry::Instance().AddWatch(Detail::MakeComputedWatch(Detail::Joined(prefix, path), std::move(getter), std::optional<Detail::NoSetter>(), options, owner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class G, class S>
|
|
|
|
|
requires std::invocable<G&>
|
|
|
|
|
void Watch(std::string_view path, G getter, S setter, WatchOptions options = {}) {
|
|
|
|
|
Detail::Registry::Instance().AddWatch(Detail::MakeComputedWatch(Detail::Joined(prefix, path), std::move(getter), std::optional<S>(std::move(setter)), options, owner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<class T>
|
|
|
|
|
void Sample(std::string_view path, const T& value) {
|
|
|
|
|
Detail::Registry::Instance().AddSample(Detail::Joined(prefix, path), Debug::Encode(value), Detail::CurrentFrame(), owner);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
std::string prefix;
|
|
|
|
|
std::uint64_t owner = 0;
|
|
|
|
|
};
|
|
|
|
|
}
|