# SB1-SPEC — Registre de mods et barre latérale (modes enveloppés) - **Fiche** : SB1-T1 — **Agent** : A-ARCH — **Modèle** : prévu opus (M4) | utilisé opus - **Dépend de** : SB0 (`D:\Game\LifePlay-Sandbox\Docs\SB0-conception.md`, commit `3b21fd1`, tag `lot-SB0-valide`) - **Dépôt** : addon `D:\Game\LifePlay-Sandbox`, branche `lot/SB1-registre` créée depuis `lot-SB0-valide`, tag `lot-SB1-avant` - **Principe SB1** : aucun code de mode n'est déplacé. Les trois modes actuels deviennent trois mods **logiques** (descripteur + enveloppe qui appelle `SelectMode` existant). La liste déroulante des modes est remplacée par la barre latérale. Comportement de chaque mode strictement inchangé. ## 1. Fichiers | Fichier (sous `Plugins/LPSandbox/Source/LPSandbox/`) | Fiche | Contenu | |---|---|---| | `LPSandboxMod.h` (nouveau) | T2 | descripteur, états, contexte, interface `ILPSandboxMod` | | `LPSandboxModRegistry.h/.cpp` (nouveaux) | T2 | registre (§ 3), persistance (§ 4), journal | | `LPSandboxModWrappers.cpp` (nouveau) | T2 | 3 enveloppes + enregistrement (§ 5) | | `LPSandbox.cpp` | T2 | `StartupModule` : enregistrement des fabriques, `Scan()` | | `LPPairSandbox.h/.cpp` | T2 | `OpenModeLabel(const FString&)` public ; choix du mod initial (§ 6) ; emplacement de la barre (§ 7) | | `LPSandbox.Build.cs` | T2 | `"Projects"` en dépendance privée (`IPluginManager`) | | `LPSandboxSidebar.h/.cpp` (nouveaux) | T3 | widget Slate `SLPSandboxSidebar` (§ 7) | | `Plugins/LPSandbox/Modules/**/.lpmod` (nouveaux) | T3 | 7 descripteurs (§ 8) | ## 2. Types (`LPSandboxMod.h`) ```cpp #pragma once #include "CoreMinimal.h" class SWidget; class ALPPairSandboxCharacter; class ALPPairSandboxStage; enum class ELPSandboxModKind : uint8 { System, Library, Mod, Data }; // Data = .lpmod du jeu, lecture seule enum class ELPSandboxModSource : uint8 { Sandbox, Game }; enum class ELPSandboxModState : uint8 { Ready, // utilisable (mod activé, prérequis satisfaits) Disabled, // case décochée Missing, // prérequis absent ou désactivé (Reason renseignée) Refused, // règle violée : doublon, mod requis par un mod, cycle (Reason renseignée) NoFactory, // descripteur de la sandbox sans code enregistré DataOnly // descripteur du jeu }; struct FLPSandboxModDescriptor { FString Id; // MODULE_UNIQUEID FString Name; // MODULE_NAME FString Author; // MODULE_AUTHOR FString Description, Version, License, Link, AuthorLink; TArray Requirements; // MODULE_REQUIREMENTS, séparés par ',' ; espaces retirés ; vides ignorés ELPSandboxModKind Kind = ELPSandboxModKind::Mod; ELPSandboxModSource Source = ELPSandboxModSource::Sandbox; bool bSystem = false, bHidden = false, bLocked = false, bDefaultEnabled = false; int32 Index = -1; // index hexadécimal du dossier (0001_…), -1 sans index FString Folder; // chemin complet du dossier FString File; // chemin complet du .lpmod TArray Warnings; // lecture : clé manquante, UNIQUEID ≠ nom du fichier, 3 premières lignes dans le désordre… TMap Keys; // toutes les clés lues (majuscules) }; struct FLPSandboxContext { ALPPairSandboxCharacter* Player = nullptr; // SB1 : les enveloppes passent par le personnage joueur ALPPairSandboxStage* Stage = nullptr; }; class ILPSandboxMod { public: virtual ~ILPSandboxMod() = default; virtual FString GetId() const = 0; virtual void Activate(const FLPSandboxContext& Context) = 0; // le mod prend la scène virtual void Deactivate(const FLPSandboxContext& Context) = 0; // il la rend (SB1 : rien, SelectMode gère la transition) virtual TSharedPtr BuildPanel() { return nullptr; } // SB1 : nul (les rangées actuelles du menu restent) virtual void Tick(float DeltaSeconds) {} }; ``` ## 3. Registre (`FLPSandboxModRegistry`, singleton `Get()`) ### 3.1 API (figée : la barre latérale de T3 n'utilise que celle-ci) ```cpp struct FLPSandboxModEntry { FLPSandboxModDescriptor Desc; ELPSandboxModState State = ELPSandboxModState::Disabled; FString Reason; // texte affichable (infobulle ⚠) bool bEnabled = false; bool bOpen = false; }; class LPSANDBOX_API FLPSandboxModRegistry { public: static FLPSandboxModRegistry& Get(); void RegisterFactory(const FString& Id, TFunction()> Factory); void Scan(); // découverte + lecture + contrôles + état persisté (§ 3.2-3.4) const TArray& GetEntries() const; // ordre d'affichage : système/bibliothèques, mods, jeu const FLPSandboxModEntry* Find(const FString& Id) const; bool SetEnabled(const FString& Id, bool bEnabled); // refus si verrouillé ou Data ; recalcul des états ; Save() bool Move(const FString& Id, int32 Delta); // mods seulement (-1 monte, +1 descend) ; Save() bool Open(const FString& Id, const FLPSandboxContext& Context); // Ready + activé exigé ; Deactivate du mod ouvert, Activate du nouveau ; Save() FString GetOpenId() const; FString IdForModeLabel(const FString& Label) const; // "Pair"→lps_couple, "Interaction"→lps_interaction, "Mesure"→lps_asset_integration FSimpleMulticastDelegate OnChanged; // diffusé après tout changement }; ``` ### 3.2 Découverte - Sandbox : `IPluginManager::Get().FindPlugin(TEXT("LPSandbox"))->GetBaseDir()` + `/Modules/System/*/` et `/Modules/Mods/*/` ; dans chaque dossier, le premier `*.lpmod`. - Jeu (lecture seule) : `FPaths::ProjectContentDir() / TEXT("Modules")` ; `/.lpmod` (même règle que `ULPModInfoSubsystem` du jeu). Source = Game, Kind = Data, état DataOnly. Absent (projet `LPSandbox.uproject` seul) : section Jeu vide, sans erreur. - Index : si le nom du dossier commence par 4 chiffres hexadécimaux suivis de `_`, `Index` = valeur ; sinon -1. ### 3.3 Lecture d'un `.lpmod` - Ligne `CLÉ: valeur` : clé = texte avant le premier `:`, sans espaces, mise en majuscules ; valeur coupée aux extrémités ; lignes vides et commençant par `//` ignorées ; première occurrence retenue. - Clés lues : `MODULE_UNIQUEID`, `MODULE_NAME`, `MODULE_AUTHOR`, `MODULE_DESCRIPTION`, `MODULE_VERSION`, `MODULE_LICENSE`, `MODULE_LINK`, `MODULE_AUTHOR_LINK`, `MODULE_REQUIREMENTS`, `MODULE_SYSTEM`, `MODULE_HIDDEN`, `MODULE_LOCKED`, `MODULE_KIND`, `MODULE_DEFAULT_ENABLED` (booléens : `true` insensible à la casse, sinon faux). - Défauts : `Kind` = `system` si `MODULE_SYSTEM: true`, `library` si l'identifiant commence par `lps_lib_`, sinon `mod` ; `MODULE_KIND` explicite prime. `bHidden` et `bLocked` valent `bSystem || Kind==Library` si la clé est absente. `bDefaultEnabled` vaut vrai pour système/bibliothèque, faux pour un mod si la clé est absente. - Avertissements (sans refus) : `MODULE_UNIQUEID` absent (identifiant = nom du fichier) ; identifiant ≠ nom du fichier ; les 3 premières lignes utiles ne sont pas, dans l'ordre, `MODULE_UNIQUEID`, `MODULE_NAME`, `MODULE_AUTHOR` (rétrocompatibilité avec la lecture par position de `GI`). ### 3.4 Contrôles et états (dans cet ordre, recalculés après chaque changement) 1. Identifiant en double (sandbox) : le second est `Refused` (« identifiant déjà utilisé par »). 2. Kind Mod qui requiert un module de Kind Mod : `Refused` (« un mod ne peut pas requérir un autre mod () : partager le composant dans une bibliothèque »). 3. Cycle dans les prérequis : tous les modules du cycle `Refused` (« cycle de prérequis : a → b → a »). 4. Prérequis introuvable : `Missing` (« prérequis absent : ») ; prérequis présent mais non `Ready` ou désactivé : `Missing` (« prérequis désactivé : »). Un prérequis peut être un module du jeu (Data) : présent = satisfait. 5. Pas de fabrique (descripteur sandbox sans code) : `NoFactory` pour un mod ; une bibliothèque ou un système sans fabrique est `Ready` (SB1 : services non encore extraits). 6. Sinon : `Ready` si activé, `Disabled` sinon. Système et bibliothèques sont toujours activés. 7. Si le mod ouvert n'est plus `Ready` : `Deactivate`, plus aucun mod ouvert (journal). ### 3.5 Ordre de `GetEntries()` Système et bibliothèques par `Index` croissant (sans index : après, par identifiant) ; puis mods dans l'ordre persisté (nouveaux en fin, par identifiant) ; puis modules du jeu par identifiant. ### 3.6 Journal `LogLPSandboxMods` (catégorie déclarée dans `LPSandboxModRegistry.h`, `LPSANDBOX_API`) - `Display` au `Scan` : `Modules : sandbox, jeu ; ordre : , , …` - `Display` par module : ` [] <État> ` ; `Warning` pour chaque avertissement de lecture et chaque refus. - `Display` à chaque `SetEnabled`, `Move`, `Open` : `Ouvert : ` (preuve de chargement du nouveau code). ## 4. Persistance : `Saved/sandbox_mods.ini` (projet hôte) ```ini [Mods] Order=lps_couple,lps_interaction,lps_asset_integration Open=lps_couple [Enabled] lps_couple=true lps_interaction=true lps_asset_integration=true ``` Lecture au `Scan` (`FConfigFile::Read`, comme `FLPSandboxConfig::LoadFile`), écriture après chaque changement (`FConfigFile::Write`). Fichier absent : ordre par identifiant, cases = `bDefaultEnabled`, aucun mod ouvert mémorisé. ## 5. Enveloppes (`LPSandboxModWrappers.cpp`) Trois classes `FLPSandboxModeWrapper : ILPSandboxMod` paramétrées par (Id, ModeLabel) : | Id | ModeLabel | |---|---| | `lps_couple` | `Pair` | | `lps_interaction` | `Interaction` | | `lps_asset_integration` | `Mesure` | - `Activate(Context)` : `Context.Player->OpenModeLabel(ModeLabel)`. - `Deactivate` : rien (la transition, y compris `EndMeasure`, `SetAutoRun(false)`, `SetFreeView(false)` et la restauration du bras caméra, est déjà faite par `SelectMode` quand le mode suivant s'ouvre). - Fermeture sans mod suivant (mod ouvert désactivé) : `OpenModeLabel("Pair")` puis rangée d'animations masquée (`Menu->AnimRow` réduit) : scène neutre. - Enregistrement dans `FLPSandboxModule::StartupModule` : `RegisterFactory` ×3 puis `Scan()`. ## 6. Personnage (`ALPPairSandboxCharacter`) - Ajouter `void OpenModeLabel(const FString& Label);` public : cherche `Label` dans `Menu->Modes` et appelle `SelectMode` (privé, inchangé) ; journalise si introuvable. - Le menu garde la liste interne `Menu->Modes` (Pair, Interaction, Mesure) mais **n'affiche plus** `ModeCombo`. - Mod initial, à la fin de `ShowAnimMenu` (remplace le bloc actuel `sandbox_mode.txt` / `[Sandbox.Mode]`, même priorité et même effacement de `sandbox_mode.txt`) : 1. `Saved/sandbox_mode.txt` présent : `Registry.Open(IdForModeLabel(contenu))` (pilote `AnimPipeline` : `Mesure`) ; 2. sinon `Open` persisté s'il est `Ready` ; 3. sinon `[Sandbox.Mode]` de `LPSandbox.ini` (défaut `Pair`) via `IdForModeLabel` ; 4. si le mod visé n'est pas `Ready` : premier mod `Ready` dans l'ordre ; aucun : scène neutre (§ 5). - `ModeLabel` reste la vérité interne du mode ; le registre ne le modifie que par `OpenModeLabel`. ## 7. Barre latérale (`SLPSandboxSidebar`, T3) - Remplace, dans `ShowAnimMenu`, le slot de `ModeCombo` (premier slot du `SVerticalBox`) ; le reste du panneau (Mesure, MC, PNJ, animation, vue, filtres, aide) reste en dessous, inchangé. - `SLATE_BEGIN_ARGS` : `_OnOpen` (`TFunction`, le personnage appelle `Registry.Open(Id, Context)`). - S'abonne à `Registry.OnChanged` et reconstruit ses lignes (désabonnement au destructeur). - Section **Mods** (Kind Mod, non masqués) — une ligne : `SCheckBox` (désactivée si verrouillé) → `SetEnabled` ; nom (`Name`) ; boutons ▲ ▼ → `Move(Id, -1/+1)` ; bouton ▶ (activé si `Ready`, libellé « ouvert » si `bOpen`) → `OnOpen(Id)` ; si `Missing`/`Refused`/`NoFactory` : « ⚠ » avec infobulle `Reason`. Ligne du mod ouvert surlignée. - Case **Afficher les modules masqués** (non persistée). - Section **Système** (visible si la case est cochée) : `Index` en hexadécimal, `Id`, `Kind`, `Version`, état ; lecture seule. - Section **Jeu (lecture)** (repliée par défaut, en-tête « Jeu : modules ») : `Id`, `Name`, `Author`, `Requirements`, avertissements de lecture en orange. - Bouton **◀ / ▶** en tête du panneau : replie / déplie tout le panneau du menu (`Menu->Widget` réduit à la barre de titre). - Style : polices et couleurs du menu actuel (`FCoreStyle::GetDefaultFontStyle("Regular", 14)` / 11 pour le détail), largeur 300 comme les listes existantes. ## 8. Descripteurs (T3) Sous `Plugins/LPSandbox/Modules/` ; trois premières lignes dans l'ordre `MODULE_UNIQUEID`, `MODULE_NAME`, `MODULE_AUTHOR`. | Dossier | Id | Nom | Kind / clés | Prérequis | |---|---|---|---|---| | `System/0001_lps_system_core` | `lps_system_core` | Socle sandbox | `MODULE_SYSTEM: true` | — | | `System/0002_lps_system_animmeasure` | `lps_system_animmeasure` | Chaîne de mesure (LPAnimMeasure) | `MODULE_SYSTEM: true` | `lps_system_core` | | `System/0010_lps_lib_scene` | `lps_lib_scene` | Bibliothèque scène | (library par préfixe) | `lps_system_core` | | `System/0011_lps_lib_actors` | `lps_lib_actors` | Bibliothèque personnages | (library) | `lps_system_core, lps_system_animmeasure` | | `Mods/lps_couple` | `lps_couple` | Couple | `MODULE_DEFAULT_ENABLED: true` | `lps_system_core, lps_lib_scene, lps_lib_actors, lps_system_animmeasure` | | `Mods/lps_interaction` | `lps_interaction` | Interaction | `MODULE_DEFAULT_ENABLED: true` | idem | | `Mods/lps_asset_integration` | `lps_asset_integration` | Asset integration | `MODULE_DEFAULT_ENABLED: true` | `lps_system_core, lps_system_animmeasure` | `MODULE_AUTHOR: LifePlay Revival`, `MODULE_VERSION: 0.1`, `MODULE_DESCRIPTION` d'une ligne. ## 9. Critères (repris par SB1-T5) (a) chaque mod s'ouvre et se comporte comme l'ancien mode ; (b) couple → interaction → asset integration → couple ; (c) mod décoché non ouvrable, et décocher le mod ouvert ramène la scène neutre ; (d) `Mods/lps_test_bad` (créé pour l'essai puis retiré) requérant `lps_couple` → `Refused` avec raison ; (e) prérequis absent (renommer temporairement `lps_lib_scene`) → `Missing` sur couple et interaction ; (f) ordre, cases et mod ouvert persistés après relance ; (g) section Jeu : `.lpmod` du jeu lus, avertissements éventuels listés ; (h) pilote : `Saved/sandbox_mode.txt` = `Mesure` + `Saved/sandbox_tour.txt` = `vignettes planches quitter` → tournée identique à AP7 (mesures, vignettes, planches) ; (i) journal `LogLPSandboxMods` : 7 modules sandbox, ordre attendu, 0 refus hors essais.