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>
93 lines
3.7 KiB
C++
93 lines
3.7 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// What a command handler returns besides a plain value: an Error with a
|
|
// stable code and a message written for the agent reading it, a Result that
|
|
// is either, and an Async for a result that takes frames to produce.
|
|
export module Crafter.Debug.Core:Result;
|
|
import std;
|
|
|
|
export namespace Crafter::Debug {
|
|
struct Error {
|
|
// Stable and machine-readable, "InvalidScene": scripts branch on it.
|
|
std::string code;
|
|
// For whoever reads the reply: what went wrong, and the next step
|
|
// ("not in a game; run Lobby_Start first").
|
|
std::string message;
|
|
};
|
|
|
|
template<class T = void>
|
|
class Result {
|
|
public:
|
|
// Anything T can be made from, so `return "text";` works for a
|
|
// Result<std::string>.
|
|
template<class U = T>
|
|
requires (std::constructible_from<T, U &&> && !std::same_as<std::remove_cvref_t<U>, Error> && !std::same_as<std::remove_cvref_t<U>, Result>)
|
|
Result(U&& value) : state(std::in_place_index<0>, std::forward<U>(value)) {}
|
|
Result(Error error) : state(std::in_place_index<1>, std::move(error)) {}
|
|
|
|
bool Ok() const { return state.index() == 0; }
|
|
explicit operator bool() const { return Ok(); }
|
|
T& Get() { return std::get<0>(state); }
|
|
const T& Get() const { return std::get<0>(state); }
|
|
const Error& GetError() const { return std::get<1>(state); }
|
|
|
|
private:
|
|
std::variant<T, Error> state;
|
|
};
|
|
|
|
template<>
|
|
class Result<void> {
|
|
public:
|
|
Result() = default;
|
|
Result(Error error) : error(std::move(error)) {}
|
|
|
|
bool Ok() const { return !error.has_value(); }
|
|
explicit operator bool() const { return Ok(); }
|
|
const Error& GetError() const { return *error; }
|
|
|
|
private:
|
|
std::optional<Error> error;
|
|
};
|
|
|
|
// A result that takes frames to arrive, such as a GPU readback. The
|
|
// session polls it once per Pump, on the app's thread, until it yields;
|
|
// the caller simply waits, with no ready:false loop of its own. The
|
|
// poller returns std::optional<T> (empty while not ready), or
|
|
// std::optional<Result<T>> to fail; for Async<void>, a bool or a
|
|
// std::optional<Result<>>.
|
|
template<class T = void>
|
|
class Async {
|
|
public:
|
|
template<class F>
|
|
requires std::invocable<F&>
|
|
Async(F poller) : poll(Wrap(std::move(poller))) {}
|
|
|
|
// Empty while not ready.
|
|
std::optional<Result<T>> Poll() { return poll(); }
|
|
|
|
private:
|
|
std::function<std::optional<Result<T>>()> poll;
|
|
|
|
template<class F>
|
|
static std::function<std::optional<Result<T>>()> Wrap(F poller) {
|
|
using R = std::invoke_result_t<F&>;
|
|
if constexpr (std::is_same_v<R, std::optional<Result<T>>>) {
|
|
return poller;
|
|
} else if constexpr (std::is_void_v<T>) {
|
|
static_assert(std::is_same_v<R, bool>, "an Async<void> poller returns bool or std::optional<Result<>>");
|
|
return [poller = std::move(poller)]() mutable -> std::optional<Result<T>> {
|
|
if (!poller()) return std::nullopt;
|
|
return Result<T>();
|
|
};
|
|
} else {
|
|
static_assert(std::is_same_v<R, std::optional<T>>, "an Async<T> poller returns std::optional<T> or std::optional<Result<T>>");
|
|
return [poller = std::move(poller)]() mutable -> std::optional<Result<T>> {
|
|
std::optional<T> value = poller();
|
|
if (!value) return std::nullopt;
|
|
return Result<T>(std::move(*value));
|
|
};
|
|
}
|
|
}
|
|
};
|
|
}
|