# 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 `//` 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 `//` (`: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 `//` (`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`) : `//` 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(/, 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 `//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()`, lu par `FIoDispatcher`) **=** `EIoContainerHeaderVersion::Latest` (5) **et** version de `Mods//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 : `\Build\Build.version` (moteur `$UE` = `$LPEngineRoot` de `\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` `//…` → `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 `//` 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//*.lpscene`, etc.) | lus par les Blueprints comme avant (pas des assets) ; un chemin `//…` 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 , 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/` 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.