# Fiche U7-T9 — Niveau N3 — Modèle prévu lp-impl (Opus xhigh) | utilisé Opus 5.5 (claude-opus-5-5) — Agent lp-impl — Skills ue-cpp-build, sk-protocol (lp-build, lp-ue-compat préchargés) Marquage, dans le gestionnaire de mods, des lignes de mods refusés par la garde U7 (copie `D:\Game\LifePlay-master 5.8`, branche `ue5.8`). Décision utilisateur : C++ seulement, aucun Blueprint modifié. Statut : **proposé, non compilé** (BUILD = fiche séparée). Ni jeu, ni éditeur, ni commandlet, ni compilation lancés. Aucun commit. ## 1. Résultat PASS-STATIC proposé (à recompter par la VAL). Critère d'arrêt (§ 9) non atteint : la ligne est identifiable sans Blueprint, et la reconstruction du widget ne rend pas le marquage instable (§ 3). - Ligne d'un mod refusé : nom suffixé « (not loaded) », ligne teintée rouge atténué, infobulle « Not loaded: », case `ModEnabledBox` désactivée. `GI.ModEnableds` n'est pas touché (choix du joueur conservé). - Idempotent : marque reconnue à la teinte de la ligne, suffixe protégé par `EndsWith` ; tri et images suivantes sans effet. - Une ligne Display par ouverture : `Modules : gestionnaire, N mod(s) refuse(s) marque(s)`. - Aucun mod refusé : aucun ticker ajouté (coût nul). ## 2. Widget et propriétés (lecture seule des relevés, aucun outil lancé) | Élément | Preuve | Niveau | |---|---|---| | Gestionnaire `/Game/UI/ModManager` (classe `ModManager_C`), liste `ModScrollBox` (ScrollBox, variable) | `etat/L6e-CX-B09_widgets_apres.txt:14811`, `:14838` ; nom de classe `ModManager_C` : `Source/LifePlay/LPDirectControlSubsystem.cpp:53` | établi | | Ligne `/Game/UI/ModuleTick` : `ModEnabledBox` (CheckBox), `HoverButton` (Button) contenant `NameText` et `AuthorText` (TextBlock, variables), `UpButton`, `DownButton` | `etat/L6e-CX-B09_widgets_apres.txt:14934`, `:14945`, `:14955`, `:14972`, `:14985`, `:14998`, `:15014` | établi | | Création à chaque ouverture : `GI.CreateModManagerWidget` → `CreateWidget` → `AddToViewport` ; fermeture `RemoveFromParent` | `etat/DOC-MOD_LPDumpFunction_20261007-083554.txt:1484`, `:1489`, `:1508` ; bouton titre → même événement : `Source/LifePlayEditorTools/LPTitleModManagerCommandlet.cpp:295-296` | établi | | Par `.lpmod` : `CreateWidget` ModuleTick, `FullString` = fichier, `UniqueID` = ligne 1 après `:` (Trim, TrimTrailing) = `MODULE_UNIQUEID`, `CheckIfModAlreadyFound`, `NameText.SetText` (ligne 2), `AuthorText.SetText` (ligne 3), ajout à `TempModuleTicks` | `083554.txt:1561`, `:1573`, `:1579`, `:1639`, `:1644`, `:3413` | établi | | Lignes ajoutées en bloc après la boucle (`AddTempModuleTicks`, dans l'ordre de `ModEnableds`) | `083554.txt:3437`, `:3482` | établi | | Tri (▲/▼, `SortLoadOrder`) : `Array_Swap(ModEnableds)`, `ModScrollBox.ClearChildren`, `AddTempModuleTicks` = **mêmes objets** remis | `083554.txt:3497`, `:3501` | établi | | `CheckIfModAlreadyFound` fixe la variable `ModEnabled` de la ligne depuis `ModEnableds` (aucun `SetIsChecked`) | `etat/L6f_dump_GI.txt:153`, `:179` | établi | | ModuleTick, un seul graphe (EventGraph, 48 nœuds) : case → `ModEnabled` → `GI.ToggleMod(UniqueID)` ; un 2ᵉ événement inverse `ModEnabled` et appelle `ToggleMod` | `etat/DOC-MOD_LPDumpFunction_20261007-083154.txt:9-12` ; `083338.txt:141`, `:165`, `:182`, `:191`, `:211` | établi | | Ce 2ᵉ événement = `HoverButton.OnClicked` ; la case affiche `ModEnabled` par une liaison de propriété (pas de graphe de liaison) | déduit : seul bouton restant hors ▲/▼ et survol ; aucun `SetIsChecked` dans le graphe | estimé | | Pocolov : dossier `lpr_hair_pocolov`, `MODULE_UNIQUEID: lpr_hair_pocolov`, `MODULE_NAME: Pocolov Hairstyles`, `MODULE_AUTHOR: LifePlay Revival (hairstyles by Pocolov)` | `D:\Game\LifePlay_Build_5.8_U7_P10\Windows\LifePlay\Content\Modules\lpr_hair_pocolov\lpr_hair_pocolov.lpmod:1-3` | établi | | Pocolov refusé et coché par défaut | `...\P10\Windows\LifePlay\Saved\Logs\LifePlay.log:827` (refus), `:887` (« actif par defaut (MODULE_DEFAULT_ENABLED), ajoute a la liste des mods ») | établi | ## 3. Mécanisme retenu, stabilité - Ticker `FTSTicker` à chaque image (`0.f`, comme `LPSettingsSubsystem.cpp:437`), **ajouté seulement si au moins un mod est refusé**. Recherche par `TObjectIterator` (requête par classe, `UObjectIterator.h:251-265`) de `ModScrollBox` dont le propriétaire est un `ModManager*` **au viewport** (`UWidget::IsInViewport`) de cette instance de jeu : une instance fermée (en attente de GC) est écartée. - Pour chaque enfant `ModuleTick*` : variable Blueprint `UniqueID` lue par réflexion (`FStrProperty`), recherchée dans une table `RefusedTips` construite à l'initialisation depuis `FLPModGuard::GetVerdicts()` (clés : nom du plugin **et** `MODULE_UNIQUEID` du `.lpmod` ; `FString` : égalité et hachage sans casse, `UnrealString.h.inl:910-912`, `:2129-2132`, comme le `EqualEqual_StriStri` du Blueprint). - Stabilité : les marques sont des propriétés UMG des objets (`Text`, `ColorAndOpacity`, `bIsEnabled`, `ToolTipText`), réappliquées à toute reconstruction Slate (`UserWidget.cpp:406-422`, `Widget.cpp:321-333`, `:526-536`, `TextBlock.cpp:469-478`). Tri = mêmes objets → marques conservées ; réouverture = nouveaux objets → marqués à la première image. Le Blueprint n'écrit `NameText` qu'à la création (avant l'ajout à la liste) : pas d'écrasement constaté. - Le marquage a lieu entre deux images (ticker), jamais au milieu de l'exécution Blueprint qui remplit la liste. ## 4. API moteur lues (UE 5.8.3, `D:\Game\Launcher\Engine\UE_5.8\Engine\Source`) | API | Source | |---|---| | `UWidget::IsInViewport` (vrai si ajouté par `AddToViewport`) | `Runtime/UMG/Public/Components/Widget.h:547-549`, `Private/Components/Widget.cpp:344-354` ; `AddToViewport` → sous-système `UserWidget.cpp:1366-1378`, drapeau `Blueprint/GameViewportSubsystem.cpp:158`, `:214` | | `UWidget::SetIsEnabled`, `SetToolTipText`, `GetGameInstance` | `Widget.h:545`, `:556`, `:871` ; `Widget.cpp:321-333`, `:526-536` | | `UUserWidget::SetColorAndOpacity` (« affects all child widgets »), `GetColorAndOpacity`, `GetWidgetFromName` | `Blueprint/UserWidget.h:1050-1061`, `:1426` ; `UserWidget.cpp:427-441`, `:1259-1262` ; réappliqué `:406-422` | | `UTextBlock::GetText`, `SetText` | `Components/TextBlock.h:95`, `:103` ; `TextBlock.cpp:457-478` | | `UPanelWidget::GetChildrenCount`, `GetChildAt` | `Components/PanelWidget.h:28`, `:36` | | Infobulle : chemin sous le curseur **y compris widgets désactivés**, de la feuille vers la racine, 1ʳᵉ infobulle non vide | `Runtime/Slate/Private/Framework/Application/SlateUser.cpp:1350-1386` | | `FStrProperty` : en-tête `UObject/StrProperty.h` (inclus par `UnrealType.h` seulement si `UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_7`, `UnrealType.h:7806-7816` ; la copie 5.8 est en `Unreal5_8`, `Source/LifePlay.Target.cs:12`) ; `GetPropertyValue_InContainer` | `CoreUObject/Public/UObject/StrProperty.h:1-19`, `StrProperty.h.inl:27-29`, `UnrealType.h:1663` ; existe aussi en 5.5 (`UE_5.5/.../UObject/StrProperty.h`) | | `FName` comparé à `const TCHAR*` | `Core/Public/UObject/NameTypes.h:1362`, `:1413-1417` (précédent du fichier : `RowNames[0]`) | | `constexpr FLinearColor`, `operator==` | `Core/Public/Math/Color.h:130`, `:332` ; précédent moteur `Developer/LogVisualizer/Private/SVisualLoggerFilterWidget.h:133` | | `FTSTicker::RemoveTicker` sans effet sur une poignée vide | `Core/Private/Containers/Ticker.cpp:39-58` (`Handle.Pin()` nul) | ## 5. Modifications par fichier (numéros après modification) | Fichier (EOL) | Lignes | Modification | |---|---|---| | `Source/LifePlay/LPModInfoSubsystem.h` (LF) | 17-21 | En-tête : 4ᵉ rôle du sous-système (gestionnaire de mods, aucun Blueprint modifié) | | idem | 31 | `class UScrollBox;` | | idem | 60-61 | `bool TickModManager(float DeltaTime);` | | idem | 71-77 | `RefusedTips`, `ManagerTickHandle`, `ManagerList` (faible), `bManagerListLogged`, `bWarnedNoUniqueId` | | `Source/LifePlay/LPModInfoSubsystem.cpp` (LF) | 25 | `#include "UObject/StrProperty.h"` | | idem | 84-118 | `LPModInfoUi` : noms `ModScrollBox` / `ModManager` / `ModuleTick`, suffixe, teinte `RefusedTint (0.75, 0.28, 0.28, 0.9)`, `MarkRefusedRow` (teinte, infobulle, suffixe si absent, case désactivée ; faux si déjà marquée) | | idem | 151-172 | `Initialize` : table `RefusedTips` (raisons `FLPModVerdict::Reasons` jointes par « ; »), ticker par image seulement si la table n'est pas vide | | idem | 178 | `Deinitialize` : retrait du ticker | | idem | 318-383 | `TickModManager` : liste à l'écran, lignes, `UniqueID`, marquage, Warning unique si `UniqueID` absent, ligne Display | Rien d'autre : `LPSettingsSubsystem.cpp`, `LPModGuard.*`, Blueprints, Build.cs inchangés (UMG et CoreUObject déjà en dépendance). Correctifs UE4 (`LPStringCompat`, `LPMeshCompat`, `LPNavigationHelpers`…) non touchés. ## 6. Diff, fins de ligne, contrôles statiques T9 seul (état T7 copié avant modification dans `%USERPROFILE%\AppData\Local\Temp\claude\D--Game-LifePlay-master-5-5\ab8918a1-261f-45b4-904a-678ecfce98f7\scratchpad\t9\avant\`, diffs `..\T9_LPModInfoSubsystem.cpp.diff` 165 lignes, `..\T9_LPModInfoSubsystem.h.diff` 47 lignes) : ``` git diff --no-index --stat /LPModInfoSubsystem.cpp Source/LifePlay/LPModInfoSubsystem.cpp 1 file changed, 128 insertions(+) git diff --no-index --stat /LPModInfoSubsystem.h Source/LifePlay/LPModInfoSubsystem.h 1 file changed, 16 insertions(+), 1 deletion(-) ``` `git -C "D:\Game\LifePlay-master 5.8" diff --stat` (cumul U7 contre HEAD) : ``` 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 | 332 ++++++++++++++++++++++++++----- Source/LifePlay/LPModInfoSubsystem.h | 36 +++- Source/LifePlay/LPSettingsSubsystem.cpp | 5 + Source/LifePlay/LifePlay.cpp | 15 +- 9 files changed, 361 insertions(+), 53 deletions(-) ``` (avant T9 : `LPModInfoSubsystem.cpp | 208`, `.h | 21`, total 220+/55− ; seules ces deux lignes changent.) `git status --short` : 9 `M` + 2 `??` (`LPModGuard.*`, T7), inchangé en nombre ; copie 5.5 : vide ; addon 5.8 : `M Tools/BuildMod.ps1` (T7). Fins de ligne relues octet par octet (Python, `count(b'\n')`, `count(b'\r\n')`, octets > 127) : | Fichier | Avant T9 | Après T9 | Non-ASCII | BOM | Fin de fichier | |---|---|---|---|---|---| | `LPModInfoSubsystem.cpp` | LF 314 / CRLF 0 | LF 442 / CRLF 0 | 0 = 0 | non | LF | | `LPModInfoSubsystem.h` | LF 63 / CRLF 0 | LF 78 / CRLF 0 | 2 = 2 (`§` l. 1) | non | LF | Noms nouveaux uniques dans `Source/` (`grep -rln "MarkRefusedRow\|TickModManager\|RefusedTint\|NotLoadedSuffix\|ManagerListName"` → les 2 fichiers seulement) ; tout dans l'espace de noms nommé `LPModInfoUi` (pas de collision unity). ## 7. Journal attendu (jeu cuit, Pocolov 5.5 dans `Mods/`) Démarrage, inchangé (T7) : `Modules : lpr_hair_pocolov refuse : moteur : conteneur 4 (jeu 5), registre 17 (jeu 24) ; …`, puis `… non charge : 1 pak(s) demonte(s) …`, puis la fenêtre `Modules : fenetre d'avertissement, 1 mod(s) refuse(s)`. Nouveau, **une fois par ouverture** du gestionnaire : ``` LogLifePlay: Display: Modules : gestionnaire, 1 mod(s) refuse(s) marque(s) ``` Aucune ligne au tri (▲/▼) ni aux images suivantes. Ne doivent pas apparaître : `Modules : gestionnaire, variable UniqueID absente de ModuleTick_C …` (Warning : lignes non marquées) ; `… 0 mod(s) …` alors que Pocolov est dans la liste (clé `UniqueID` non reconnue). Sans mod refusé : aucune ligne « gestionnaire ». ## 8. Plan d'essai (après BUILD par lp-builder et empaquetage ; jeu de référence `LifePlay_Build_5.8_U7_P10`) Le jeu P10 est empaqueté : le code T9 n'y est pas. Compilation `.\Tools\Build.ps1 -Lot U7`, puis empaquetage Development (`Tools\BuildDebug.ps1`) vers un dossier neuf (ex. `LifePlay_Build_5.8_U7_P11`) avec le même `Mods\lpr_hair_pocolov` (cuit 5.5) et le même `Content\Modules\lpr_hair_pocolov`. Sauvegardes du joueur mises de côté comme en P10. 1. Lancer ; fenêtre « Mod compatibility warning » → OK (non-régression T7). 2. Écran titre → Mod Manager. Attendu : « Pocolov Hairstyles (not loaded) », ligne teintée rouge atténué, case grisée dans l'état choisi (cochée ici), auteur inchangé ; les autres lignes inchangées. Capture même cadrage que la capture utilisateur. 3. Survol de la ligne (nom ou case) : infobulle `Not loaded: built with an older engine (format 4, game 5): rebuild the mod for LifePlay 2026.10.2.0` ; le panneau de détail montre toujours le `.lpmod` (survol Blueprint intact). 4. Clic sur la case : sans effet. Clic sur le **nom** : noter si la coche change (point ouvert § 9.6). 5. ▲/▼ sur Pocolov puis sur une autre ligne : marque conservée, pas de double suffixe, aucune nouvelle ligne de journal. 6. Confirm, rouvrir : marque présente dès l'ouverture, une nouvelle ligne Display, pas de double suffixe. 7. Relancer le jeu : Pocolov toujours dans la liste avec le choix laissé (ModEnableds non modifié par le C++). 8. Journal : compter `Modules : gestionnaire` (= nombre d'ouvertures) ; aucun Warning « UniqueID absente ». 9. Non-régression sans mod refusé : `Mods\lpr_hair_pocolov` déplacé (vers `Backup_5.5\U7`) → gestionnaire normal, aucune ligne « gestionnaire ». Ne pas lever le contrôle Moteur avec Pocolov 5.5 (plantage T4). ## 9. Écarts à la fiche et points ouverts 1. Identification : `FLPModGuard::GetVerdicts()` au lieu de `IsRefused()` (même source, donne aussi les raisons de l'infobulle) ; clé double nom du plugin + `MODULE_UNIQUEID` (la ligne porte `MODULE_UNIQUEID`, la garde le nom du plugin ; égaux pour Pocolov, établi § 2). 2. Infobulle : préfixe « Not loaded: » devant les raisons jointes par « ; » (repli « see the mod compatibility warning » si la liste était vide, cas non attendu). 3. Teinte appliquée à toute la ligne (`UUserWidget::SetColorAndOpacity` : texte, case, flèches), pas seulement au texte. 4. Ligne de journal écrite aussi avec N = 0 à l'ouverture (diagnostic) ; Warning unique ajouté si `UniqueID` manque. 5. Ticker dédié par image, enregistré seulement s'il y a un mod refusé ; le ticker 0,5 s existant est inchangé. 6. **Point ouvert** : le clic sur le nom (`HoverButton`, estimé § 2) appelle toujours `GI.ToggleMod` : le joueur peut encore changer son choix enregistré, et la case désactivée suivrait la liaison. Rien n'est chargé pour autant (garde au démarrage). Options si l'orchestrateur veut l'empêcher : (a) laisser ainsi (choix du joueur, recommandé) ; (b) `HoverButton->SetIsEnabled(false)` (perte probable du survol qui affiche le `.lpmod`, à vérifier) ; (c) retirer le délégué Blueprint `OnClicked` de la ligne (intrusif). Non fait sans décision. 7. Observation hors périmètre : `LPHairstyleSubsystem` ajoute un mod refusé à la liste des mods actifs par défaut (`MODULE_DEFAULT_ENABLED`, journal P10 l. 887) : c'est pourquoi Pocolov apparaît coché. ## 10. Compte rendu (sk-protocol) [sk-protocol] Skills présents : sk-protocol, ue-cpp-build, lp-build, lp-ue-compat. 1. **Résultat** : réussi (PASS-STATIC proposé, non compilé) ; 2 fichiers modifiés, aucun Blueprint. 2. **Preuves** : § 2 (relevés `etat/`), § 4 (sources moteur 5.8 `fichier:ligne`). 3. **Chiffres** : T9 = +128 (`.cpp`), +16/−1 (`.h`) (`git diff --no-index --stat`) ; EOL LF conservées (442 et 78 lignes, 0 CRLF). 4. **Incertitudes** : identité du 2ᵉ événement (`HoverButton`) et liaison de la case : estimées ; rendu (teinte, infobulle) : à voir en jeu (§ 8) ; compilation non faite. 5. **Écarts** : § 9 ; fichiers écrits : `D:\Game\LifePlay-master 5.8\Source\LifePlay\LPModInfoSubsystem.cpp`, `D:\Game\LifePlay-master 5.8\Source\LifePlay\LPModInfoSubsystem.h`, ce livrable. 6. Suite : lp-builder doit compiler la copie 5.8 (`.\Tools\Build.ps1 -Lot U7`) puis empaqueter pour l'essai § 8, état : code T9 écrit, non compilé, non commité.