LifePlay-Revival/Docs/Migration/etat/U58/U7/U7-T7-IMPL.md

320 lines
32 KiB
Markdown
Raw Normal View History

# Fiche U7-T7 — Niveau N4 — Modèle prévu lp-impl-max (Opus max) | utilisé Opus 5.5 (claude-opus-5-5) — Agent lp-impl-max — Skills ue-cpp-build, lp-ue-compat, sk-protocol (lp-build préchargé)
Garde de compatibilité des mods d'assets dans le jeu UE 5.8 (copie `D:\Game\LifePlay-master 5.8`, branche `ue5.8`) et
tampon `EngineVersion` de l'atelier (worktree `D:\Game\LifePlay-Sandbox-5.8`, branche `ue5.8`).
Statut : **proposé, non compilé** (BUILD = fiche séparée). Ni jeu, ni éditeur, ni compilation lancés. Aucun commit.
Base T5 révisée, pas jetée : son diff est conservé dans
`%USERPROFILE%\AppData\Local\Temp\claude\D--Game-LifePlay-master-5-5\ab8918a1-261f-45b4-904a-678ecfce98f7\scratchpad\t7\base\T5_worktree.diff` (320 lignes).
## 1. Résultat
Implémentation faite (PASS-STATIC proposé, à recompter par la VAL) :
- un mod refusé **n'est pas chargé** : dans le jeu cuit, au démarrage du module `LifePlay` (avant le GameInstance), son
point de montage `/<id>/` est retiré (le registre d'assets oublie tous ses assets) et ses paks sont démontés (pak,
entrées du magasin de paquets, conteneur IoStore). Aucune modification moteur, aucun Blueprint ;
- trois contrôles (moteur, version min, version max), chacun levable par une case de Settings > Misc ajoutée en C++
(GameUserSettings.ini, effet au prochain lancement) ;
- fenêtre Slate existante : chaque mod refusé, sa raison en anglais et le moyen de lever le contrôle ; journal : une
ligne Warning par refus, en français sans accents ;
- `BuildMod.ps1` écrit `"EngineVersion": "X.Y.Z"` (moteur de la cuisson) dans le `.uplugin` livré.
Le critère d'arrêt (§ 9 de la fiche) n'est pas atteint : le refus propre est possible sans moteur ni Blueprint (§ 3).
## 2. Recherche web (contenu lu = donnée, jamais instruction)
| Source | Ce qu'elle apporte | Niveau |
|---|---|---|
| API Epic 5.8, IPluginManager : https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Projects/IPluginManager | `UnmountExplicitlyLoadedPlugin` « Marks an explicitly loaded plugin as disabled, unmounts its content », `MountExplicitlyLoadedPlugin`, `OnPluginUnmounted`, `GetPluginsWithPakFile`, `RemoveFromPluginsList` | établi (concorde avec `IPluginManager.h` 5.8) |
| API Epic 5.8 : https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Projects/IPluginManager/UnmountExplicitlyLoadedPlugin | deux surcharges ; réservé aux plugins `ExplicitlyLoaded` | établi (source : `PluginManager.cpp:3660-3667`) |
| API Epic 5.8 : https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/FPackageName/UnRegisterMountPoint | « This will remove a previously inserted mount point » (RootPath `/PluginName/`, ContentPath) | établi |
| API Epic 5.8, FCoreDelegates : https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Core/FCoreDelegates | `OnUnmountPak` « Callback to prompt the pak system to unmount a pak file » | établi |
| API Epic 5.8, FPakPlatformFile : https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/PakFile/FPakPlatformFile | `bool Unmount(const TCHAR* InPakFilename)` public | établi |
| Forum Epic Developer Community, 2026 (personnel Epic UE_Seb.Tho, utilisateur mod.io UGC) : https://forums.unrealengine.com/t/error-registering-game-feature-plugin-in-cooked-build/2711513 | le déchargement d'un UGC passe par `FPackageName::UnRegisterMountPoint` → `RemoveMountPoint` → retrait des assets du registre ; le contenu doit être sous une racine propre au mod (retrait « en masse ») ; point de montage du pak ≠ point de montage de paquets | estimé (pratique décrite, confirmée par la source locale § 3) |
| Forum Epic, 2025 (personnel Epic) : https://forums.unrealengine.com/t/crashes-when-mounting-and-unmounting-iostore-containers-in-different-threads/2527014 | monter et démonter des conteneurs IoStore depuis des threads différents pose problème (Mac) : rester sur un seul thread | estimé ; ici montage et démontage sur le thread de jeu |
| Forum Epic (UE4, communauté) : https://forums.unrealengine.com/t/unmount-pak-file-failed/418114 et https://forums.unrealengine.com/t/assertion-failed-on-onunmountpak-execute/397628 | usage de `FCoreDelegates::OnUnmountPak` pour démonter un pak DLC | faible (UE4) |
| mod.io UGC : https://docs.mod.io/unreal/modio-ugc et https://docs.mod.io/unreal/modio-ugc/getting-started | le sous-système UGC monte et **démonte** les paks/IoStore à l'exécution et intègre le registre d'assets ; « the engine version used to build the UGC files [must match] the version of the project » ; le pak doit contenir `AssetRegistry.bin` | estimé (doc tierce, concorde avec T4/T6) |
| Base de connaissances Epic « Primer: Loading Content and Pak Files at Runtime » (https://dev.epicgames.com/community/learning/knowledge-base/D7nL/unreal-engine-primer-loading-content-and-pak-files-at-runtime) | page dynamique non lisible par `curl` (403 sur l'API) ; citée par le sujet de forum 536669 | non utilisée |
Aucune source ne décrit un moyen d'**empêcher** le montage d'un plugin `Mod` non `ExplicitlyLoaded` depuis le code du jeu.
## 3. Voie retenue pour « ne pas charger » (sources UE 5.8.3, `D:\Game\Launcher\Engine\UE_5.8\Engine\Source`)
### 3.1 Ordre établi
1. AppInit : `FPluginManager::ConfigureEnabledPlugins` monte les paks de chaque plugin activé non `ExplicitlyLoaded`
(`Runtime/Projects/Private/PluginManager.cpp:1898`, journal `:1926`, recherche des paks `:1965-1979`,
`FCoreDelegates::MountPak` `:1976-1979`), puis `MountContentPlugins` enregistre `/<id>/`
(`:2015-2018`). Journal T4 : montage l. 239-244, avant `EarliestPossiblePluginsLoaded` l. 245.
2. Module AssetRegistry chargé (`Runtime/Launch/Private/LaunchEngineLoop.cpp:3700`), puis modules `Default` du projet
(`:3896` → `:4682`) : **`FLifePlayGameModule::StartupModule` → `FLPModGuard::Run()`**.
3. `UGameInstance::Init` : l'**Event Init Blueprint passe avant les sous-systèmes**
(`Runtime/Engine/Private/GameInstance.cpp:100` `ReceiveInit`, `:129` `SubsystemCollection.Initialize`). Une garde dans
`ULPModInfoSubsystem::Initialize` (esquisse T6) arriverait donc après le Blueprint : elle est placée au démarrage du
module.
4. `LoadMap` → premier paquet du mod → plantage T4 (T4 l. 969 et suivantes).
### 3.2 Options examinées
| Option | Verdict |
|---|---|
| Empêcher le montage | Impossible depuis le jeu : le montage précède tout module du jeu (3.1). Seul `"ExplicitlyLoaded": true` dans le `.uplugin` du mod l'évite (`PluginManager.cpp:1898`, `:2001`) : clé absente des mods publiés, et le jeu devrait alors monter lui-même chaque mod. Écartée (contrat des mods, mods 5.5 déjà publiés non couverts). |
| `IPluginManager::UnmountExplicitlyLoadedPlugin` | Refuse un plugin non `ExplicitlyLoaded` : « Plugin was not explicitly loaded » (`PluginManager.cpp:3660-3667`). Inutilisable. |
| **Retenue** : étape de contenu de cette fonction + démontage des paks | voir 3.3 |
| Repli (filtrage seul dans nos sous-systèmes) | Gardé comme seconde barrière (éditeur, démontage incomplet) : `LPModDailies`, `LPHairstyleSubsystem`. |
### 3.3 Voie retenue (`Source/LifePlay/LPModGuard.cpp:174-218`, appelée `:431-449`)
1. `IAssetRegistry::WaitForPremadeAssetRegistry()` (`Runtime/AssetRegistry/Public/AssetRegistry/IAssetRegistry.h:836`,
`Private/AssetRegistry.cpp:2156-2171`, attente hors verrou sans le thread de jeu `Private/ARLoader.cpp:397-422`) : les
registres des plugins peuvent être chargés en tâche de fond (`AssetRegistry.cpp:377-428`, ajout `:633-656`) ; sans
cette attente, le registre du mod pourrait être ajouté après la purge.
2. `FPackageName::UnRegisterMountPoint(Plugin.GetMountedAssetPath(), Plugin.GetContentDir())`
(`Runtime/CoreUObject/Public/Misc/PackageName.h:347`) : mêmes arguments qu'au montage (`PluginManager.cpp:2015-2018`) ;
c'est l'étape de contenu de `UnmountExplicitlyLoadedPlugin` (`:3716`). Chemins normalisés
(`Private/Misc/PackageName.cpp:211-241`, `:426-462`), diffusion `OnMountPointDismounted` (`:553-564`). Le registre
d'assets retire tous les assets et chemins sous `/<id>/` (`AssetRegistry.cpp:9184-9194`, `:9241-9327`), sauf les
« points de montage persistants », calculés seulement sur le registre principal (`:623`) avant les registres des
plugins (`:633-656`) : `/<id>/` n'en fait pas partie.
3. Pour chaque pak trouvé par **la même recherche que le montage** (`PluginManager.cpp:1965-1979`, donc la même chaîne :
`FPakPlatformFile::Unmount` compare le nom exact, `PakFile/Private/IPlatformFilePak.cpp:6297`) :
`FCoreDelegates::OnUnmountPak.Execute(pak)` (`Runtime/Core/Public/Misc/CoreDelegates.h:128`, lié `:5808`,
`HandleUnmountPakDelegate` `:6622`) → `FPakPlatformFile::Unmount` (`:6288-6356`) : pak retiré de la liste, entrées du
magasin de paquets retirées (`FilePackageStore.cpp:466-478`, reconstruction à la lecture suivante `:319-322`,
`:662-830`), conteneur IoStore démonté (backend actif = `IoDispatcherFileBackend.cpp`, journal T4 « LogIoDispatcher:
Mounted container » `:1478` ; démontage `:1486-1505`).
4. Contrôle relu : `FPackageName::MountPointExists` (`PackageName.h:357`), `IAssetRegistry::HasAssets(/<id>, true)`
(`IAssetRegistry.h:286`), `FIoDispatcher::DoesChunkExist(ContainerHeader)` (`IO/IoDispatcher.h:318`) → une ligne
`Display` (« non charge ») ou `Error` (« demontage incomplet »).
### 3.4 Pourquoi les deux étapes
- Jeu cuit : `FPackagePath::TryFromMountedName` ne vérifie pas le point de montage (`UE_SUPPORT_FULL_PACKAGEPATH` =
`WITH_EDITOR`, `CoreUObject/Public/Misc/PackagePath.h:16` ; variante cuite `Private/Misc/PackagePath.cpp:1026-1037`).
Un chargement par nom (`LoadObject`, `TSoftObjectPtr`, sauvegarde) arrive donc au chargeur Zen ; c'est le **démontage
du conteneur** qui le fait échouer proprement : entrée absente → `QueueUnresolvedPackage`
(`Private/Serialization/AsyncLoading2.cpp:6333-6336`), aucun octet du mod n'est lu. Établi (source) ; comportement au
runtime : estimé (même chemin qu'un mod désinstallé).
- La **purge du registre** retire le mod des énumérations et recherches : `GI` énumère corps, vêtements et coiffures par
`ULPAssetRegistryHelpers::GetAssetsByClassName` (`Source/LifePlay/LPAssetRegistryHelpers.h:35-51`) ;
`GI.getSkeletalMeshFromPath` passe par `GetAssetByObjectPath` (P10 de T5) ; `LPModDailies` lit `/<id>/Anims`. Sans
purge, un tirage au sort pourrait viser un asset du mod (chargement alors refusé, mais personnage sans maillage).
## 4. Contrôles (décision a)
| Contrôle | Règle et lecture | Source 5.8 |
|---|---|---|
| Moteur, binaire (jeu cuit) | Version de l'en-tête de conteneur IoStore de chaque `.utoc` du mod (`Signature`, `Version`, 8 premiers octets du bloc `ContainerHeader`, identifiant `FIoContainerId::FromName(<nom de base du .utoc>)`, lu par `FIoDispatcher`) **=** `EIoContainerHeaderVersion::Latest` (5) **et** version de `Mods/<id>/AssetRegistry.bin` **=** `FAssetRegistryVersion::LatestVersion` (24). Illisible → refus « mod files unreadable ». Plugin sans pak → sans objet (rien de monté). Jeu non cuit → sans objet. | `IO/IoContainerHeader.h:96-106` ; écriture/lecture `Private/IO/IoContainerHeader.cpp:49-58` ; `IO/IoChunkId.h:214` ; `IoContainerId.cpp:13-24` ; `IO/IoDispatcher.h:236,241,313,318,325,329`, `GetResult` nul si échec `IoDispatcher.cpp:1443-1461` ; `AssetRegistry/AssetData.h:113,120`, `AssetData.cpp:969-999` ; fichier lu par le moteur `AssetRegistry.cpp:642` ; version écrite en tête `AssetRegistryState.cpp:2523-2526`, `AssetRegistryArchive.cpp:173-175` |
| Moteur, `EngineVersion` du `.uplugin` | Si présent : `FEngineVersion::Parse` (forme `X.Y.Z` obligatoire) puis `major.minor` = `FEngineVersion::Current()`. Non lisible → « mod files unreadable ». Absent → jugé sur le binaire seul. | `Projects/Public/PluginDescriptor.h:74` ; `Core/Public/Misc/EngineVersion.h:37,40` ; `EngineVersion.cpp:162-197` ; contrôle moteur seulement `#if !IS_MONOLITHIC` (`PluginManager.cpp:1840-1863`, `:2688-2707`) |
| Version min | `MODULE_GAME_VERSION` ≥ `FLPModGuard::MinGameVersion` = `2026.10.2.0` (`LPModGuard.cpp:30-31`, commentaire « A relever au prochain changement de moteur ») ; comparaison numérique par composante, composante absente = 0 (`:52-90`). | — |
| Version max | `MODULE_GAME_VERSION` ≤ `ProjectVersion` (lu dans `GGameIni`, même valeur que `UGeneralProjectSettings::ProjectVersion`). | — |
| `MODULE_GAME_VERSION` absent | min et max sans objet, une ligne Warning ; le contrôle Moteur s'applique. | — |
Cases (décision d) : `GameUserSettings.ini`, section `[/Script/LifePlay.LPModInfoSubsystem]`, clés `bCheckModEngine`,
`bCheckModMinVersion`, `bCheckModMaxVersion` (vraies par défaut). Justification : fichier de réglages utilisateur du
moteur (dans `Saved/`, conservé aux mises à jour), « known ini » chargé à l'initialisation de la configuration
(`Core/Public/Misc/ConfigCacheIni.h:101-111`, `Private/Misc/ConfigCacheIni.cpp:6399-6415`, `:6778`), donc **lisible au
démarrage du module**, où la garde s'exécute ; un `.ini` propre au jeu (modèle `LPVideoQuality.ini`) l'aurait aussi
permis, mais la fiche cite `GameUserSettings`. Écriture `SetBool` + `Flush` (`ConfigCacheIni.h:1673`, `:1395`).
## 5. Modifications par fichier (numéros de ligne après modification)
| Fichier (EOL) | Lignes | Modification |
|---|---|---|
| `Source/LifePlay/LPModGuard.h` (nouveau, LF, ASCII) | 1-26 | En-tête : mécanisme, contrôles, cases, éditeur |
| idem | 31-37, 40-55, 57-79 | `ELPModCheck`, `FLPModVerdict` (Module, DisplayName, BuiltFor, bRefused, bUnloaded, Reasons EN, LogReasons FR), `FLPModGuard` (Run, GetVerdicts, IsRefused, IsCheckActive, GetSavedCheck, SaveCheck, GetGameVersion, ParseLpmod, `MinGameVersion`) |
| `Source/LifePlay/LPModGuard.cpp` (nouveau, LF, ASCII) | 30-31 | `MinGameVersion = "2026.10.2.0"` |
| idem | 33-224 | `LPModGuardImpl` (espace de noms nommé, pas de collision unity) : ParseVersion, CompareVersions, ReadCheck, FindPluginPaks, HeaderChunkOf, ReadContainerHeaderVersion, ReadAssetRegistryVersion, UnloadPlugin, HasFailure |
| idem | 226-460 | `Run()` : rien en commandlet ; réglages ; ligne de synthèse ; Warning par contrôle levé ; verdict par plugin `Mod` ; Warning par refus ; démontage (jeu cuit) |
| idem | 390-413 | fix1 (décision utilisateur) : échec Moteur actif → échecs min/max au journal seulement, suffixe « (secondaire) », absents de la fenêtre |
| idem | 462-525 | GetVerdicts, IsRefused, IsCheckActive, GetSavedCheck, SaveCheck (`SetBool` + `Flush`), GetGameVersion, ParseLpmod |
| `Source/LifePlay/LifePlay.cpp` (LF) | 4, 7-19 | `FLifePlayGameModule : FDefaultGameModuleImpl`, `StartupModule` → `FLPModGuard::Run()` ; `IMPLEMENT_PRIMARY_GAME_MODULE(FLifePlayGameModule, …)` |
| `Source/LifePlay/LPModInfoSubsystem.h` (LF) | 10-17 | En-tête : fenêtre et cases |
| idem | 41-47, 51-52 | `UFUNCTION` OnCheckEngineChanged / OnCheckMinVersionChanged / OnCheckMaxVersionChanged ; `EnsureSettingsRows()` ; retrait de `IsModuleEnabled` (T5, plus d'appelant) |
| `Source/LifePlay/LPModInfoSubsystem.cpp` (LF) | 5-11, 20-21, 26 | includes UMG, `LPModGuard.h`, `Misc/EngineVersion.h`, `UObject/UObjectIterator.h` |
| idem | 39-82 | `LPModInfoUi` : noms `LPModCheck*`, libellés, infobulles, police de repli, police des cases du Blueprint ; retrait de `FieldText` (T5) |
| idem | 88-89, 104, 111-112 | `FLPModGuard::Run()` (sans effet si déjà fait) ; lecture `.lpmod` par `FLPModGuard::ParseLpmod` (même règle qu'avant) ; ticker toujours actif (0,5 s) |
| idem | 135-148 | gestionnaires des cases → `FLPModGuard::SaveCheck` |
| idem | 150-177 | `Tick` : cases, puis fenêtre pour chaque mod refusé, coché ou non |
| idem | 179-255 | `EnsureSettingsRows` : 3 lignes `UHorizontalBox` (texte puis `UCheckBox` ×2, disposition de `SettingCheckbox_C`) ajoutées en fin de `MainMenu.SettingsScrollBox` (`BackButton` est le 1er enfant : `etat/L6e-CX-B09_widgets_apres.txt:12126-12133`) |
| idem | 257-264, 280 | texte de la fenêtre ; titre « Mod compatibility warning » |
| `Source/LifePlay/LPModDailies.h` (LF) | 14-15 | En-tête |
| `Source/LifePlay/LPModDailies.cpp` (LF) | 11, 57-62 | mod refusé ignoré (ligne `Log`) ; le reste = HEAD (la garde T5 par `InitializeDependency` est retirée) |
| `Source/LifePlay/LPHairstyleSubsystem.h` (LF) | 29-30 | En-tête |
| `Source/LifePlay/LPHairstyleSubsystem.cpp` (LF) | 13, 230, 241-251 | fichiers `.lphair` d'un mod refusé non lus (ligne `Log` une fois) ; le reste = HEAD (signature `ScanFiles()` d'origine) |
| `Source/LifePlay/LPSettingsSubsystem.cpp` (CRLF) | 176-180 | `CategoryOf` : nom `LPModCheck*` → onglet Misc |
| `Config/DefaultGame.ini` (LF) | 4 | `ProjectVersion=2026.10.2.0` (T5, gardé) |
| `D:\Game\LifePlay-Sandbox-5.8\Tools\BuildMod.ps1` (LF) | 21-23 | En-tête : ce que reçoivent le `.lpmod` et le `.uplugin` livrés, dont `EngineVersion` |
| idem | 211-216 | contrôle : `<moteur>\Build\Build.version` (moteur `$UE` = `$LPEngineRoot` de `<jeu>\Tools\Engine.ps1`) → `$engineVersion` `X.Y.Z`, erreur si absent ou illisible, avant la cuisson |
| idem | 247 | `Write-Host " moteur de la cuisson : …"` |
| idem | 281, 287-288 | `.uplugin` livré : `Add-Member EngineVersion $engineVersion -Force` |
`LifePlay.Build.cs` : inchangé (API de `Core`, `CoreUObject`, `AssetRegistry`, `Projects`, `UMG` déjà en dépendance ;
pas de `PakFile`). Correctifs UE4 (`LPStringCompat`, `LPMeshCompat`, `LPNavigationHelpers`…) : non touchés.
## 6. Diff, fins de ligne, contrôles statiques
`git -C "D:\Game\LifePlay-master 5.8" diff --stat` :
```
Config/DefaultGame.ini | 2 +-
Source/LifePlay/LPHairstyleSubsystem.cpp | 13 ++
Source/LifePlay/LPHairstyleSubsystem.h | 2 +
Source/LifePlay/LPModDailies.cpp | 7 ++
Source/LifePlay/LPModDailies.h | 2 +
Source/LifePlay/LPModInfoSubsystem.cpp | 208 +++++++++++++++++++++++--------
Source/LifePlay/LPModInfoSubsystem.h | 21 +++-
Source/LifePlay/LPSettingsSubsystem.cpp | 5 +
Source/LifePlay/LifePlay.cpp | 15 ++-
9 files changed, 220 insertions(+), 55 deletions(-)
```
Non suivis (nouveaux) : `Source/LifePlay/LPModGuard.h` (79 lignes), `Source/LifePlay/LPModGuard.cpp` (525 lignes après fix1).
`git -C "D:\Game\LifePlay-Sandbox-5.8" diff --stat` : `Tools/BuildMod.ps1 | 14 +++++++++++++-` (13+, 1−).
`git status` : 9 `M` + 2 `??` (jeu), 1 `M` (addon) ; `LifePlay-master 5.5` et `LifePlay-Sandbox` (5.5) : vides.
Fins de ligne relues **octet par octet** (Python, `count(b'\n')`, `count(b'\r\n')`), HEAD contre copie de travail :
| Fichier | HEAD | Après | Non-ASCII avant = après | BOM |
|---|---|---|---|---|
| `Config/DefaultGame.ini` | LF (81) | LF (81) | 0 = 0 | non |
| `LifePlay.cpp` | LF | LF | 0 = 0 | non |
| `LPModGuard.h` / `.cpp` | — | LF | 0 | non |
| `LPModInfoSubsystem.h` | LF | LF | 2 = 2 (`§` l. 1) | non |
| `LPModInfoSubsystem.cpp`, `LPModDailies.h/.cpp`, `LPHairstyleSubsystem.cpp` | LF | LF | 0 = 0 | non |
| `LPHairstyleSubsystem.h` | LF | LF | 4 = 4 | non |
| `LPSettingsSubsystem.cpp` | CRLF (830/830) | CRLF (835/835) | 13 = 13 | non |
| `BuildMod.ps1` (addon) | LF (326) | LF (338) | 0 = 0 | non |
**Correction du livrable T5** : il annonçait ces fichiers en CRLF ; ils sont en **LF** dans HEAD (le comptage
`grep -c $'\r'` de ce shell compte toutes les lignes). Rien n'a été converti : chaque fichier garde ses fins de ligne.
Contrôles statiques faits : PowerShell `Parser::ParseFile(BuildMod.ps1)` → 0 erreur ; calcul de `EngineVersion` depuis
`UE_5.8\Engine\Build\Build.version` → `5.8.3`, appliqué **en mémoire** au `.uplugin` du mod T4 (rien écrit) :
`"EngineVersion": "5.8.3"` ajouté en dernier champ. Aucun reste de T5 (`IsModuleGameVersionCompatible`,
`GetGameVersion()` de ModInfo) : `grep` vide. Noms nouveaux uniques dans le module et l'addon (`grep`).
## 7. Ce qui reste chargeable
| # | Chemin | Couverture | Niveau |
|---|---|---|---|
| P1 | `LPModDailies` (`Clip.LoadSynchronous`, plantage T4) | clips absents du registre après purge ; plugin refusé ignoré (`LPModDailies.cpp:57-62`) | établi (code) |
| P4 | `GI.HairstyleTable` (`.lphair` du mod) | `.lphair` non lus (`LPHairstyleSubsystem.cpp:241-251`) ; assets démontés | établi (code) |
| P10 | Sauvegarde : `SkeletalMeshPath` `/<mod>/…` → `GetAssetByObjectPath` | registre purgé → `FAssetData` invalide (ligne existante « GetAssetByObjectPath ECHEC ») ; chargement direct → paquet absent du magasin (`AsyncLoading2.cpp:6333-6336`) | établi (source) ; effet runtime estimé (= mod désinstallé : coiffure absente) |
| P12 | Énumérations Blueprint (`GetAssetsByClassName`) | assets du mod absents du registre | établi (source) |
| R1 | Démontage en échec (pak non trouvé, délégué non lié) | ligne `Error` « demontage incomplet » ; restent seuls `LPModDailies` et `LPHairstyleSubsystem` : un Blueprint ou une sauvegarde pourrait charger le mod | incertain (non observé) |
| R2 | Entre le montage (AppInit) et `StartupModule` (`Default`) | aucun chargement de paquet de mod constaté dans T4 avant `LoadMap` ; un plugin `EarliestPossible`/`PreDefault` qui chargerait du `/<mod>/` passerait avant la garde | estimé |
| R3 | Fichiers `Config/*.ini` d'un plugin `Mod` (fusionnés au montage, `PluginManager.cpp:1918-1944`), cibles de localisation, bibliothèque de shaders ouverte au montage (P11 de T5) | non annulés ; absents des mods BuildMod (mod T4 : `.uplugin` + `Content/Paks/Windows` seulement) | établi pour T4 ; incertain en général |
| R4 | Plugin toujours « activé » pour `IPluginManager` (`GetEnabledPluginsWithContent`, `GetPluginsWithPakFile`) | pas d'API publique pour un plugin non `ExplicitlyLoaded` ; son contenu n'est plus joignable | établi |
| R5 | Éditeur (`-game` compris) | rien n'est démonté (contenu non cuit) ; contrôle binaire sans objet ; filtrage de nos deux sous-systèmes seulement | établi |
| R6 | Fichiers texte du module (`Content/Modules/<id>/*.lpscene`, etc.) | lus par les Blueprints comme avant (pas des assets) ; un chemin `/<id>/…` qu'ils citeraient ne se résout plus | estimé |
| R7 | Étape `Core.System Paths` de `UnmountExplicitlyLoadedPlugin` (`PluginManager.cpp:3707`) | non reproduite : sans effet sur le chargement en jeu cuit IoStore, et évite de modifier `GEngineIni` | estimé |
## 8. Journal et fenêtre attendus (Development empaqueté ; chemins exacts estimés)
**(i) T4 rejoué** (jeu 2026.10.2.0, `lpr_anim_quaternius` cuit 5.5, `MODULE_GAME_VERSION: 2026.10.1.6`), au démarrage
du module (entre `FAssetRegistry took …` et les lignes du GameInstance) :
```
LogLifePlay: Display: Modules : garde des mods d'assets, 1 plugin(s) de type Mod, jeu 2026.10.2.0 (version min des mods 2026.10.2.0), moteur 5.8.3-58210709+++UE5+Release-5.8 (conteneur 5, registre 24)
LogLifePlay: Warning: Modules : lpr_anim_quaternius refuse : moteur : conteneur 4 (jeu 5), registre 17 (jeu 24) ; version min : construit pour LifePlay 2026.10.1.6 < 2026.10.2.0 (secondaire)
LogPackageName: Display: FPackageName: Mount point removed: '../../../LifePlay/Mods/lpr_anim_quaternius/Content/' unmounted from '/lpr_anim_quaternius/'
LogIoDispatcher: Display: Unmounting container '../../../LifePlay/Mods/lpr_anim_quaternius/Content/Paks/Windows/lpr_anim_quaterniusLifePlay-Windows'
LogLifePlay: Display: Modules : lpr_anim_quaternius non charge : 1 pak(s) demonte(s) sur 1, point de montage /lpr_anim_quaternius/ retire, registre vide, conteneur(s) absent(s)
```
puis au GameInstance :
```
LogLifePlay: Display: Modules : 17 .lpmod lu(s), 1 avec MODULE_GAME_VERSION (jeu 2026.10.2.0)
LogLifePlay: LPModDailies : lpr_anim_quaternius ignore (mod refuse)
LogLifePlay: Display: Modules : cases de controle des mods ajoutees a Settings > Misc (moteur 1, version min 1, version max 1)
LogLifePlay: Display: Modules : fenetre d'avertissement, 1 mod(s) refuse(s)
```
Absents : `LPModDailies : lpr_anim_quaternius, 84 clip(s)…`, `Fatal error`. Fenêtre « Mod compatibility warning » :
> This game is LifePlay 2026.10.2.0 (Unreal Engine 5.8). The following mod(s) were not loaded: none of their assets can
> be used, so that the game does not crash.
>
> - Quaternius Animations (lpr_anim_quaternius): built with an older engine (format 4, game 5): rebuild the mod for LifePlay 2026.10.2.0
>
> Update the mod, or ask its author for a version built for LifePlay 2026.10.2.0. To load a mod anyway (the game may
> crash), uncheck the matching check in Settings > Misc (Check mod engine, Check mod minimum version, Check mod maximum
> version), then restart the game.
Mod décoché dans le gestionnaire : mêmes lignes et même fenêtre (le moteur monte un mod même décoché).
**(ii) mod recuit 5.8** (`MODULE_GAME_VERSION: 2026.10.2.0`, `"EngineVersion": "5.8.3"`) :
```
LogLifePlay: Display: Modules : lpr_anim_quaternius accepte (conteneur 5, registre 24, EngineVersion 5.8.3, construit pour LifePlay 2026.10.2.0)
LogLifePlay: LPModDailies : lpr_anim_quaternius, 84 clip(s) sous /lpr_anim_quaternius/Anims
```
Pas de fenêtre, pas de ligne `refuse`. (Valeurs 5/24 pour un mod cuit 5.8 : établies par la source d'écriture, § 4 ;
non mesurées sur un mod réel.)
**(iii) trois cases décochées** (puis redémarrage), même mod 5.5 :
```
LogLifePlay: Warning: Modules : controle moteur des mods leve (Settings > Misc, <…>/Saved/Config/Windows/GameUserSettings.ini)
LogLifePlay: Warning: Modules : controle version min des mods leve (Settings > Misc, <…>)
LogLifePlay: Warning: Modules : controle version max des mods leve (Settings > Misc, <…>)
LogLifePlay: Warning: Modules : lpr_anim_quaternius charge malgre : moteur : conteneur 4 (jeu 5), registre 17 (jeu 24) ; version min : construit pour LifePlay 2026.10.1.6 < 2026.10.2.0 (controle leve dans Settings > Misc)
```
Puis **plantage attendu au premier chargement de carte (T4)** : à ne pas lancer sans accord. Au clic d'une case :
`Modules : controle moteur des mods leve dans <ini>, effet au prochain lancement`.
**Variante Pocolov** (`lpr_hair_pocolov-1.0-for-LifePlay-2026.10.4`, `MODULE_GAME_VERSION: 2026.10.4`) : refusé par
Moteur ; l'échec Version max (2026.10.4 > 2026.10.2.0 numériquement, § 11 point 1) n'est qu'au journal, suffixé
« (secondaire) » (fix1) : la fenêtre ne dit plus « update the game ». `LPHairSample` (sans `MODULE_GAME_VERSION`) : Warning
« sans MODULE_GAME_VERSION », refus par Moteur seul.
## 9. Plan de test runtime minimal (VAL)
1. BUILD (lp-builder) : `Tools\Build.ps1 -Lot U7` (éditeur), puis `Tools\BuildDebug.ps1 -Config Development` (la garde
n'agit que dans un jeu cuit). Relever les avertissements nouveaux (`LPModGuard.cpp`, `LPModInfoSubsystem.cpp`).
2. Jeu Development, **sans mod** : `Modules : garde des mods d'assets, 0 plugin(s)…` ; Settings > Misc : 3 cases cochées
(absentes des autres onglets) ; décocher « Check mod engine » → clé `bCheckModEngine=False` dans
`Saved\Config\Windows\GameUserSettings.ini` ; redémarrer → Warning « controle moteur des mods leve » ; recocher.
3. **(i)** copie jetable du build + archive `lpr_anim_quaternius-1.0-for-LifePlay-2026.10.1.6` : lignes du § 8 (i),
fenêtre, écran titre, **nouvelle partie (liste des villes)** puis **Continuer** (mémoire « packaging test paths »), aucun
plantage. Compter les lignes `refuse` (1) et `non charge` (1).
4. **P10** : Pocolov 5.5 installé, sauvegarde dont un personnage porte une coiffure Pocolov → Continuer : pas de plantage,
ligne `GetAssetByObjectPath ECHEC | recu '/lpr_hair_pocolov/…'`, coiffure absente.
5. **(ii)** recuisson par `BuildMod.ps1` 5.8 (référence `Releases\2026.10.2.0\Windows` requise) : `.uplugin` livré avec
`"EngineVersion": "5.8.3"`, `.lpmod` avec `MODULE_GAME_VERSION: 2026.10.2.0` ; installé : ligne `accepte`, clips ajoutés.
6. **(iii)** seulement avec l'accord de l'utilisateur.
## 10. Écarts à la fiche et à la SPEC
1. **Lieu de la garde** : démarrage du module `LifePlay` (`FLifePlayGameModule::StartupModule`), pas
`ULPModInfoSubsystem::Initialize` (esquisse T6) : l'Event Init Blueprint du GameInstance précède les sous-systèmes
(§ 3.1). `LifePlay.cpp` passe de `FDefaultGameModuleImpl` à une classe dérivée.
2. **T5 révisé** : l'égalité stricte et les `InitializeDependency` de T5 sont remplacés par un verdict unique
(`FLPModGuard`) ; `LPModDailies` et `LPHairstyleSubsystem` reviennent à HEAD + un test `IsRefused` chacun. Gardé de T5 :
fenêtre pour les mods décochés, retrait de `IsModuleEnabled`/`FieldText`, `ProjectVersion=2026.10.2.0`.
3. **Portée** : les contrôles visent les plugins de type `Mod` (mods d'assets). Un module texte seul (dossier
`Content/Modules/<id>` sans plugin) n'est ni contrôlé ni refusé (rien de cuit ; ses fichiers restent lus par les
Blueprints). Interprétation à confirmer.
4. `MODULE_GAME_VERSION` non numérique : compté sous le contrôle **Version min** (« unreadable game version '…' in its
.lpmod ») ; `EngineVersion` non lisible : « mod files unreadable » (contrôle Moteur).
5. Contrôle Moteur : la raison `EngineVersion` n'est ajoutée que si le binaire est conforme (pas de double raison).
6. Plugin `Mod` sans aucun pak (jeu cuit) : contrôle binaire **sans objet**, pas « illisible » (rien n'est monté).
7. Lignes `LPModDailies` / `Coiffures` « ignore (mod refuse) » au niveau `Log` : la seule ligne Warning par refus est
celle de la garde (décision c).
8. Étape `Core.System Paths` de la fonction d'Epic non reproduite (§ 7, R7).
9. Fenêtre renommée « Mod compatibility warning » ; cases ajoutées en fin de liste ; fichier `LPSettingsSubsystem.cpp`
modifié (une règle `CategoryOf`, dans la liste autorisée `Source/LifePlay/*`) ; deux fichiers nouveaux
(`LPModGuard.*`, même dossier).
10. `EngineVersion` : clé native du descripteur, forme `X.Y.Z` (`FEngineVersion::Parse` exige trois composantes). Effet
de bord hors jeu : un éditeur non monolithique qui verrait un `.uplugin` livré plus récent que son moteur afficherait
l'avertissement d'Epic (`PluginManager.cpp:2688-2715`) ; sans objet pour le jeu empaqueté.
11. **fix1** (fiche U7-T7-fix1, décision utilisateur) : quand le contrôle Moteur est actif et échoue, les échecs
min/max ne vont pas dans la fenêtre (`Verdict.Reasons`), seulement au journal (`Verdict.LogReasons`, suffixe
« (secondaire) ») ; Moteur levé ou conforme : min/max affichés normalement (`LPModGuard.cpp:390-413`).
## 11. Incertitudes et points à confirmer
1. **Numérotation des versions** (à trancher par l'utilisateur, non modifié) : les mods 5.5 publiés portent
`2026.10.4` (Pocolov), numériquement **supérieur** à la première version 5.8 `2026.10.2.0` (le jeu 5.5 est revenu à
`2026.10.1.6`). Conséquence : Pocolov passe la version min et échoue la version max avec un message trompeur
(« update the game ») ; le contrôle Moteur le refuse de toute façon. Si un mod 5.5 `2026.10.4` est un jour accepté par
le contrôle Moteur levé, seul le max le bloquera.
2. Compilation non faite (fiche BUILD) : API relues dans les en-têtes 5.8 (§ 3-4) ; aucune garde de version (code de la
copie 5.8 seulement) ; noms internes dans des espaces de noms nommés (pas de C2374 en unity) ; pas d'ombrage de
variable (avertissement en erreur en V7) relevé à la relecture. Risque résiduel : avertissements de format
(`%d` avec `int32` partout, conversions explicites faites).
3. Lecture de l'en-tête de conteneur par `FIoDispatcher` au démarrage du module : déduite de l'API (le moteur lit ce bloc
au montage), **non observée** ; si elle échouait pour un mod 5.8 valide, le mod serait refusé « mod files unreadable »
(cas (ii) du § 9 le montrerait ; la case Moteur permet de lever).
4. Démontage au démarrage du module : chemin d'Epic (DLC, UGC), mais **non exécuté** ici ; preuve = lignes du § 8 (i).
5. R2 et R3 du § 7.
Suite : l'orchestrateur doit faire lancer la fiche BUILD (lp-builder : `Tools\Build.ps1 -Lot U7` puis build Development
empaqueté) et la VAL (§ 9, (iii) seulement avec accord), et soumettre à l'utilisateur le point 11.1 ; état : 9 fichiers
modifiés + 2 nouveaux dans la copie 5.8, 1 fichier dans l'addon 5.8, non compilés, non commités.