Files
SEQUA-Engine/docs/specs/protocole-studio-core-0.2.md
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

14 KiB
Raw Permalink Blame History

Spécification : protocole Studio - Core 0.2 et Studio host

  • Statut : référence de développement (squelette). Décisions : ADR 0006.
  • Le format reste du JSON provisoire (ADR 0005) en attendant l'ADR du schéma.
  • Toute ambiguïté se règle en modifiant CE document, pas en interprétant chacun de son côté.

1. Acteurs

Acteur Exécutable / dossier Rôle
Core sequa-core (core/) Serveur WebSocket, session de simulation en direct, enregistrement, rejeu
Host sequa-studio (studio/host/) Lance ou rejoint le Core, le supervise, le recharge par rejeu, sert l'interface
UI studio/ui/ (navigateur) Interface ; client WebSocket du Core

Ports par défaut : Core 9100, interface servie par le host 9180, faux Core de développement 9101, Vite 5173. Écoute sur 127.0.0.1 par défaut partout.

2. Enveloppe et versions

  • Enveloppe : { "v": 1, "type": "<type>", "id": "<chaîne>"?, "payload": { ... } }. v reste 1.
  • id (facultatif) : identifiant de requête choisi par le client. Toute réponse à une requête qui portait un id recopie cet id. Une requête sans id reçoit quand même sa réponse, sans id. hello est envoyé sans id (le host et l'UI le font ainsi) ; welcome et les erreurs de poignée de main n'en portent pas.
  • Version du protocole : 0.3.0 (0.3 ajoute les messages du rendu, §9 ; le reste est inchangé depuis 0.2). Compatibilité inchangée : même majeure et, en 0.x, même mineure. 0.1 et 0.2 sont donc incompatibles.
  • payload est toujours un objet (éventuellement vide {}).
  • Les entiers 64 bits (empreintes) sont transmis en chaîne hexadécimale de 16 caractères minuscules, jamais en nombre JSON (perte de précision en JavaScript au-delà de 2^53).
  • Les données binaires (enregistrements, tableaux d'empreintes) sont en base64 standard (RFC 4648, avec remplissage =).

3. Connexion, authentification, origine

À l'ouverture d'une connexion (avant tout message), le Core applique dans cet ordre :

  1. Jeton : si le Core a été lancé avec --token <t>, l'URL de connexion doit contenir le paramètre de requête token=<t> (ex. ws://127.0.0.1:9100/?token=ab12...). Comparaison en temps constant. Sinon : message error { code: "unauthorized" } puis fermeture 1008.
  2. Origine : si l'en-tête HTTP Origin est présent (connexion venant d'un navigateur), il doit figurer dans la liste autorisée :
    • les valeurs passées par --allowed-origin <origine> (répétable), comparées exactement (schéma, hôte, port ; ex. http://127.0.0.1:9180) ;
    • si aucune n'est passée : toute origine http://localhost:<port> ou http://127.0.0.1:<port>. Sinon : error { code: "forbidden-origin" } puis fermeture 1008. Une connexion sans en-tête Origin, ou avec une origine ws://… / wss://… (client natif : IXWebSocket, donc le host, envoie de lui-même Origin: ws://hôte:port ; un navigateur n'envoie jamais ce schéma), est acceptée, sous réserve du jeton.

Puis la poignée de main (inchangée sur le principe) : le premier message doit être hello.

  • hello : { protocolVersion, client } avec client ∈ "sequa-studio" (UI) | "sequa-studio-host" (host).
  • welcome : { protocolVersion, coreVersion, instanceId, pid, linked }
    • instanceId : 32 caractères hexadécimaux aléatoires, tirés au démarrage du Core (identifie CETTE exécution) ;
    • pid : entier, PID du processus Core ;
    • linked : booléen, état du bail (§5).
  • Erreurs de poignée de main : comme en 0.1 (invalid-json, invalid-envelope, unsupported-version, invalid-payload, handshake-required, incompatible-protocol), suivies d'une fermeture.

Après la poignée de main, un message de type inconnu reçoit error { code: "unknown-type" } sans fermeture. Une requête dont le payload est invalide reçoit error { code: "invalid-payload" } sans fermeture.

4. Messages après la poignée de main

Chaque requête reçoit une réponse du même type (avec l'id recopié), ou un error (avec l'id recopié). Payloads :

Requête Payload envoyé Payload de la réponse
core/ping {} {}
core/status {} { coreVersion, protocolVersion, instanceId, pid, uptimeMs, linked, controllerAttached, clients }
core/attach { force?: bool } { linked: true } — la connexion devient contrôleur (§5). S'il existe déjà un contrôleur connecté : error controller-exists, sauf force: true (l'ancien perd le rôle).
core/detach {} { linked: false } — lève le bail (§5). Réservé au contrôleur, sinon error not-controller.
core/shutdown {} {} envoyé, puis arrêt propre du Core (code de sortie 0) en moins de 2 s.
session/start { seed: string } état de session (ci-dessous). seed : entier 64 bits en hexadécimal (1 à 16 caractères). Remplace la session courante, tick 0, en marche.
session/pause {} état de session
session/resume {} état de session
session/status {} état de session
session/input { buttons: int, moveX: int, moveY: int } { tick } : tick auquel l'entrée s'appliquera. buttons ∈ [0, 2^32-1], moveX/moveY ∈ [-32767, 32767]. L'entrée vaut pour le prochain tick exécuté ; plusieurs entrées avant ce tick : la dernière gagne. Elle reste ensuite active (entrée maintenue) jusqu'à la suivante.
session/export { includeHashes?: bool } { recording, tick, stateHash, hashes? } : recording = base64 de sim::serialize(Recording) (format binaire « SQRC » existant) ; hashes (si demandé) = base64 du tableau des empreintes après chaque tick, uint64 petit-boutiste, tick éléments.
session/import { recording, resume: bool, includeHashes?: bool } état de session + { replayMs, hashes? }. Désérialise, rejoue au plus vite (pas en temps réel) tous les ticks enregistrés avec les réglages courants du Core, puis la session continue depuis là ; resume choisit marche ou pause. Enregistrement illisible : error invalid-recording, la session courante est conservée.

État de session : { seed, tickRateHz, tick, stateHash, running, inputCount } — seed et stateHash en hexadécimal (§2), inputCount = nombre d'entrées non neutres enregistrées.

Le Core n'envoie aucun message spontané en 0.2.

5. Session en direct et bail

  • Le Core possède une session en direct : une sim::Simulation avec son SessionRecorder, avancée par un fil dédié à la fréquence fixe (60 Hz) via sim::FixedStepClock. Au démarrage : graine --seed (défaut 0), en marche.
  • Toutes les opérations de session sont sérialisées (verrou) : le fil de simulation et les connexions ne se marchent pas dessus. Les entrées de session/input sont enregistrées avec le tick auquel elles s'appliquent : le rejeu est exact.
  • Bail (mode lié) : lancé avec --linked, le Core exige un contrôleur.
    • Si aucun contrôleur ne s'attache dans les 10 s suivant le démarrage, il s'arrête (code 3).
    • Si le contrôleur se déconnecte, il s'arrête après un délai de grâce --lease-grace-ms (défaut 3000) sauf si un contrôleur s'attache entre-temps.
    • core/detach lève le bail : le Core continue sans contrôleur. core/attach (depuis n'importe quelle connexion authentifiée) le rétablit.
    • Sans --linked, le Core n'a pas de bail au démarrage (usage autonome) ; core/attach l'active quand même.

6. Options du Core (sequa-core)

--host <adresse> (défaut 127.0.0.1), --port <n> (9100), --token <hex>, --allowed-origin <origine> (répétable), --linked, --lease-grace-ms <n>, --seed <hex>, --help. Codes de sortie : 0 arrêt normal, 1 erreur de démarrage, 2 arguments invalides, 3 bail expiré.

7. Studio host (sequa-studio)

7.1 Options

Option Effet
--core-exe <chemin> Exécutable source du Core (défaut : core/sequa-core[.exe] depuis la racine sequa-root.json)
--core-port <n> 9100
--ui-port <n> 9180
--ui-dir <chemin> Dossier de l'interface construite (défaut : studio/ui depuis la racine sequa-root.json)
--dev [url] Ne sert pas l'interface ; ouvre le navigateur sur le serveur Vite (défaut http://localhost:5173)
--detach-core Lance le Core puis lève le bail (core/detach) : il survivra au host
--no-browser N'ouvre pas le navigateur
--core-console Laisse le Core dans sa propre console au lieu de capturer sa sortie
--watch Recharge automatiquement le Core quand son exécutable source change (sinon : simple avertissement)

7.2 Démarrage ou rattachement

  1. Lire le fichier d'exécution du port (§7.4). S'il existe : se connecter avec son jeton, hello ; si welcome.instanceId == celui du fichier → rattachement (core/attach). Sinon le fichier est périmé : le supprimer.
  2. Sinon : copie fantôme (§7.5), lancement du Core avec --port, --token <nouveau jeton aléatoire 32 hex>, --allowed-origin http://127.0.0.1:<ui-port> et http://localhost:<ui-port> (et l'origine Vite en --dev), --linked ; attendre que le port accepte les connexions (10 s max), hello, core/attach, écrire le fichier d'exécution.
  3. Servir l'interface (sauf --dev) et ouvrir le navigateur sur http://127.0.0.1:<ui-port>/?core=<url du Core encodée, jeton inclus>.

7.3 Commandes (console du host, une par ligne)

status, restart, reload, detach, attach, log (20 dernières lignes du Core), quit (arrête le host ; arrête le Core s'il est lié), help.

7.4 Fichier d'exécution

Chemin : Windows %LOCALAPPDATA%\SEQUA\run\core-<port>.json ; Linux $XDG_RUNTIME_DIR/sequa/core-<port>.json (à défaut ~/.local/state/sequa/run/) ; macOS ~/Library/Application Support/SEQUA/run/. Contenu :

{ "pid": 1234, "host": "127.0.0.1", "port": 9100, "token": "…32 hex…", "instanceId": "…32 hex…",
  "exePath": "…copie fantôme…", "sourceExePath": "…build/…/sequa-core.exe…", "startedAt": "2026-11-06T21:00:00Z" }

Écrit par le host après le premier welcome, supprimé quand le Core s'arrête par le host. Contient un secret (jeton) : droits limités à l'utilisateur.

7.5 Copie fantôme

Le Core est lancé depuis <run>/bin/sequa-core-<horodatage UTC compact>-<pid du host>[.exe], copie de l'exécutable source. Au démarrage, le host supprime les copies qui ne sont plus utilisées (suppression qui échoue = copie encore utilisée : ignorer). La journalisation du Core capturée va dans <run>/core-<port>.log (ajout, avec horodatage de démarrage).

7.6 Rechargement (reload, ou --watch quand l'exécutable source change et reste stable 1 s)

  1. session/export { includeHashes: true } sur l'ancien Core → recording, tick, stateHash, hashes ; noter running.
  2. core/shutdown ; attendre la fin du processus (3 s), sinon terminaison forcée.
  3. Nouvelle copie fantôme du nouvel exécutable source, lancement (mêmes port et jeton), hello, core/attach.
  4. session/import { recording, resume: <running>, includeHashes: true }.
  5. Comparer les empreintes tick par tick : afficher « identique » ou « divergence au tick N », ainsi que replayMs. Si les deux tableaux n'ont pas la même longueur, la divergence est au premier tick absent de l'un des deux.
  6. Mettre à jour le fichier d'exécution (nouveau pid, instanceId, exePath).

restart fait la même chose avec le même exécutable, sans comparaison.

8. Ce qui reste hors 0.3

Messages spontanés du Core (événements), API du host pour l'interface (panneau « Core »), webview, écoute hors poste local.

9. Rendu (ajouté en 0.3)

Studio ne parle jamais au processus de rendu : il demande au Core de le démarrer ou de l'arrêter (ADR 0009, protocole Core - rendu). Requêtes ordinaires (§4) : réponse du même type, id recopié.

Requête Payload envoyé Réponse
render/start { adapter: "godot" | "debug", width?: int, height?: int, headless?: bool, fps?: int } état du rendu. Erreurs : render-running (déjà lancé), render-unavailable (sequa-render ou bibliothèque d'adaptateur introuvable, lancement impossible), invalid-payload
render/stop {} état du rendu (stopped). Sans erreur si le rendu est déjà arrêté
render/status {} état du rendu

Bornes : width et height dans [64, 7680] (défaut 1280 × 720), fps dans [0, 240] (0 : cadence du moteur). render/start répond dès le lancement du processus (starting) ; Studio interroge ensuite render/status (une fois par seconde suffit) pour voir running.

État du rendu :

{
  "state": "stopped" | "starting" | "running" | "restarting",
  "adapter": "godot",                 // absent si stopped
  "pid": 1234,                        // absent si aucun processus
  "restarts": 0,
  "engine": { "name": "godot", "version": "4.7.2-stable", "buffers": ["color", "depth"], "windowed": true, "headless": false, "playerInput": true },
  "width": 1280, "height": 720,       // absents tant que le rendu n'est pas prêt
  "measurements": { "framesPerSecond": 180, "cpuMsP95": 6.1, "latencyUsP95": 340, "applyUsP95": 5900 },
  "worldVersion": "00000000000001d5", // dernière version de scène envoyée
  "lastFailure": "adapter-start : …"  // absent s'il n'y en a pas eu
}

starting : processus lancé, render/ready pas encore reçu ; running : prêt ; restarting : plantage, relance programmée (protocole Core - rendu §7). engine, width et height viennent de render/ready ; measurements du dernier render/frames.