LifePlay-Revival/Docs/Migration/etat/ST/STE/STE-ORG5-I2a-IMPL.md

157 lines
20 KiB
Markdown
Raw Normal View History

Fiche STE-ORG5-I2a — Niveau N3 — Modèle prévu (modèle de l'agent lp-impl) | utilisé claude-opus-5-5 — Agent lp-impl — Skills sk-protocol, ue-cpp-build, lp-build, lp-ue-compat (préchargés) ; sk-change-safety non invoqué (écart, § 4)
# STE-ORG5-I2a-IMPL : affichage des récompenses ORG5 appliquées dans EquipMenu (2026-10-14)
Sources : fiche `fiches\STE-ORG5-I2a.md` ; SPEC `STE-ORG5-I2-SPEC.md` option (a) et « Décisions de l'utilisateur » (Q-I2-1 à Q-I2-4) ; code I1 (`LPBonusRewardSubsystem.h` 119 lignes, `.cpp` 668 lignes, lus en entier avant modification). Moteur lu : `D:\Game\Launcher\Engine\UE_5.8\Engine\Source\Runtime` (ENG ci-dessous). Ni compilation, ni éditeur, ni MCP, ni jeu.
## 1. Fichiers modifiés (chemins, lignes)
Contrôle préalable : `UMG` est dans les dépendances du module, `Source\LifePlay\LifePlay.Build.cs:38` (`PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "UMG" });`) : pas d'arrêt.
`D:\Game\LifePlay-master 5.8\Source\LifePlay\LPBonusRewardSubsystem.h` (119 → 165 lignes) :
- `:34-41` commentaire d'en-tête I2a ; `:52-55` déclarations avancées `UPanelWidget`, `UTextBlock`, `UUserWidget`, `UVerticalBox` ;
- `:86-90` `struct FLPBonusRewardLine { TWeakObjectPtr<UTextBlock> Line; double ExpireAt; }` (une échéance par ligne) ;
- `:132` `FirstWarning` ; `:134-148` `ShowAppliedReward`, `FindCursor`, `FindEquipMenu`, `EnsureEquipMenuHeight`, `EnsureRewardBox`, `ExpireRewardLines`, `ClearRewardDisplay` ;
- `:158` `RewardLines`, `:160` `TWeakObjectPtr<UVerticalBox> RewardBox`, `:162` `bInScene`, `:164` `bDisplayTargetLogged`.
`D:\Game\LifePlay-master 5.8\Source\LifePlay\LPBonusRewardSubsystem.cpp` (668 → 1028 lignes) :
- `:1` en-tête ; `:7-13`, `:18` inclusions UMG / Slate (`Blueprint/UserWidget.h`, `Blueprint/WidgetTree.h`, `Components/CanvasPanelSlot.h`, `PanelWidget.h`, `TextBlock.h`, `VerticalBox.h`, `VerticalBoxSlot.h`, `Styling/CoreStyle.h`) ;
- `:30-39` constantes (`RewardLineSeconds = 20.0`, `MaxRewardLines = 1`, `EquipMenuMinHeight = 500.0`, `RewardFontSize = 16`, `RewardOutlineSize = 2`, noms `Cursor`, `EquipMenu`, `ActionText`, `LPBonusRewardBox`) ; `:42-49` `RemoveLine` (retrait si `IsValid`) ; `:55-62` `RewardFont` ;
- `:205-240` `ReadStrings` : D2, éléments `FString`, `FName` ou `FText` acceptés ;
- `:263-273` `Deinitialize` : `ClearRewardDisplay(false)`, `RewardBox.Reset()` ; `:413` `Tick` appelle `ExpireRewardLines()` après `FirePending()` ;
- `:417-446` `Poll` : `bInScene` ; `ClearRewardDisplay(true)` quand `SexOn` est faux ou absent ; D1 pour les trois avertissements du sondage (`GI.SexOn`, `GI.SexPersons`, `Person.Sex_OrgasmCounts`, `:423`, `:447`, `:466`) ;
- `:574-576` `OnOrgasm` : `ShowAppliedReward(Name, Stats[0], Change)` après le journal de la récompense (donc après `ModifyStat` réussi et après le retour `bDisabled`) ;
- `:579-641` `ShowAppliedReward` ; `:643-668` `FindCursor` ; `:670-686` `FindEquipMenu` ; `:688-719` `EnsureEquipMenuHeight` ; `:721-801` `EnsureRewardBox` ; `:803-820` `ExpireRewardLines` ; `:822-838` `ClearRewardDisplay` ; `:1011-1028` `FirstWarning`, `WarnOnce` réécrit sur `FirstWarning`.
### 1.1 Comportement écrit (décisions de l'utilisateur)
| Décision | Où | Comment |
|---|---|---|
| Emplacement (option a) | `EnsureRewardBox` `:721-801` | `UVerticalBox` nommée `LPBonusRewardBox` ajoutée par `UPanelWidget::AddChild` (en dernier) à `EquipMenu` ; slot `Fill` si `UVerticalBoxSlot` (comme `BonusRewardBox`, SPEC A4) |
| Q-I2-1 lignes seules | `ShowAppliedReward` `:619-634` | `UTextBlock` « `Bonus : %s %s %+.1f` » (prénom, stat, valeur, format du journal), blanc `(1,1,1,1)`, police d'`ActionText` (sinon `FCoreStyle` « Regular ») en taille 16, contour `(2, noir alpha 0,8)`, slot `Padding(10, 5, 10, 0)`, `HAlign_Left` ; ni en-tête ni bouton |
| Q-I2-2 minuterie 20 s par ligne | `:636-639`, `ExpireRewardLines` `:803-820` | échéance `FPlatformTime::Seconds() + 20` par ligne ; relue à chaque passage du ticker existant (0,1 s) ; ligne retirée par `RemoveFromParent` ; remplacement à la récompense suivante (`MaxRewardLines = 1`, `:612-617`) |
| Q-I2-3 hauteur 500 si inférieure | `EnsureEquipMenuHeight` `:688-719` | `UCanvasPanelSlot::GetSize().Y` inférieur à 500 → `SetSize(FVector2D(X, 500))`, si le slot n'est ni en taille automatique ni étiré verticalement ; une ligne de journal à chaque agrandissement |
| Q-I2-4 `bDisabled` coupe tout | `OnOrgasm` (retour existant avant `Pick`) | `ShowAppliedReward` n'est appelé qu'après `ModifyStat` réussi : `bDisabled` vrai → ni application, ni case, ni ligne ; aucune ligne ne survit à la scène (garde 3) |
### 1.2 Gardes de Big Boss : où elles sont tenues
| Garde | Tenue | Lignes |
|---|---|---|
| (1) `Cursor` ou `EquipMenu` introuvable : pas de plantage, avertissement unique, récompense gardée | l'affichage vient **après** `ModifyStat` et le journal ; chaque échec rend sans effet, un avertissement par cause (`FirstWarning`) : `GI.Cursor` (propriété absente), `GI.Cursor.Value` (vide, pas un `UUserWidget`, ou sans `WidgetTree`), `Cursor.EquipMenu` (absent ou pas un `UPanelWidget`, classe trouvée journalisée), `LPBonusRewardBox.Type`, `EquipMenu.AddChild` ; hauteur : `EquipMenu.Slot`, `EquipMenu.Slot.Layout` (affichage maintenu) | `.cpp:574-576`, `:643-686`, `:762-783`, `:696-713` |
| (2) aucun ajout en double si `EquipMenu` est reconstruit | case retrouvée **avant** toute création, dans l'ordre : pointeur faible `RewardBox` encore enfant de cet `EquipMenu` ; enfant d'`EquipMenu` nommé `LPBonusRewardBox` ; objet de ce nom dans l'arbre de `Cursor` (`FindObjectFast`, case détachée : reprise, pas de `NewObject` du même nom) ; création seulement sinon. Pointeurs faibles seulement (`RewardBox`, lignes) | `.cpp:726-761`, `.h:86-90`, `:160` |
| (3) minuteries nettoyées en fin de scène et dans `Deinitialize` | `Poll` : `SexOn` faux (ou absent) → `ClearRewardDisplay(true)` (lignes retirées, échéances vidées) ; `Deinitialize` : ticker retiré, `ClearRewardDisplay(false)` (échéances vidées, widgets laissés au monde qui se ferme), `RewardBox.Reset()` ; tempo fini après la scène : pas de ligne (`bInScene`, `:583-588`) | `.cpp:263-273`, `:417-446`, `:822-838` |
### 1.3 Appels moteur et preuves (UE 5.8, ENG)
| Appel | Source |
|---|---|
| `UUserWidget::GetWidgetFromName(const FName&) const` = `WidgetTree ? WidgetTree->FindWidget(Name) : nullptr` ; `WidgetTree` public | `UMG\Public\Blueprint\UserWidget.h:1426`, `:1524` ; `UMG\Private\UserWidget.cpp:1259-1262` |
| `UWidgetTree::FindWidget(const FName&)` parcourt l'arbre (`ForEachWidgetUntil`) : un widget détaché n'est pas trouvé | `UMG\Public\Blueprint\WidgetTree.h:34` ; `UMG\Private\WidgetTree.cpp:40-54` |
| `UWidgetTree::ConstructWidget<T>(TSubclassOf<T>, FName = NAME_None)` `[[nodiscard]]` = `NewObject<T>(this, Class, Name, RF_Transactional)` pour un `UWidget` | `WidgetTree.h:105-122` (`:117`) |
| `UPanelWidget::GetChildrenCount`, `GetChildAt`, `AddChild` (rend `UPanelSlot*`), `ClearChildren` | `UMG\Public\Components\PanelWidget.h:28`, `:36`, `:59`, `:122` |
| `UWidget::Slot` public (`TObjectPtr<UPanelSlot>`), `GetParent()`, `RemoveFromParent()` | `UMG\Public\Components\Widget.h:222` (`public:`), `:264`, `:769`, `:776` |
| `UVerticalBox::AddChildToVerticalBox` → `UVerticalBoxSlot*` ; `UVerticalBoxSlot::SetPadding`, `SetSize`, `SetHorizontalAlignment` | `VerticalBox.h:30` ; `VerticalBoxSlot.h:53`, `:58`, `:63` |
| `FSlateChildSize(ESlateSizeRule::Type)`, `ESlateSizeRule::Fill` | `UMG\Public\Components\SlateWrapperTypes.h:155`, `:173`, `:118-119` |
| `UTextBlock::SetText(FText)`, `SetColorAndOpacity(FSlateColor)`, `GetFont() const` (référence constante), `SetFont(FSlateFontInfo)` | `TextBlock.h:103`, `:114`, `:148`, `:156` |
| `FSlateFontInfo::OutlineSettings`, `Size` (float) ; `FFontOutlineSettings(int32, FLinearColor)` ; `OutlineSize` int32, `OutlineColor` | `SlateCore\Public\Fonts\SlateFontInfo.h:160`, `:175`, `:78`, `:42`, `:67` |
| `FCoreStyle::GetDefaultFontStyle(const FName, const float, …)` | `SlateCore\Public\Styling\CoreStyle.h:51` |
| `UCanvasPanelSlot::SetSize(FVector2D)`, `GetSize()`, `GetAnchors()`, `GetAutoSize()` ; `FAnchors::IsStretchedVertical()` | `CanvasPanelSlot.h:112`, `:116`, `:132`, `:148` ; `Slate\Public\Widgets\Layout\Anchors.h:78` |
| `FindObjectFast<T>(UObject* Outer, FName Name, EFindObjectFlags = None, EObjectFlags = RF_NoFlags)` | `CoreUObject\Public\UObject\UObjectGlobals.h:2059-2061` |
| `TPropertyTypeFundamentals::GetPropertyValue(void const*)` (statique) ; `FScriptArrayHelper(const FArrayProperty*, const void*)` ; `FNameProperty` = `TProperty_WithEqualityAndSerializer<FName, FProperty>` ; `FTextProperty` = `TProperty<FText, FProperty>` ; `FStrProperty` = `TProperty_WithEqualityAndSerializer<FString, FProperty>` | `UnrealType.h:1488`, `:4285`, `:3723` ; `TextProperty.h:20` ; `StrProperty.h:10` + `StrProperty.h.inl:27` |
Précédents du dépôt suivis : police copiée d'un voisin `LPColorGradeSubsystem.cpp:295` ; `TWeakObjectPtr<UTextBlock>` sur déclaration avancée dans un en-tête `UCLASS` `LPColorGradeSubsystem.h:20`, `:53` ; `Slot->SetSize(FSlateChildSize(ESlateSizeRule::Fill))` `LPColorGradeSubsystem.cpp:217`.
### 1.4 Choix de la minuterie (demandé par la fiche, rubrique 4)
`FTSTicker` existant (déjà ajouté dans `Initialize`, retiré dans `Deinitialize`) plutôt que `FTimerManager` du monde : (a) le sous-système appartient au `GameInstance`, qui survit aux changements de niveau, alors que les minuteries d'un monde disparaissent avec lui ; (b) aucune poignée de plus à garder : la « poignée » d'une ligne est son entrée dans `RewardLines` (pointeur faible + échéance), vidée en fin de scène et dans `Deinitialize` ; (c) même horloge (`FPlatformTime::Seconds`) que le tempo de 0,2 s d'I1. Conséquences (estimé) : délai en temps réel, insensible à une pause ou une dilatation du temps de jeu ; précision 0,1 s (retrait entre 20,0 et 20,1 s).
### 1.5 Mesures (commandes et sorties brutes)
Avant modification :
```
git -C "D:/Game/LifePlay-master 5.8" status --porcelain -- Source Content Config ; rev-parse --short HEAD ; md5sum …
M Content/BPs/GI.uasset
M Content/BPs/PC.uasset
M Content/BPs/Person.uasset
M Content/BPs/Room.uasset
M Content/UI/TopRight.uasset
M Source/LifePlay/LPDebugSubsystem.cpp
M Source/LifePlay/LPDebugSubsystem.h
M Source/LifePlay/LPDirectControlSubsystem.cpp
M Source/LifePlay/LPDirectControlSubsystem.h
M Source/LifePlay/LPOutdoorRoomSubsystem.cpp
M Source/LifePlay/LPOutdoorRoomSubsystem.h
M Source/LifePlay/LifePlay.cpp
?? Source/LifePlay/LPBonusRewardSubsystem.cpp
?? Source/LifePlay/LPBonusRewardSubsystem.h
?? Source/LifePlay/LPCameraModeSubsystem.cpp
?? Source/LifePlay/LPCameraModeSubsystem.h
?? Source/LifePlay/LPSaveRelaunch.cpp
?? Source/LifePlay/LPSaveRelaunch.h
0745917
aa56bd8a655ecc55818a4aabeae3c028 *D:/Game/LifePlay-master 5.8/Source/LifePlay/LPBonusRewardSubsystem.h
20abc05064db9fbce3440b5057c6af8a *D:/Game/LifePlay-master 5.8/Source/LifePlay/LPBonusRewardSubsystem.cpp
```
Après modification (`for f in …; do printf …; cat "$f" | tr -cd '\r' | wc -c; …; cat "$f" | wc -l; done; md5sum …; git … status --porcelain -- Source Content Config`) :
```
LPBonusRewardSubsystem.h CR=0
LPBonusRewardSubsystem.h lignes=165
LPBonusRewardSubsystem.cpp CR=0
LPBonusRewardSubsystem.cpp lignes=1028
87177c7a53fb5b5cc946e904060a3aaa *LPBonusRewardSubsystem.h
16147626d2fa8a11dac8747cdbe12e9a *LPBonusRewardSubsystem.cpp
```
suivi des 18 mêmes lignes de `git status` qu'avant, dans le même ordre (aucun autre fichier touché ; les deux fichiers restent `??`).
Grep d'un nom d'utilisateur et d'un chemin de profil sur `LPBonusRewardSubsystem.*` (motif non recopié ici : il contient le nom) : `Found 0 total occurrences across 0 files.` Grep `AddToViewport|AddViewportWidgetContent|CreateWidget|UButton` : aucune ligne dans le `.cpp` (le motif combiné n'a sorti que `GetWidgetFromName`, `ConstructWidget`, `LPBonusRewardBox` et les appels internes, `.cpp:39`, `:57`, `:621`, `:674`, `:759`).
## 2. Écarts à la SPEC
- E1 (décision Q-I2-2, voulu) : SPEC 2.2 « Durée : … Aucune minuterie » remplacé par une échéance de 20 s par ligne, lue par le ticker existant (1.4), en plus du retrait en fin de scène et du remplacement.
- E2 : SPEC 2.2 points 3 et 4 écrivent `NewObject<UVerticalBox>(Equip, …)` et `NewObject<UTextBlock>(Box)` ; écrit : `Cursor->WidgetTree->ConstructWidget<…>` (outer = arbre de widgets de `Cursor`, voie UMG de création à l'exécution, `NewObject` appelé dedans, `WidgetTree.h:117`). La boîte est créée dans `EnsureRewardBox`, appelée seulement par `ShowAppliedReward` : critère V5-4 à lire ainsi.
- E3 (garde 2 de Big Boss) : recherche de la case élargie (enfant d'`EquipMenu` par nom, puis objet de ce nom dans l'arbre) au lieu du seul test « `RewardBox` invalide ou parent différent » ; une case retrouvée ou reprise est vidée (`ClearChildren`) et les échéances remises à zéro.
- E4 : remplacement par retrait des lignes suivies (`MaxRewardLines = 1`, `RemoveFromParent`) au lieu de `Box->ClearChildren()` à chaque affichage ; même effet visible (une ligne au plus, V6-3) ; passer la constante à 3 donnerait l'empilement, chaque ligne gardant ses 20 s.
- E5 : hauteur 500 seulement si le slot n'est ni en taille automatique ni étiré verticalement (sinon `GetSize().Y` n'est pas une hauteur) ; avertissement unique si le slot n'est pas un `UCanvasPanelSlot` ; ligne de journal `hauteur d'EquipMenu portee de X a 500` à chaque agrandissement (non prévue par la SPEC).
- E6 : pas de ligne si la scène s'est terminée pendant le tempo de 0,2 s (`bInScene`, journal `Verbose`) ; la SPEC ne traitait pas ce cas.
- E7 : `Deinitialize` vide les échéances sans retirer les widgets (choix de sûreté en fermeture) ; la fin de scène, elle, retire les lignes.
- E8 : avertissements uniques **par cause** (7 clés, 1.2), comme I1 et la SPEC R1, et non un seul avertissement global.
- E9 : police de repli `FCoreStyle::GetDefaultFontStyle("Regular", 16)` si `ActionText` est absent (la SPEC ne prévoyait pas de repli) ; `bApplyOutlineToDropShadows` laissé par défaut.
- E10 : `Poll` vide aussi l'affichage quand la propriété `SexOn` est absente.
- E11 (hors SPEC, permis par la fiche rubrique 3) : D1 corrigé par `FirstWarning` pour le nouveau code et les trois avertissements de `Poll` (sondé toutes les 0,1 s) ; D2 corrigé (`ReadStrings` accepte `FName` et `FText`).
## 3. Instantané (lot, manifeste)
Aucun instantané : `Tools\Snapshot.ps1` copie vers `D:\Game\LifePlay_Backup_5.5\<Lot>\` (`Tools\Snapshot.ps1:6-7`, `:22`, `:26`) et Glob `**/LPBonusRewardSubsystem.cpp` sous `D:\Game\LifePlay_Backup_5.5` → `No files found` ; idem sous `Saved\` de la copie 5.8. Les deux fichiers sont non suivis (`??`) : **la version I1 n'est récupérable ni par git ni par une sauvegarde**. Empreintes de la version I1 : `.h` `aa56bd8a655ecc55818a4aabeae3c028`, `.cpp` `20abc05064db9fbce3440b5057c6af8a` ; son texte complet n'existe plus que dans la transcription de cette session (lecture intégrale au premier tour). Le repli SPEC § 3 « `git restore` des deux fichiers » est donc impossible tant qu'I1 n'est pas commité ; le repli reste possible en retirant les appels `ShowAppliedReward` (`.cpp:576`), `ClearRewardDisplay` (`:270`, `:429`, `:440`) et `ExpireRewardLines` (`:413`).
## 4. Ce qui n'a pas été fait
- Aucune compilation (build COMMUN-8, fiche BUILD de `lp-builder`), aucun essai en jeu, aucun éditeur.
- `sk-change-safety` (« à invoquer », fiche rubrique 2b) n'a pas été invoqué et aucun instantané n'a été pris avant la modification (§ 3) : écart de méthode.
- D1 non corrigé aux appels de `WarnOnce` liés à un orgasme (`GI.CurrentTags`, `isPlayer`, `PlayerCharacter`, `IsInterestedIn`, `QueryStat`, `ModifyStat`, `Name_First`) : message encore construit à chaque appel, mais ces appels sont rares (un par orgasme).
- Pas d'en-tête vert, pas de bouton, pas de grille V-1 (Q-I2-1, SPEC § 5).
## 5. Points à confirmer en VAL
- V5-2 : compilation 5.8 sans erreur ni avertissement nouveau sur les deux fichiers (code jamais compilé).
- Compilation : `TWeakObjectPtr<UTextBlock>` dans `FLPBonusRewardLine` (`.h:88`) sur déclaration avancée, comme `LPColorGradeSubsystem.h:53`.
- Compilation : `FindObjectFast<UObject>(Tree, BoxName)` à deux arguments (`UObjectGlobals.h:2059`).
- Compilation : `ConstructWidget<UTextBlock>(UTextBlock::StaticClass())` et `<UVerticalBox>(…, BoxName)` (`[[nodiscard]]`, résultat utilisé).
- Compilation : `GetPropertyValue(const void*)` sur `FStrProperty`, `FNameProperty`, `FTextProperty` (`.cpp:227-235`).
- Compilation : `Equip->Slot.Get()`, `Cursor->WidgetTree.Get()`, `FCoreStyle::GetDefaultFontStyle("Regular", float)`.
- Compilation unitaire : fonctions `RemoveLine` et `RewardFont` dans l'espace de noms nommé `LPBonusReward` (pas de collision C2374 attendue).
- V5-3 : 0 `AddToViewport`, `AddViewportWidgetContent`, `CreateWidget`, `UButton` dans le `.cpp`.
- V5-4 (lu avec E2) : création des widgets seulement dans `ShowAppliedReward` / `EnsureRewardBox`, jamais dans `Poll` ni `Initialize`.
- V5-5 : appel d'affichage après `ModifyStat` réussi et après le retour `bDisabled` (`.cpp:574-576`).
- V5-6 (lu avec E3 à E5) : `GI.Cursor` → `GetWidgetFromName("EquipMenu")` → `Cast<UPanelWidget>` ; police 16, contour (2, noir 0,8), marge (10, 5, 10, 0) ; 500 seulement si inférieur.
- V6-2 : journal `Bonus : affichage dans EquipMenu (classe VerticalBox, 4 enfants)` une fois par session (4 = les 3 enfants du dump + la case, compte pris après l'ajout).
- V6-2 : journal `Bonus : hauteur d'EquipMenu portee de 350 a 500` au premier affichage (puis à chaque `Cursor` recréé).
- V6-1 : ligne visible menu ouvert, sous la grille d'objets, alignée à gauche (+ 10), blanche à contour noir.
- V6-8 (nouveau, Q-I2-2) : la ligne disparaît seule entre 20,0 et 20,1 s après son apparition, menu ouvert ou fermé puis rouvert.
- V6-3 : second orgasme avant 20 s : ligne remplacée, nouvelle échéance de 20 s.
- V6-4 : fin de scène puis nouvelle scène : aucune ligne.
- V6-5 : `bDisabled=True` : aucune case créée, aucune ligne, aucun journal d'affichage.
- V6-7 : 0 plantage, 0 `Accessed None`, aucun avertissement `Bonus :` de widget nouveau par rapport à COMMUN-6.
- Mise en page : `ScrollBox_3` (Fill) partage désormais la place restante avec la case (Fill), même vide : vérifier que la liste de bondage reste lisible à 500 de haut.
- Pause éventuelle du jeu : le délai de 20 s court en temps réel (1.4).
## 6. Fichiers à commiter
Dépôt du jeu `D:\Game\LifePlay-master 5.8`, branche `ue5.8` ; les deux sources sont non suivies : I1 et I2a partent ensemble (SPEC R7).
- `A Source/LifePlay/LPBonusRewardSubsystem.h`
- `A Source/LifePlay/LPBonusRewardSubsystem.cpp`
- `A Docs/Migration/etat/ST/STE/STE-ORG5-I2a-IMPL.md`
Tours : 24 / 40 (comptés à la main, remise du compte rendu comprise ; `python -I Tools/FicheStats.py --lot STE` fait foi)
## Constats
- F-I2a-1 : les fichiers I1 sont non suivis et sans instantané ; toute nouvelle fiche qui les modifie avant le commit du gardien efface la version précédente sans retour possible. Recommandation : commit I1 + I2a avant toute autre fiche sur ces fichiers.
- F-I2a-2 : `FStrProperty` n'a pas de ligne `class FStrProperty` : la classe est produite par macro (`StrProperty.h:10`, `StrProperty.h.inl:27`) ; une recherche de la déclaration doit passer par `StrProperty.h.inl`.
- F-I2a-4 : la première version de ce livrable recopiait le motif du contrôle « aucun nom d'utilisateur » ; détecté par le même Grep (1 occurrence) et corrigé (même piège que SPEC A.12).
- F-I2a-3 : `NewObject` d'un nom déjà pris dans le même outer remplace l'objet existant (estimé, non relu dans `StaticAllocateObject`) : d'où la recherche par `FindObjectFast` avant `ConstructWidget` avec un nom fixe.