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>
102 lines
4.9 KiB
C++
102 lines
4.9 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Watches and samples resolve by path, including into their values
|
|
// ("bodies[1].pos.y" against the watch "bodies"), and set the same way:
|
|
// writing one member of a watched struct writes the whole struct back. Sets
|
|
// respect bounds and read-only watches, computed watches set through their
|
|
// setter (which may refuse), samples keep a frame-stamped history, unknown
|
|
// paths get a suggestion, and a Group's watches go with it.
|
|
import std;
|
|
import Crafter.Debug;
|
|
using namespace Crafter::Debug;
|
|
|
|
namespace {
|
|
std::int32_t failures = 0;
|
|
|
|
void Check(bool ok, std::string_view what) {
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
if (!ok) ++failures;
|
|
}
|
|
|
|
struct Vec3 {
|
|
float x = 0;
|
|
float y = 0;
|
|
float z = 0;
|
|
};
|
|
|
|
struct Body {
|
|
std::int32_t id = 0;
|
|
Vec3 pos;
|
|
};
|
|
|
|
float gravity = -9.81F;
|
|
const std::int32_t build = 7;
|
|
std::vector<Body> bodies = { { 1, { 0, 1, 0 } }, { 2, { 3, 4, 5 } } };
|
|
std::int32_t lives = 3;
|
|
|
|
Detail::Registry& registry() {
|
|
return Detail::Registry::Instance();
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
Watch("physics.gravity", gravity, { .min = -50, .max = 0, .help = "m/s^2" });
|
|
Watch("build", build);
|
|
Watch("bodies", bodies);
|
|
Watch("lives", [] { return lives; }, [](const std::int32_t& value) -> Result<> {
|
|
if (value < 0) return Error{ "Negative", "lives cannot be negative" };
|
|
lives = value;
|
|
return {};
|
|
});
|
|
Watch("score", [] { return 1234; });
|
|
|
|
Check(registry().Resolve("physics.gravity").value.AsNumber() == static_cast<double>(-9.81F), "a variable watch reads the variable");
|
|
Check(registry().Resolve("bodies[1].pos.y").value.AsNumber() == 4, "a path reads into a watched value");
|
|
Check(registry().Resolve("bodies[1]").value.Find("id")->AsInteger() == 2, "an index reads an item");
|
|
|
|
Check(!registry().Set("physics.gravity", Value(-3.5)) && gravity == -3.5F, "a set writes the variable");
|
|
const auto bounded = registry().Set("physics.gravity", Value(10));
|
|
Check(bounded && bounded->find("outside") != std::string::npos && gravity == -3.5F, "a set outside the bounds is refused");
|
|
Check(registry().Set("build", Value(8)) == std::optional<std::string>("'build' is read-only") && build == 7, "a const variable's watch is read-only");
|
|
Check(!registry().Set("bodies[0].pos.x", Value(9)) && bodies[0].pos.x == 9 && bodies[0].pos.y == 1, "a set into a member writes back the whole value");
|
|
const auto wrongType = registry().Set("physics.gravity", Value("down"));
|
|
Check(wrongType && wrongType->find("expected a number") != std::string::npos, "a set of the wrong type is refused");
|
|
|
|
Check(!registry().Set("lives", Value(5)) && lives == 5, "a computed watch sets through its setter");
|
|
const auto negative = registry().Set("lives", Value(-1));
|
|
Check(negative && negative->find("lives cannot be negative") != std::string::npos && lives == 5, "a setter can refuse");
|
|
Check(registry().Set("score", Value(1)) == std::optional<std::string>("'score' is read-only"), "a getter-only watch is read-only");
|
|
|
|
const auto missing = registry().Resolve("physics.gravty");
|
|
Check(!missing.Ok() && missing.error.find("did you mean 'physics.gravity'") != std::string::npos, "an unknown path gets a suggestion");
|
|
const auto badMember = registry().Resolve("bodies[0].pos.w");
|
|
Check(!badMember.Ok() && badMember.error.find("has no member 'w'") != std::string::npos, "an unknown member is named");
|
|
const auto outOfRange = registry().Resolve("bodies[5]");
|
|
Check(!outOfRange.Ok() && outOfRange.error.find("2 items") != std::string::npos, "an index out of range gives the size");
|
|
|
|
for (std::uint64_t frame = 1; frame <= 3; ++frame) {
|
|
Detail::SetFrame(frame);
|
|
Sample("xpbd.residual", static_cast<double>(frame) / 10);
|
|
}
|
|
Check(registry().Resolve("xpbd.residual").value.AsNumber() == 0.3, "a sample resolves to its latest value");
|
|
const auto history = registry().SampleHistory("xpbd.residual");
|
|
Check(history.size() == 3 && history.front().frame == 1 && history.back().frame == 3, "a sample keeps a frame-stamped history");
|
|
|
|
Check(registry().Watches("physics.*").size() == 1 && registry().Watches("*").size() == 5, "watches can be listed by glob");
|
|
Check(registry().Watches("physics.gravity").front()->help == "m/s^2" && registry().Watches("physics.gravity").front()->type.Find("maximum") != nullptr,
|
|
"a watch carries its help and bounds");
|
|
|
|
{
|
|
struct Fort {
|
|
std::int32_t hp = 100;
|
|
Group debug{ "fort1" };
|
|
Fort() { debug.Watch("core.hp", hp); }
|
|
};
|
|
Fort fort;
|
|
Check(registry().Resolve("fort1.core.hp").value.AsInteger() == 100, "a Group prefixes its watches");
|
|
}
|
|
Check(!registry().Resolve("fort1.core.hp").Ok(), "a Group's watches go with it");
|
|
|
|
return failures == 0 ? 0 : 1;
|
|
}
|