L'adaptateur traduit clavier et manette en actions SEQUA (PlayerInput) ; le Core ne recoit et n'enregistre que ces actions, le rejeu reste exact. - SEQUA 3D : PlayerInput, poll_input(), capacite player_input (interface des adaptateurs en version 2) - protocole Core - rendu 0.2 : render/input, envoye quand les actions changent ; entree remise au neutre si le rendu disparait - Godot : InputMap du projet (ZQSD, fleches, espace, manette) ou script SequaInput.sequa_player_input() pour une correspondance avancee ; entree neutre sans focus ; exemple examples/sequa_input.gd - adaptateur debug : entrees scriptees par numero d'image - Studio : ligne "Entrees du joueur" du panneau Rendu
534 lines
21 KiB
C++
534 lines
21 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
//
|
|
// Tests du codec JSON provisoire du protocole Core - rendu (spec §5) : hex64, aller-retours,
|
|
// refus stricts et exemple conforme au texte de la spécification.
|
|
#include "sequa/3d/wire.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace sequa::three_d;
|
|
using namespace sequa::three_d::wire;
|
|
|
|
namespace {
|
|
|
|
// Analyse sans exception : le codec ne doit jamais en laisser sortir.
|
|
[[nodiscard]] nlohmann::json parse_text(const std::string& text)
|
|
{
|
|
return nlohmann::json::parse(text, nullptr, false);
|
|
}
|
|
|
|
// sans operator== défini : comparaison champ par champ.
|
|
[[nodiscard]] bool capabilities_equal(const EngineCapabilities& left, const EngineCapabilities& right)
|
|
{
|
|
return left.engine_name == right.engine_name && left.engine_version == right.engine_version &&
|
|
left.buffers == right.buffers && left.windowed == right.windowed && left.headless == right.headless &&
|
|
left.player_input == right.player_input;
|
|
}
|
|
|
|
Transform sample_transform()
|
|
{
|
|
Transform transform;
|
|
transform.position = {1, -2.5, 3};
|
|
transform.rotation = {0.0f, 0.70710678f, 0.0f, 0.70710678f};
|
|
transform.scale = {2, 2, 2};
|
|
return transform;
|
|
}
|
|
|
|
Visual sample_visual(Primitive primitive)
|
|
{
|
|
Visual visual;
|
|
visual.primitive = primitive;
|
|
visual.size = {0.5, 1.5, 2.5};
|
|
visual.color = {0.1f, 0.2f, 0.9f, 0.75f};
|
|
visual.semantic_class = 42;
|
|
return visual;
|
|
}
|
|
|
|
Transform sample_camera_transform()
|
|
{
|
|
Transform transform;
|
|
transform.position = {5, 6, 7};
|
|
return transform;
|
|
}
|
|
|
|
Camera sample_camera()
|
|
{
|
|
Camera camera;
|
|
camera.transform = sample_camera_transform();
|
|
camera.vertical_fov_deg = 60;
|
|
camera.near_plane = 0.1f;
|
|
camera.far_plane = 100;
|
|
return camera;
|
|
}
|
|
|
|
// Valeurs de l'exemple conforme au texte de la spécification (protocole 0.1 §5).
|
|
Transform spec_transform()
|
|
{
|
|
Transform transform;
|
|
transform.position = {1, 2, 3};
|
|
return transform;
|
|
}
|
|
|
|
Visual spec_visual()
|
|
{
|
|
Visual visual;
|
|
visual.primitive = Primitive::box;
|
|
visual.size = {1, 1, 1};
|
|
visual.color = {0.5f, 0.6f, 0.7f, 1.0f};
|
|
visual.semantic_class = 2;
|
|
return visual;
|
|
}
|
|
|
|
Camera spec_camera()
|
|
{
|
|
Camera camera;
|
|
camera.transform.position = {0, 2, 5}; // rotation et échelle par défaut
|
|
return camera;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Wire : hex64, 16 caractères minuscules en sortie")
|
|
{
|
|
CHECK(to_hex64(0ULL) == "0000000000000000");
|
|
CHECK(to_hex64(1ULL) == "0000000000000001");
|
|
CHECK(to_hex64(0x0123456789abcdefULL) == "0123456789abcdef");
|
|
CHECK(to_hex64(std::numeric_limits<std::uint64_t>::max()) == "ffffffffffffffff");
|
|
}
|
|
|
|
TEST_CASE("Wire : from_hex64, de 1 à 16 caractères, majuscules ou minuscules")
|
|
{
|
|
CHECK(from_hex64("0") == 0U);
|
|
CHECK(from_hex64("f") == 15U);
|
|
CHECK(from_hex64("01a") == 26U);
|
|
CHECK(from_hex64("01A") == 26U);
|
|
CHECK(from_hex64("0123456789abcdef") == 0x0123456789abcdefULL);
|
|
CHECK(from_hex64("0123456789ABCDEF") == 0x0123456789abcdefULL);
|
|
CHECK(from_hex64("ffffffffffffffff") == std::numeric_limits<std::uint64_t>::max());
|
|
|
|
// Refus : vide, trop long, caractère hors alphabet.
|
|
CHECK_FALSE(from_hex64("").has_value());
|
|
CHECK_FALSE(from_hex64("0123456789abcdef0").has_value());
|
|
CHECK_FALSE(from_hex64("01g").has_value());
|
|
CHECK_FALSE(from_hex64("01fG").has_value());
|
|
CHECK_FALSE(from_hex64(" 1").has_value());
|
|
|
|
// Aller-retour sur des valeurs de tous les poids.
|
|
const std::uint64_t values[] = {1U, 255U, 65536U, 0xdeadbeefcafef00dULL};
|
|
for (const std::uint64_t value : values) {
|
|
CHECK(from_hex64(to_hex64(value)) == value);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Wire : create fait l'aller-retour, pour chaque primitive")
|
|
{
|
|
for (const Primitive primitive : {Primitive::box, Primitive::sphere, Primitive::capsule, Primitive::plane}) {
|
|
const SceneCommand command = CreateObject{0x1a2b3c4d5e6fULL, sample_transform(), sample_visual(primitive)};
|
|
const nlohmann::json encoded = encode_command(command);
|
|
CHECK(encoded["op"] == "create");
|
|
CHECK(encoded["id"] == to_hex64(0x1a2b3c4d5e6fULL));
|
|
|
|
std::string error;
|
|
const std::optional<SceneCommand> decoded = decode_command(encoded, &error);
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(error.empty());
|
|
CHECK(*decoded == command);
|
|
CHECK(std::get<CreateObject>(*decoded).visual.primitive == primitive);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Wire : destroy, transform, visual et camera font l'aller-retour")
|
|
{
|
|
{
|
|
const SceneCommand command = DestroyObject{0xfeedface0001ULL};
|
|
std::optional<SceneCommand> decoded = decode_command(encode_command(command));
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(*decoded == command);
|
|
}
|
|
{
|
|
const SceneCommand command = SetTransform{7ULL, sample_transform()};
|
|
std::optional<SceneCommand> decoded = decode_command(encode_command(command));
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(*decoded == command);
|
|
}
|
|
{
|
|
const SceneCommand command = SetVisual{7ULL, sample_visual(Primitive::capsule)};
|
|
std::optional<SceneCommand> decoded = decode_command(encode_command(command));
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(*decoded == command);
|
|
}
|
|
{
|
|
const SceneCommand command = SetCamera{sample_camera()};
|
|
std::optional<SceneCommand> decoded = decode_command(encode_command(command));
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(*decoded == command);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Wire : un SceneDeltaMessage complet fait l'aller-retour")
|
|
{
|
|
SceneDeltaMessage message;
|
|
message.delta.world_version = 0xabcdef9876543210ULL;
|
|
message.delta.commands = {
|
|
SceneCommand{CreateObject{1ULL, sample_transform(), sample_visual(Primitive::box)}},
|
|
SceneCommand{DestroyObject{2ULL}},
|
|
SceneCommand{SetTransform{1ULL, sample_camera_transform()}},
|
|
SceneCommand{SetVisual{1ULL, sample_visual(Primitive::sphere)}},
|
|
SceneCommand{SetCamera{sample_camera()}},
|
|
};
|
|
message.tick = 0x123456789ULL;
|
|
message.full = true;
|
|
message.sent_at_us = 0x987654321ULL;
|
|
|
|
std::string error;
|
|
const std::optional<SceneDeltaMessage> decoded = decode_scene_delta(encode_scene_delta(message), &error);
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(error.empty());
|
|
CHECK(decoded->delta.world_version == message.delta.world_version);
|
|
CHECK(decoded->delta.commands == message.delta.commands);
|
|
CHECK(decoded->tick == message.tick);
|
|
CHECK(decoded->full == message.full);
|
|
CHECK(decoded->sent_at_us == message.sent_at_us);
|
|
|
|
const nlohmann::json payload = encode_scene_delta(message);
|
|
REQUIRE(payload.contains("worldVersion"));
|
|
CHECK(payload["worldVersion"] == "abcdef9876543210");
|
|
CHECK(payload["full"] == true);
|
|
}
|
|
|
|
TEST_CASE("Wire : les capacités font l'aller-retour")
|
|
{
|
|
EngineCapabilities caps;
|
|
caps.engine_name = "godot";
|
|
caps.engine_version = "4.3.stable.official";
|
|
caps.buffers = BufferSet::all();
|
|
caps.windowed = true;
|
|
caps.headless = false;
|
|
|
|
const nlohmann::json encoded = encode_capabilities(caps);
|
|
CHECK(encoded["engineName"] == "godot");
|
|
CHECK(encoded["buffers"] == nlohmann::json::array({"color", "depth", "normals", "motion_vectors", "albedo",
|
|
"object_ids", "semantic_mask"}));
|
|
|
|
std::string error;
|
|
std::optional<EngineCapabilities> decoded = decode_capabilities(encoded, &error);
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(error.empty());
|
|
CHECK(capabilities_equal(*decoded, caps));
|
|
|
|
// Sous-ensemble de tampons : BufferSet vide compris.
|
|
caps.buffers = BufferSet{FrameBuffer::color, FrameBuffer::semantic_mask};
|
|
decoded = decode_capabilities(encode_capabilities(caps));
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(capabilities_equal(*decoded, caps));
|
|
|
|
caps.buffers = BufferSet{};
|
|
decoded = decode_capabilities(encode_capabilities(caps));
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(decoded->buffers.empty());
|
|
}
|
|
|
|
TEST_CASE("Wire : refus d'un id 0 et d'un id non hexadécimal")
|
|
{
|
|
for (const char* id : {"0", "0000000000000000"}) {
|
|
const std::string text = std::string(R"({"op": "destroy", "id": ")") + id + R"("})";
|
|
const nlohmann::json command = parse_text(text);
|
|
REQUIRE(command.is_object());
|
|
std::string error;
|
|
CHECK_FALSE(decode_command(command, &error).has_value());
|
|
CHECK_FALSE(error.empty());
|
|
}
|
|
|
|
std::string error;
|
|
const nlohmann::json bad_format = parse_text(R"({"op": "destroy", "id": "nothex"})");
|
|
CHECK_FALSE(decode_command(bad_format, &error).has_value());
|
|
CHECK_FALSE(error.empty());
|
|
|
|
// Trop d'hexadécimal et identifiant numérique : refus (spec : hexadécimal, jamais nombre).
|
|
const nlohmann::json too_long = parse_text(R"({"op": "destroy", "id": "00000000deadbeef01"})");
|
|
CHECK_FALSE(decode_command(too_long).has_value());
|
|
const nlohmann::json numeric = parse_text(R"({"op": "destroy", "id": 7})");
|
|
CHECK_FALSE(decode_command(numeric).has_value());
|
|
}
|
|
|
|
TEST_CASE("Wire : refus des tableaux de mauvaise longueur")
|
|
{
|
|
// position à 2 composantes, rotation à 5, size à 3, color à 2.
|
|
struct Case {
|
|
const char* text;
|
|
};
|
|
const Case cases[] = {
|
|
{R"({"op": "transform", "id": "1",
|
|
"transform": {"position": [1, 2], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}})"},
|
|
{R"({"op": "transform", "id": "1",
|
|
"transform": {"position": [1, 2, 3], "rotation": [0, 0, 0, 1, 1], "scale": [1, 1, 1]}})"},
|
|
{R"({"op": "visual", "id": "1", "visual": {"primitive": "box", "size": [1, 1, 1],
|
|
"color": [0.5, 0.5], "semanticClass": 0}})"},
|
|
{R"({"op": "create", "id": "1", "transform": {"position": [1, 2, 3], "rotation": [0, 0, 0, 1],
|
|
"scale": [1, 1, 1]}, "visual": {"primitive": "box", "size": [1], "color": [1, 1, 1, 1],
|
|
"semanticClass": 0}})"},
|
|
};
|
|
for (const Case& case_entry : cases) {
|
|
std::string error;
|
|
CHECK_FALSE(decode_command(parse_text(case_entry.text), &error).has_value());
|
|
CHECK_FALSE(error.empty());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Wire : refus des nombres non finis")
|
|
{
|
|
std::string error;
|
|
nlohmann::json transform = parse_text(
|
|
R"({"position": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]})");
|
|
transform["position"] = nlohmann::json::array(
|
|
{1.0, std::numeric_limits<double>::quiet_NaN(), 3.0});
|
|
CHECK_FALSE(decode_command(nlohmann::json{{"op", "transform"}, {"id", "1"}, {"transform", transform}}, &error)
|
|
.has_value());
|
|
CHECK(error.find("fini") != std::string::npos);
|
|
|
|
transform["position"] = nlohmann::json::array(
|
|
{1.0, std::numeric_limits<double>::infinity(), 3.0});
|
|
CHECK_FALSE(decode_command(nlohmann::json{{"op", "transform"}, {"id", "1"}, {"transform", transform}}).has_value());
|
|
|
|
// Depuis un texte : nlohmann/json refuse déjà un nombre hors limites (1e999) à la lecture.
|
|
const nlohmann::json from_text = parse_text(
|
|
R"({"op": "camera", "camera": {"transform": {"position": [0, 0, 1e999], "rotation": [0, 0, 0, 1],
|
|
"scale": [1, 1, 1]}, "verticalFovDeg": 60, "near": 0.1, "far": 100}})");
|
|
CHECK_FALSE(from_text.is_object());
|
|
CHECK_FALSE(decode_command(from_text).has_value());
|
|
}
|
|
|
|
TEST_CASE("Wire : refus d'une primitive et d'un op inconnus")
|
|
{
|
|
std::string error;
|
|
const nlohmann::json bad_primitive = parse_text(
|
|
R"({"op": "visual", "id": "1", "visual": {"primitive": "torus", "size": [1, 1, 1],
|
|
"color": [1, 1, 1, 1], "semanticClass": 0}})");
|
|
CHECK_FALSE(decode_command(bad_primitive, &error).has_value());
|
|
CHECK(error.find("primitive") != std::string::npos);
|
|
|
|
error.clear();
|
|
const nlohmann::json bad_op = parse_text(R"({"op": "rotate", "id": "1"})");
|
|
CHECK_FALSE(decode_command(bad_op, &error).has_value());
|
|
CHECK(error.find("op") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("Wire : refus d'un champ manquant et d'un mauvais type")
|
|
{
|
|
// Champs manquants.
|
|
for (const char* text : {R"({"id": "1", "visual": {"primitive": "box", "size": [1, 1, 1],
|
|
"color": [1, 1, 1, 1], "semanticClass": 0}})",
|
|
R"({"op": "create", "id": "1"})",
|
|
R"({"op": "create", "id": "1", "transform": {"position": [1, 2, 3],
|
|
"rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}})",
|
|
R"({"op": "camera"})"}) {
|
|
std::string error;
|
|
CHECK_FALSE(decode_command(parse_text(text), &error).has_value());
|
|
CHECK_FALSE(error.empty());
|
|
}
|
|
|
|
// Mauvais types : visual non objet, scale non tableau, semanticClass non entier ou hors 32 bits.
|
|
for (const char* text : {R"({"op": "visual", "id": "1", "visual": 3})",
|
|
R"({"op": "transform", "id": "1", "transform": {"position": [1, 2, 3],
|
|
"rotation": [0, 0, 0, 1], "scale": [1, 1]}})",
|
|
R"({"op": "visual", "id": "1", "visual": {"primitive": "box", "size": [1, 1, 1],
|
|
"color": [1, 1, 1, 1], "semanticClass": 1.5}})",
|
|
R"({"op": "visual", "id": "1", "visual": {"primitive": "box", "size": [1, 1, 1],
|
|
"color": [1, 1, 1, 1], "semanticClass": -1}})",
|
|
R"({"op": "visual", "id": "1", "visual": {"primitive": "box", "size": [1, 1, 1],
|
|
"color": [1, 1, 1, 1], "semanticClass": 4294967296}})"}) {
|
|
CHECK_FALSE(decode_command(parse_text(text)).has_value());
|
|
}
|
|
|
|
// Enveloppe du delta : valeur non hexadécimale, full non booléen, champ manquant.
|
|
for (const char* text : {R"({"worldVersion": 1, "tick": "1", "full": false, "sentAtUs": "2", "commands": []})",
|
|
R"({"worldVersion": "1", "tick": "1", "full": "o", "sentAtUs": "2", "commands": []})",
|
|
R"({"tick": "1", "full": false, "sentAtUs": "2", "commands": []})",
|
|
R"({"worldVersion": "1", "tick": "1", "full": true, "sentAtUs": "2"})"}) {
|
|
std::string error;
|
|
CHECK_FALSE(decode_scene_delta(parse_text(text), &error).has_value());
|
|
CHECK_FALSE(error.empty());
|
|
}
|
|
|
|
// La commande encodée n'est pas un tableau de commandes, pas plus qu'un objet nu.
|
|
CHECK_FALSE(decode_commands(nlohmann::json{"not", "an", "array"}).has_value());
|
|
CHECK_FALSE(decode_command(nlohmann::json::array()).has_value());
|
|
CHECK_FALSE(decode_scene_delta(nlohmann::json::array()).has_value());
|
|
|
|
// Une entrée malveillante malfomée ne lève pas : parse sans exception puis rejet.
|
|
CHECK(parse_text("{ no json").is_discarded());
|
|
CHECK_FALSE(decode_commands(parse_text("{ no json")).has_value());
|
|
}
|
|
|
|
TEST_CASE("Wire : refus d'un tampon inconnu dans les capacités")
|
|
{
|
|
std::string error;
|
|
const nlohmann::json caps = parse_text(
|
|
R"({"engineName": "godot", "engineVersion": "4.3", "buffers": ["color", "normals"],
|
|
"windowed": true, "headless": true})");
|
|
REQUIRE(decode_capabilities(caps, &error).has_value());
|
|
|
|
error.clear();
|
|
const nlohmann::json bad_caps = parse_text(
|
|
R"({"engineName": "godot", "engineVersion": "4.3", "buffers": ["color", "godot_power"],
|
|
"windowed": true, "headless": true})");
|
|
CHECK_FALSE(decode_capabilities(bad_caps, &error).has_value());
|
|
CHECK(error.find("tampon") != std::string::npos);
|
|
|
|
// Mauvais type et champ manquant.
|
|
CHECK_FALSE(decode_capabilities(nlohmann::json{{"engineName", 42}}).has_value());
|
|
CHECK_FALSE(decode_capabilities(nlohmann::json::array()).has_value());
|
|
}
|
|
|
|
TEST_CASE("Wire : playerInput des capacités, facultatif au décodage")
|
|
{
|
|
const nlohmann::json without = parse_text(
|
|
R"({"engineName": "godot", "engineVersion": "4.7", "buffers": [], "windowed": true, "headless": false})");
|
|
const auto decoded = decode_capabilities(without);
|
|
REQUIRE(decoded.has_value());
|
|
CHECK_FALSE(decoded->player_input);
|
|
|
|
EngineCapabilities caps;
|
|
caps.engine_name = "godot";
|
|
caps.player_input = true;
|
|
const auto round_trip = decode_capabilities(encode_capabilities(caps));
|
|
REQUIRE(round_trip.has_value());
|
|
CHECK(round_trip->player_input);
|
|
|
|
nlohmann::json bad = encode_capabilities(caps);
|
|
bad["playerInput"] = 1;
|
|
CHECK_FALSE(decode_capabilities(bad).has_value());
|
|
}
|
|
|
|
TEST_CASE("Wire : render/input fait l'aller-retour, bornes comprises")
|
|
{
|
|
PlayerInputMessage message;
|
|
message.input = PlayerInput{0xFFFFFFFFu, axis_max, -axis_max};
|
|
message.captured_at_us = 0x123456789ULL;
|
|
const nlohmann::json encoded = encode_player_input(message);
|
|
CHECK(encoded["capturedAtUs"] == "0000000123456789");
|
|
std::string error;
|
|
const auto decoded = decode_player_input(encoded, &error);
|
|
REQUIRE_MESSAGE(decoded.has_value(), error);
|
|
CHECK(decoded->input == message.input);
|
|
CHECK(decoded->captured_at_us == message.captured_at_us);
|
|
}
|
|
|
|
TEST_CASE("Wire : refus d'un render/input hors bornes ou mal typé")
|
|
{
|
|
const auto payload = [](nlohmann::json buttons, nlohmann::json x, nlohmann::json y) {
|
|
return nlohmann::json{{"buttons", std::move(buttons)}, {"moveX", std::move(x)}, {"moveY", std::move(y)},
|
|
{"capturedAtUs", "0"}};
|
|
};
|
|
std::string error;
|
|
CHECK(decode_player_input(payload(0, 0, 0)).has_value());
|
|
CHECK_FALSE(decode_player_input(payload(-1, 0, 0), &error).has_value());
|
|
CHECK(error.find("buttons") != std::string::npos);
|
|
CHECK_FALSE(decode_player_input(payload(0x100000000LL, 0, 0)).has_value());
|
|
CHECK_FALSE(decode_player_input(payload(std::numeric_limits<std::uint64_t>::max(), 0, 0)).has_value());
|
|
CHECK_FALSE(decode_player_input(payload(0, 32768, 0)).has_value());
|
|
CHECK_FALSE(decode_player_input(payload(0, 0, -32768)).has_value());
|
|
CHECK_FALSE(decode_player_input(payload(0, 0.5, 0)).has_value());
|
|
CHECK_FALSE(decode_player_input(payload("1", 0, 0)).has_value());
|
|
|
|
nlohmann::json no_time = payload(0, 0, 0);
|
|
no_time.erase("capturedAtUs");
|
|
CHECK_FALSE(decode_player_input(no_time).has_value());
|
|
no_time["capturedAtUs"] = "zz";
|
|
CHECK_FALSE(decode_player_input(no_time).has_value());
|
|
CHECK_FALSE(decode_player_input(nlohmann::json::array()).has_value());
|
|
}
|
|
|
|
TEST_CASE("Wire : un delta décodé s'applique à un SceneState sans erreur")
|
|
{
|
|
SceneDeltaMessage message;
|
|
message.delta.world_version = 4;
|
|
message.delta.commands = {
|
|
SceneCommand{CreateObject{1ULL, sample_transform(), sample_visual(Primitive::plane)}},
|
|
SceneCommand{SetCamera{sample_camera()}},
|
|
};
|
|
message.tick = 2;
|
|
message.full = false;
|
|
message.sent_at_us = 99;
|
|
|
|
std::string error;
|
|
const std::optional<SceneDeltaMessage> decoded = decode_scene_delta(encode_scene_delta(message), &error);
|
|
REQUIRE(decoded.has_value());
|
|
|
|
SceneState state;
|
|
CHECK_FALSE(state.apply(decoded->delta).has_value());
|
|
CHECK(state.world_version() == 4);
|
|
REQUIRE(state.objects().size() == 1);
|
|
const auto it = state.objects().find(1);
|
|
REQUIRE(it != state.objects().end());
|
|
CHECK(it->second == ObjectState{sample_transform(), sample_visual(Primitive::plane)});
|
|
REQUIRE(state.camera().has_value());
|
|
CHECK(*state.camera() == sample_camera());
|
|
}
|
|
|
|
TEST_CASE("Wire : décodage d'un exemple conforme au texte de la spec (protocole 0.1 §5)")
|
|
{
|
|
const char* text =
|
|
R"json({
|
|
"worldVersion": "0000000000000042",
|
|
"tick": "0000000000000017",
|
|
"full": true,
|
|
"sentAtUs": "00000000000296e6",
|
|
"commands": [
|
|
{
|
|
"op": "create",
|
|
"id": "0000000000000001",
|
|
"transform": {
|
|
"position": [1.0, 2.0, 3.0],
|
|
"rotation": [0.0, 0.0, 0.0, 1.0],
|
|
"scale": [1.0, 1.0, 1.0]
|
|
},
|
|
"visual": {
|
|
"primitive": "box",
|
|
"size": [1.0, 1.0, 1.0],
|
|
"color": [0.5, 0.6, 0.7, 1.0],
|
|
"semanticClass": 2
|
|
}
|
|
},
|
|
{
|
|
"op": "camera",
|
|
"camera": {
|
|
"transform": {
|
|
"position": [0.0, 2.0, 5.0],
|
|
"rotation": [0.0, 0.0, 0.0, 1.0],
|
|
"scale": [1.0, 1.0, 1.0]
|
|
},
|
|
"verticalFovDeg": 70.0,
|
|
"near": 0.05,
|
|
"far": 1000.0
|
|
}
|
|
}
|
|
]
|
|
})json";
|
|
|
|
const nlohmann::json payload = parse_text(text);
|
|
REQUIRE(payload.is_object());
|
|
|
|
std::string error;
|
|
const std::optional<SceneDeltaMessage> message = decode_scene_delta(payload, &error);
|
|
REQUIRE(message.has_value());
|
|
CHECK(error.empty());
|
|
CHECK(message->delta.world_version == 0x42ULL);
|
|
CHECK(message->tick == 0x17ULL);
|
|
CHECK(message->full == true);
|
|
CHECK(message->sent_at_us == 0x296e6ULL);
|
|
REQUIRE(message->delta.commands.size() == 2);
|
|
CHECK(message->delta.commands[0] == SceneCommand{CreateObject{1, spec_transform(), spec_visual()}});
|
|
CHECK(message->delta.commands[1] == SceneCommand{SetCamera{spec_camera()}});
|
|
|
|
// Ré-encodage : bytes stables, si ce n'est le formatage du codec.
|
|
nlohmann::json reencoded = encode_scene_delta(*message);
|
|
CHECK(reencoded["commands"] == payload["commands"]);
|
|
}
|