Files
SEQUA-Engine/3d/wire/src/wire.cpp
T
Ange-Marie MAURIN - A2M Systèmes 98c778aa28 feat(input): entrees du joueur captees par l'adaptateur (ADR 0011)
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
2026-11-07 20:48:26 +01:00

644 lines
22 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/3d/wire.hpp"
#include <array>
#include <cmath>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
namespace sequa::three_d::wire {
namespace {
using json = nlohmann::json;
constexpr std::array<std::string_view, 4> primitive_names{"box", "sphere", "capsule", "plane"};
// Champ `semanticClass` : entier non négatif tenant sur 32 bits.
constexpr std::uint64_t semantic_class_limit = 0xFFFFFFFFULL;
void refuse(std::string* error, std::string message)
{
if (error != nullptr) {
*error = std::move(message);
}
}
// Nombre JSON fini : refuse les non-nombres comme NaN, infini (représentable
// seulement dans un JSON construit par programme) et 1e999 décodé infini.
[[nodiscard]] bool finite_number(const json& value)
{
if (!value.is_number()) {
return false;
}
return std::isfinite(value.get<double>());
}
// Tableau d'autant de nombres finis que le tampon contient de Cases (3 ou 4),
// convertis en float par cast explicite. `label` nomme le champ dans les erreurs.
bool read_numbers(const json& object, const char* key, std::span<float> values, const char* label, std::string* error)
{
if (!object.contains(key) || !object[key].is_array()) {
refuse(error, std::string(label) + " : tableau de nombres attendu");
return false;
}
const json& array = object[key];
if (array.size() != values.size()) {
refuse(error, std::string(label) + " : tableau de " + std::to_string(values.size()) + " nombres attendu");
return false;
}
for (std::size_t i = 0; i < values.size(); ++i) {
const json& element = array[i];
if (!finite_number(element)) {
refuse(error, std::string(label) + " : nombre fini attendu");
return false;
}
values[i] = static_cast<float>(element.get<double>());
}
return true;
}
[[nodiscard]] json number_array(double a, double b, double c, double d)
{
return json::array({a, b, c, d});
}
[[nodiscard]] json vec3_json(const Vec3& value)
{
return json::array({static_cast<double>(value.x), static_cast<double>(value.y), static_cast<double>(value.z)});
}
[[nodiscard]] json quat_json(const Quat& value)
{
return number_array(static_cast<double>(value.x), static_cast<double>(value.y), static_cast<double>(value.z),
static_cast<double>(value.w));
}
[[nodiscard]] json color_json(const Color& value)
{
return number_array(static_cast<double>(value.r), static_cast<double>(value.g), static_cast<double>(value.b),
static_cast<double>(value.a));
}
[[nodiscard]] std::optional<Transform> decode_transform(const json& value, std::string* error)
{
if (!value.is_object()) {
refuse(error, "transform : objet attendu");
return std::nullopt;
}
float position[3];
float rotation[4];
float scale[3];
if (!read_numbers(value, "position", position, "position", error)) {
return std::nullopt;
}
if (!read_numbers(value, "rotation", rotation, "rotation", error)) {
return std::nullopt;
}
if (!read_numbers(value, "scale", scale, "scale", error)) {
return std::nullopt;
}
Transform transform;
transform.position = {position[0], position[1], position[2]};
transform.rotation = {rotation[0], rotation[1], rotation[2], rotation[3]};
transform.scale = {scale[0], scale[1], scale[2]};
return transform;
}
[[nodiscard]] json encode_transform(const Transform& transform)
{
json out;
out["position"] = vec3_json(transform.position);
out["rotation"] = quat_json(transform.rotation);
out["scale"] = vec3_json(transform.scale);
return out;
}
[[nodiscard]] std::optional<Visual> decode_visual(const json& value, std::string* error)
{
if (!value.is_object()) {
refuse(error, "visual : objet attendu");
return std::nullopt;
}
if (!value.contains("primitive") || !value["primitive"].is_string()) {
refuse(error, "primitive : chaîne attendue");
return std::nullopt;
}
const std::string primitive = value["primitive"].get<std::string>();
std::optional<Primitive> found;
for (std::size_t i = 0; i < primitive_names.size(); ++i) {
if (primitive == primitive_names[i]) {
found = static_cast<Primitive>(i);
}
}
if (!found.has_value()) {
refuse(error, "primitive inconnue « " + primitive + " »");
return std::nullopt;
}
float size[3];
float color[4];
if (!read_numbers(value, "size", size, "size", error)) {
return std::nullopt;
}
if (!read_numbers(value, "color", color, "color", error)) {
return std::nullopt;
}
if (!value.contains("semanticClass")) {
refuse(error, "semanticClass manquant");
return std::nullopt;
}
const json& field = value["semanticClass"];
if (field.is_number_unsigned()) {
const std::uint64_t raw = field.get<std::uint64_t>();
if (raw > semantic_class_limit) {
refuse(error, "semanticClass : entier non négatif sur 32 bits attendu");
return std::nullopt;
}
} else if (field.is_number_integer()) {
const std::int64_t raw = field.get<std::int64_t>();
if (raw < 0 || raw > static_cast<std::int64_t>(semantic_class_limit)) {
refuse(error, "semanticClass : entier non négatif sur 32 bits attendu");
return std::nullopt;
}
} else {
refuse(error, "semanticClass : entier attendu");
return std::nullopt;
}
Visual visual;
visual.primitive = *found;
visual.size = {size[0], size[1], size[2]};
visual.color = {color[0], color[1], color[2], color[3]};
visual.semantic_class = static_cast<std::uint32_t>(field.get<std::uint64_t>());
return visual;
}
[[nodiscard]] json encode_visual(const Visual& visual)
{
const std::size_t index = static_cast<std::size_t>(visual.primitive);
json out;
out["primitive"] = std::string(index < primitive_names.size() ? primitive_names[index] : "unknown");
out["size"] = vec3_json(visual.size);
out["color"] = color_json(visual.color);
out["semanticClass"] = visual.semantic_class;
return out;
}
[[nodiscard]] std::optional<Camera> decode_camera(const json& value, std::string* error)
{
if (!value.is_object()) {
refuse(error, "camera : objet attendu");
return std::nullopt;
}
if (!value.contains("transform")) {
refuse(error, "transform manquant");
return std::nullopt;
}
std::optional<Transform> transform = decode_transform(value["transform"], error);
if (!transform.has_value()) {
return std::nullopt;
}
Camera camera;
camera.transform = *transform;
for (const char* key : {"verticalFovDeg", "near", "far"}) {
if (!value.contains(key) || !finite_number(value[key])) {
refuse(error, std::string(key) + " : nombre fini attendu");
return std::nullopt;
}
}
camera.vertical_fov_deg = static_cast<float>(value["verticalFovDeg"].get<double>());
camera.near_plane = static_cast<float>(value["near"].get<double>());
camera.far_plane = static_cast<float>(value["far"].get<double>());
return camera;
}
[[nodiscard]] json encode_camera(const Camera& camera)
{
json out;
out["transform"] = encode_transform(camera.transform);
out["verticalFovDeg"] = static_cast<double>(camera.vertical_fov_deg);
out["near"] = static_cast<double>(camera.near_plane);
out["far"] = static_cast<double>(camera.far_plane);
return out;
}
// Champ `id` : chaîne hexadécimale non nulle.
[[nodiscard]] std::optional<ObjectId> decode_id(const json& object, std::string* error)
{
if (!object.contains("id") || !object["id"].is_string()) {
refuse(error, "id : chaîne hexadécimale attendue");
return std::nullopt;
}
const std::optional<std::uint64_t> id = from_hex64(object["id"].get<std::string>());
if (!id.has_value()) {
refuse(error, "id : hexadécimal invalide");
return std::nullopt;
}
if (*id == invalid_object) {
refuse(error, "id 0 réservé");
return std::nullopt;
}
return id;
}
} // namespace
std::string to_hex64(std::uint64_t value)
{
std::string out(16, '0');
for (std::size_t i = 16; i-- > 0;) {
out[i] = "0123456789abcdef"[value & 0xFU];
value >>= 4U;
}
return out;
}
std::optional<std::uint64_t> from_hex64(std::string_view text)
{
if (text.empty() || text.size() > 16U) {
return std::nullopt;
}
std::uint64_t value = 0;
for (const char character : text) {
std::uint64_t nibble = 0;
if (character >= '0' && character <= '9') {
nibble = static_cast<std::uint64_t>(character - '0');
} else if (character >= 'a' && character <= 'f') {
nibble = static_cast<std::uint64_t>(character - 'a' + 10);
} else if (character >= 'A' && character <= 'F') {
nibble = static_cast<std::uint64_t>(character - 'A' + 10);
} else {
return std::nullopt;
}
value = (value << 4U) | nibble;
}
return value;
}
nlohmann::json encode_command(const SceneCommand& command)
{
return std::visit(
[](const auto& object) -> nlohmann::json {
using T = std::decay_t<decltype(object)>;
json out;
if constexpr (std::is_same_v<T, CreateObject>) {
out["op"] = "create";
out["id"] = to_hex64(object.id);
out["transform"] = encode_transform(object.transform);
out["visual"] = encode_visual(object.visual);
} else if constexpr (std::is_same_v<T, DestroyObject>) {
out["op"] = "destroy";
out["id"] = to_hex64(object.id);
} else if constexpr (std::is_same_v<T, SetTransform>) {
out["op"] = "transform";
out["id"] = to_hex64(object.id);
out["transform"] = encode_transform(object.transform);
} else if constexpr (std::is_same_v<T, SetVisual>) {
out["op"] = "visual";
out["id"] = to_hex64(object.id);
out["visual"] = encode_visual(object.visual);
} else {
static_assert(std::is_same_v<T, SetCamera>, "commande de scène inattendue");
out["op"] = "camera";
out["camera"] = encode_camera(object.camera);
}
return out;
},
command);
}
nlohmann::json encode_commands(const std::vector<SceneCommand>& commands)
{
json out = json::array();
for (const SceneCommand& command : commands) {
out.push_back(encode_command(command));
}
return out;
}
std::optional<SceneCommand> decode_command(const nlohmann::json& value, std::string* error)
{
if (!value.is_object()) {
refuse(error, "commande : objet attendu");
return std::nullopt;
}
if (!value.contains("op") || !value["op"].is_string()) {
refuse(error, "op : chaîne attendue");
return std::nullopt;
}
const std::string op = value["op"].get<std::string>();
if (op == "destroy") {
const std::optional<ObjectId> id = decode_id(value, error);
if (!id.has_value()) {
return std::nullopt;
}
return SceneCommand{DestroyObject{*id}};
}
if (op == "create") {
const std::optional<ObjectId> id = decode_id(value, error);
if (!id.has_value()) {
return std::nullopt;
}
if (!value.contains("transform")) {
refuse(error, "transform manquant");
return std::nullopt;
}
if (!value.contains("visual")) {
refuse(error, "visual manquant");
return std::nullopt;
}
std::optional<Transform> transform = decode_transform(value["transform"], error);
if (!transform.has_value()) {
return std::nullopt;
}
std::optional<Visual> visual = decode_visual(value["visual"], error);
if (!visual.has_value()) {
return std::nullopt;
}
return SceneCommand{CreateObject{*id, *transform, *visual}};
}
if (op == "transform") {
const std::optional<ObjectId> id = decode_id(value, error);
if (!id.has_value()) {
return std::nullopt;
}
if (!value.contains("transform")) {
refuse(error, "transform manquant");
return std::nullopt;
}
std::optional<Transform> transform = decode_transform(value["transform"], error);
if (!transform.has_value()) {
return std::nullopt;
}
return SceneCommand{SetTransform{*id, *transform}};
}
if (op == "visual") {
const std::optional<ObjectId> id = decode_id(value, error);
if (!id.has_value()) {
return std::nullopt;
}
if (!value.contains("visual")) {
refuse(error, "visual manquant");
return std::nullopt;
}
std::optional<Visual> visual = decode_visual(value["visual"], error);
if (!visual.has_value()) {
return std::nullopt;
}
return SceneCommand{SetVisual{*id, *visual}};
}
if (op == "camera") {
if (!value.contains("camera")) {
refuse(error, "camera manquant");
return std::nullopt;
}
std::optional<Camera> camera = decode_camera(value["camera"], error);
if (!camera.has_value()) {
return std::nullopt;
}
return SceneCommand{SetCamera{*camera}};
}
refuse(error, "op inconnu « " + op + " »");
return std::nullopt;
}
std::optional<std::vector<SceneCommand>> decode_commands(const nlohmann::json& value, std::string* error)
{
if (!value.is_array()) {
refuse(error, "commandes : tableau attendu");
return std::nullopt;
}
std::vector<SceneCommand> commands;
commands.reserve(value.size());
for (std::size_t i = 0; i < value.size(); ++i) {
std::string reason;
std::optional<SceneCommand> command = decode_command(value[i], &reason);
if (!command.has_value()) {
refuse(error, "commande " + std::to_string(i) + " : " + reason);
return std::nullopt;
}
commands.push_back(*std::move(command));
}
return commands;
}
nlohmann::json encode_scene_delta(const SceneDeltaMessage& message)
{
json out;
out["worldVersion"] = to_hex64(message.delta.world_version);
out["tick"] = to_hex64(message.tick);
out["full"] = message.full;
out["sentAtUs"] = to_hex64(message.sent_at_us);
out["commands"] = encode_commands(message.delta.commands);
return out;
}
std::optional<SceneDeltaMessage> decode_scene_delta(const nlohmann::json& payload, std::string* error)
{
if (!payload.is_object()) {
refuse(error, "scene/delta : objet attendu");
return std::nullopt;
}
SceneDeltaMessage message;
for (const char* key : {"worldVersion", "tick", "sentAtUs"}) {
if (!payload.contains(key) || !payload[key].is_string()) {
refuse(error, std::string(key) + " : chaîne hexadécimale attendue");
return std::nullopt;
}
const std::optional<std::uint64_t> value = from_hex64(payload[key].get<std::string>());
if (!value.has_value()) {
refuse(error, std::string(key) + " : hexadécimal invalide");
return std::nullopt;
}
if (key == std::string_view("worldVersion")) {
message.delta.world_version = *value;
} else if (key == std::string_view("tick")) {
message.tick = *value;
} else {
message.sent_at_us = *value;
}
}
if (!payload.contains("full") || !payload["full"].is_boolean()) {
refuse(error, "full : booléen attendu");
return std::nullopt;
}
message.full = payload["full"].get<bool>();
if (!payload.contains("commands") || !payload["commands"].is_array()) {
refuse(error, "commands : tableau attendu");
return std::nullopt;
}
std::optional<std::vector<SceneCommand>> commands = decode_commands(payload["commands"], error);
if (!commands.has_value()) {
return std::nullopt;
}
message.delta.commands = *std::move(commands);
return message;
}
nlohmann::json encode_capabilities(const EngineCapabilities& caps)
{
json out;
out["engineName"] = caps.engine_name;
out["engineVersion"] = caps.engine_version;
json buffers = json::array();
for (std::uint32_t index = 0; index < frame_buffer_count; ++index) {
const auto buffer = static_cast<FrameBuffer>(index);
if (caps.buffers.contains(buffer)) {
buffers.push_back(to_string(buffer));
}
}
out["buffers"] = std::move(buffers);
out["windowed"] = caps.windowed;
out["headless"] = caps.headless;
out["playerInput"] = caps.player_input;
return out;
}
std::optional<EngineCapabilities> decode_capabilities(const nlohmann::json& value, std::string* error)
{
if (!value.is_object()) {
refuse(error, "capacités : objet attendu");
return std::nullopt;
}
EngineCapabilities caps;
for (const char* key : {"engineName", "engineVersion"}) {
if (!value.contains(key) || !value[key].is_string()) {
refuse(error, std::string(key) + " : chaîne attendue");
return std::nullopt;
}
}
caps.engine_name = value["engineName"].get<std::string>();
caps.engine_version = value["engineVersion"].get<std::string>();
if (!value.contains("buffers") || !value["buffers"].is_array()) {
refuse(error, "buffers : tableau de noms attendu");
return std::nullopt;
}
const json& buffers = value["buffers"];
BufferSet set;
for (std::size_t i = 0; i < buffers.size(); ++i) {
const json& element = buffers[i];
if (!element.is_string()) {
refuse(error, "buffers : nom de tampon attendu");
return std::nullopt;
}
const std::string name = element.get<std::string>();
bool found = false;
for (std::uint32_t index = 0; !found && index < frame_buffer_count; ++index) {
const auto buffer = static_cast<FrameBuffer>(index);
if (name == to_string(buffer)) {
set = set | BufferSet{buffer};
found = true;
}
}
if (!found) {
refuse(error, "tampon inconnu « " + name + " »");
return std::nullopt;
}
}
caps.buffers = set;
for (const char* key : {"windowed", "headless"}) {
if (!value.contains(key) || !value[key].is_boolean()) {
refuse(error, std::string(key) + " : booléen attendu");
return std::nullopt;
}
}
caps.windowed = value["windowed"].get<bool>();
caps.headless = value["headless"].get<bool>();
if (value.contains("playerInput")) {
if (!value["playerInput"].is_boolean()) {
refuse(error, "playerInput : booléen attendu");
return std::nullopt;
}
caps.player_input = value["playerInput"].get<bool>();
}
return caps;
}
nlohmann::json encode_player_input(const PlayerInputMessage& message)
{
json out;
out["buttons"] = message.input.buttons;
out["moveX"] = message.input.move_x;
out["moveY"] = message.input.move_y;
out["capturedAtUs"] = to_hex64(message.captured_at_us);
return out;
}
std::optional<PlayerInputMessage> decode_player_input(const nlohmann::json& payload, std::string* error)
{
if (!payload.is_object()) {
refuse(error, "render/input : objet attendu");
return std::nullopt;
}
// Entier JSON dans [low, high] (les flottants, même entiers, sont refusés).
const auto integer = [&](const char* key, std::int64_t low, std::int64_t high) -> std::optional<std::int64_t> {
if (!payload.contains(key)) {
refuse(error, std::string(key) + " manquant");
return std::nullopt;
}
const json& field = payload[key];
std::int64_t raw = 0;
if (field.is_number_unsigned()) {
const std::uint64_t value = field.get<std::uint64_t>();
raw = value > static_cast<std::uint64_t>(high) ? high + 1 : static_cast<std::int64_t>(value);
} else if (field.is_number_integer()) {
raw = field.get<std::int64_t>();
} else {
refuse(error, std::string(key) + " : entier attendu");
return std::nullopt;
}
if (raw < low || raw > high) {
refuse(error, std::string(key) + " : hors de [" + std::to_string(low) + ", " + std::to_string(high) + "]");
return std::nullopt;
}
return raw;
};
const auto buttons = integer("buttons", 0, 0xFFFFFFFFLL);
if (!buttons) {
return std::nullopt;
}
const auto move_x = integer("moveX", -axis_max, axis_max);
if (!move_x) {
return std::nullopt;
}
const auto move_y = integer("moveY", -axis_max, axis_max);
if (!move_y) {
return std::nullopt;
}
if (!payload.contains("capturedAtUs") || !payload["capturedAtUs"].is_string()) {
refuse(error, "capturedAtUs : chaîne hexadécimale attendue");
return std::nullopt;
}
const std::optional<std::uint64_t> captured = from_hex64(payload["capturedAtUs"].get<std::string>());
if (!captured) {
refuse(error, "capturedAtUs : hexadécimal invalide");
return std::nullopt;
}
PlayerInputMessage message;
message.input.buttons = static_cast<std::uint32_t>(*buttons);
message.input.move_x = static_cast<std::int16_t>(*move_x);
message.input.move_y = static_cast<std::int16_t>(*move_y);
message.captured_at_us = *captured;
return message;
}
} // namespace sequa::three_d::wire