LifePlay-Revival/Docs/Migration/etat/sandbox/SB1/SB1-SPEC.md

245 lines
14 KiB
Markdown
Raw Normal View History

# 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/**/<id>.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<FString> 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<FString> Warnings; // lecture : clé manquante, UNIQUEID ≠ nom du fichier, 3 premières lignes dans le désordre…
TMap<FString, FString> 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<SWidget> 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<TSharedRef<ILPSandboxMod>()> Factory);
void Scan(); // découverte + lecture + contrôles + état persisté (§ 3.2-3.4)
const TArray<FLPSandboxModEntry>& 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")` ; `<dossier>/<dossier>.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 <dossier> »).
2. Kind Mod qui requiert un module de Kind Mod : `Refused` (« un mod ne peut pas requérir un autre mod (<id>) :
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 : <id> ») ; prérequis présent mais non `Ready` ou désactivé :
`Missing` (« prérequis désactivé : <id> »). 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 : <n> sandbox, <m> jeu ; ordre : <id>, <id>, …`
- `Display` par module : `<id> [<Kind>] <État> <raison>` ; `Warning` pour chaque avertissement de lecture et chaque refus.
- `Display` à chaque `SetEnabled`, `Move`, `Open` : `Ouvert : <id>` (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<void(const FString&)>`, 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 : <n> 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.