feat(core): Value, reflection, codecs and results, shared by app and client
Crafter.Debug.Core is what the app side and the crafter-debug client
both need, without the session:
- Value: a JSON value with its own parser and printer, so no consumer
interface sees a JSON library. Integers stay integers and whole
doubles stay doubles; NaN and the infinities, which JSON cannot
spell, are written as strings rather than hidden, because a debugger
must not lose a NaN; invalid UTF-8 is replaced so output always
parses. Objects keep insertion order, the order an agent reads.
- Reflect: aggregate members and names through structured-binding
packs and source_location, taken from Crafter.DB, plus scoped-enum
names by probing a range of values. Self-tested with static_asserts
so a compiler that changes function_name()'s shape fails the build.
- Codec<T>: encode, decode and describe for bool, integers, floating
point, strings, enums, optional, vector, array, string-keyed maps,
Value, Required<T> and every aggregate. Decode reports every
problem in a call with its path and, for a typo, the likely intended
name, and never throws: a bad call cannot take an app down, which a
wrongly typed parameter could in 3DForts' hand-written from_json.
- Result, Error and Async: handler results, including ones that take
frames to arrive, so callers need no ready:false polling.
project.cpp now builds Crafter.Debug.Core as its own library, used by
both the app library and the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
// Codec round-trips an aggregate built from every supported field type
|
|
|
|
|
// (strings, nested aggregates in a vector, optional, std::array, string-keyed
|
|
|
|
|
// maps, a scoped enum, Required) through Value and its text, with no
|
|
|
|
|
// hand-written serialization. Fields the input leaves out keep their default
|
|
|
|
|
// member initializers, null clears an optional, and an enum also accepts its
|
2026-11-01 03:21:41 +01:00
|
|
|
// integer. A float encodes as its shortest decimal, 0.1 rather than the
|
|
|
|
|
// 0.10000000149011612 it widens to, which every float watch used to show.
|
|
|
|
|
// Guards the reflection that command arguments and results rely on.
|
feat(core): Value, reflection, codecs and results, shared by app and client
Crafter.Debug.Core is what the app side and the crafter-debug client
both need, without the session:
- Value: a JSON value with its own parser and printer, so no consumer
interface sees a JSON library. Integers stay integers and whole
doubles stay doubles; NaN and the infinities, which JSON cannot
spell, are written as strings rather than hidden, because a debugger
must not lose a NaN; invalid UTF-8 is replaced so output always
parses. Objects keep insertion order, the order an agent reads.
- Reflect: aggregate members and names through structured-binding
packs and source_location, taken from Crafter.DB, plus scoped-enum
names by probing a range of values. Self-tested with static_asserts
so a compiler that changes function_name()'s shape fails the build.
- Codec<T>: encode, decode and describe for bool, integers, floating
point, strings, enums, optional, vector, array, string-keyed maps,
Value, Required<T> and every aggregate. Decode reports every
problem in a call with its path and, for a typo, the likely intended
name, and never throws: a bad call cannot take an app down, which a
wrongly typed parameter could in 3DForts' hand-written from_json.
- Result, Error and Async: handler results, including ones that take
frames to arrive, so callers need no ready:false polling.
project.cpp now builds Crafter.Debug.Core as its own library, used by
both the app library and the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
|
|
|
import std;
|
|
|
|
|
import Crafter.Debug.Core;
|
|
|
|
|
using namespace Crafter::Debug;
|
|
|
|
|
|
|
|
|
|
namespace Game {
|
|
|
|
|
enum class Mode : std::uint8_t { Idle, Build, Fight = 5 };
|
|
|
|
|
|
|
|
|
|
struct Vec3 {
|
|
|
|
|
float x = 0;
|
|
|
|
|
float y = 0;
|
|
|
|
|
float z = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct Loadout {
|
|
|
|
|
std::string name = "default";
|
|
|
|
|
std::vector<Vec3> points;
|
|
|
|
|
std::optional<std::int32_t> target;
|
|
|
|
|
std::array<bool, 3> flags{};
|
|
|
|
|
std::map<std::string, double> weights;
|
|
|
|
|
Mode mode = Mode::Idle;
|
|
|
|
|
Required<std::uint16_t> slot;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
std::int32_t failures = 0;
|
|
|
|
|
|
|
|
|
|
void Check(bool ok, std::string_view what) {
|
|
|
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
|
|
|
if (!ok) ++failures;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
Game::Loadout in;
|
|
|
|
|
in.name = "fort \"one\"";
|
|
|
|
|
in.points = { { 1, 2, 3 }, { -0.5F, 0, 1e10F } };
|
|
|
|
|
in.target = -7;
|
|
|
|
|
in.flags = { true, false, true };
|
|
|
|
|
in.weights = { { "cannon", 0.25 }, { "laser", 1 } };
|
|
|
|
|
in.mode = Game::Mode::Fight;
|
|
|
|
|
*in.slot = 3;
|
|
|
|
|
|
|
|
|
|
const Value encoded = Encode(in);
|
|
|
|
|
Check(encoded.IsObject() && encoded.Size() == 7, "an aggregate encodes as an object with one member per field");
|
|
|
|
|
Check(encoded.Find("mode")->AsString() == "Fight", "a scoped enum encodes by name");
|
|
|
|
|
Check(encoded.Find("slot")->AsInteger() == 3, "Required encodes as its value");
|
|
|
|
|
Check(encoded.Find("target")->AsInteger() == -7, "an optional with a value encodes as the value");
|
|
|
|
|
Check(encoded.Find("points")->Items()[1].Find("z")->AsNumber() == 1e10F, "nested aggregates encode inside a vector");
|
|
|
|
|
|
|
|
|
|
Game::Loadout out;
|
|
|
|
|
DecodeContext context;
|
|
|
|
|
Decode(Value::Parse(encoded.Dump()), out, context);
|
|
|
|
|
Check(context.Ok(), std::format("decodes what it encoded ({})", context.Report()));
|
|
|
|
|
Check(out.name == in.name && out.points.size() == 2 && out.points[1].z == 1e10F && out.target == -7 && out.flags[2]
|
|
|
|
|
&& out.weights.at("laser") == 1 && out.mode == Game::Mode::Fight && *out.slot == 3,
|
|
|
|
|
"every field survives the round trip");
|
|
|
|
|
|
|
|
|
|
Game::Loadout partial;
|
|
|
|
|
partial.target = 9;
|
|
|
|
|
DecodeContext partialContext;
|
|
|
|
|
Decode(Value::Parse(R"({"slot": 1, "target": null, "mode": 1})"), partial, partialContext);
|
|
|
|
|
Check(partialContext.Ok(), std::format("a partial object decodes ({})", partialContext.Report()));
|
|
|
|
|
Check(partial.name == "default" && partial.points.empty(), "fields left out keep their defaults");
|
|
|
|
|
Check(!partial.target.has_value(), "null clears an optional");
|
|
|
|
|
Check(partial.mode == Game::Mode::Build, "an enum accepts its integer");
|
|
|
|
|
|
2026-11-01 03:21:41 +01:00
|
|
|
Check(Encode(0.1F).Dump() == "0.1" && Encode(-9.81F).Dump() == "-9.81" && Encode(4.0F).Dump() == "4.0",
|
|
|
|
|
std::format("a float reads as its shortest decimal ({}, {})", Encode(0.1F).Dump(), Encode(-9.81F).Dump()));
|
|
|
|
|
float tenth = 0;
|
|
|
|
|
DecodeContext tenthContext;
|
|
|
|
|
Decode(Value::Parse(Encode(0.1F).Dump()), tenth, tenthContext);
|
|
|
|
|
Check(tenthContext.Ok() && tenth == 0.1F && Encode(0.1).Dump() == "0.1", "and decodes to the same float; a double is unchanged");
|
|
|
|
|
|
feat(core): Value, reflection, codecs and results, shared by app and client
Crafter.Debug.Core is what the app side and the crafter-debug client
both need, without the session:
- Value: a JSON value with its own parser and printer, so no consumer
interface sees a JSON library. Integers stay integers and whole
doubles stay doubles; NaN and the infinities, which JSON cannot
spell, are written as strings rather than hidden, because a debugger
must not lose a NaN; invalid UTF-8 is replaced so output always
parses. Objects keep insertion order, the order an agent reads.
- Reflect: aggregate members and names through structured-binding
packs and source_location, taken from Crafter.DB, plus scoped-enum
names by probing a range of values. Self-tested with static_asserts
so a compiler that changes function_name()'s shape fails the build.
- Codec<T>: encode, decode and describe for bool, integers, floating
point, strings, enums, optional, vector, array, string-keyed maps,
Value, Required<T> and every aggregate. Decode reports every
problem in a call with its path and, for a typo, the likely intended
name, and never throws: a bad call cannot take an app down, which a
wrongly typed parameter could in 3DForts' hand-written from_json.
- Result, Error and Async: handler results, including ones that take
frames to arrive, so callers need no ready:false polling.
project.cpp now builds Crafter.Debug.Core as its own library, used by
both the app library and the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
|
|
|
float nan = 0;
|
|
|
|
|
DecodeContext nanContext;
|
|
|
|
|
Decode(Encode(std::numeric_limits<float>::quiet_NaN()), nan, nanContext);
|
|
|
|
|
Check(nanContext.Ok() && std::isnan(nan), "NaN survives the round trip");
|
|
|
|
|
|
|
|
|
|
std::vector<bool> bits;
|
|
|
|
|
DecodeContext bitsContext;
|
|
|
|
|
Decode(Value::Parse("[true,false,true]"), bits, bitsContext);
|
|
|
|
|
Check(bitsContext.Ok() && bits.size() == 3 && bits[2] && !bits[1], "std::vector<bool> decodes");
|
|
|
|
|
|
|
|
|
|
const std::uint64_t large = (std::uint64_t{ 1 } << 63) + 2048;
|
|
|
|
|
std::uint64_t decoded = 0;
|
|
|
|
|
DecodeContext largeContext;
|
|
|
|
|
Decode(Value::Parse(Encode(large).Dump()), decoded, largeContext);
|
|
|
|
|
Check(largeContext.Ok() && decoded == large, "a uint64 above the int64 range survives");
|
|
|
|
|
|
|
|
|
|
return failures == 0 ? 0 : 1;
|
|
|
|
|
}
|