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
644 lines
22 KiB
C++
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
|