LifePlay-Revival/Docs/Migration/etat/CAM/CAM-B2p-H-VAL.md

204 lines
34 KiB
Markdown
Raw Normal View History

Fiche CAM-B2p-H-VAL — Niveau N1 — Modèle prévu sonnet | utilisé claude-sonnet-5-5 — Agent lp-validator — Skills sk-protocol, sk-recount, debug-verification, lp-validate
# CAM-B2p-H-VAL : validation statique de G-e (pose sur le lit), G-f (surfaces admises), G-g (clic molette), maillage dans les refus, renommage de `Reach` (2026-10-14)
Portée : VAL **statique** (aucune compilation, aucun jeu, aucun éditeur, aucun commandlet, aucun MCP, aucune écriture git), par un validateur distinct de l'auteur.
Copie `D:\Game\LifePlay-master 5.8`, branche `ue5.8` (`git branch --show-current` : `ue5.8`), UE 5.8, BuildSettingsVersion V7 (`/we4456 /we4458 /we4459`).
`LPCameraModeSubsystem.cpp` et `.h` sont **non suivis** par git (`git status --short` : `??`), donc pas de tag `CAM-B2p-H-avant` : chaque point est jugé sur le diff de son instantané à l'instantané suivant (`git diff --no-index`), comme pour `CAM-B2p-G-VAL.md`.
Convention des instantanés : `CAM-B2p-Hn` est l'état **d'avant** le point Hn ; le point Hn est donc le diff `CAM-B2p-Hn` vers `CAM-B2p-H(n+1)` (H5 : vers l'état final).
Niveaux de preuve : établi = lecture ou commande rejouée aujourd'hui ; estimé = déduit ; incertain = non vérifié (essai nommé).
## 1. Fiches amont relues
- `Docs/Migration/etat/CAM/CAM-B2p-H-IMPL.md` (100 lignes, lue en entier ; seule IMPL du lot).
- `Docs/Migration/etat/CAM/CAM-B2p-G-SPEC.md` § 5 (l. 94-165 : DIAG 5.1, G-e 5.2, G-f 5.3, G-g 5.4, essais 5.5).
- `Docs/Migration/etat/CAM/CAM-B2p-G-VAL.md` (183 lignes, lue en entier : N7 et forme d'une VAL statique C++ de ce lot).
- Lu en lecture seule : `Source/LifePlay/LPShowCoupleSubsystem.cpp` l. 25-70 (`IsBedMesh` l. 38-62), `Source/LifePlay/LPDirectControlSubsystem.cpp` l. 48-62 (espace anonyme, `Reach` l. 58), `Intermediate/Build/Win64/x64/*/*/LifePlay/Module.LifePlay.cpp`.
- Gabarit VAL lu : `D:\Game\LifePlay-Sandbox\Tools\claude\skills\lp-doc\gabarits.md`.
- Écart de gabarit de l'IMPL (N5 ci-dessous) : l'IMPL n'a ni tête commune, ni section « Écarts à la SPEC », ni « Ce qui n'a pas été fait », ni « Points à confirmer en VAL », ni ligne `Tours :`. Les points à confirmer sont donc pris dans ses § 3 (risques R1 à R4) et § 4 (essais CAM-81 à CAM-85), plus la liste de la fiche.
## 2. Points à confirmer des IMPL (un par ligne)
| # | Point | Verdict | Preuve |
|---|---|---|---|
| P-H1 | N7 : `Reach` renommé `ObstacleReach`, rien d'autre ; symbole STG confirmé ; aucun `Reach` local restant | **PASS-STATIC** | § 3 contrôles 5 à 7 ; `.cpp` l. 969-974 ; `LPDirectControlSubsystem.cpp:58` |
| P-H2 | `ComponentMesh`, `IsAdmittedBed`, `DescribeComponent` ; règle des lits identique à `IsBedMesh` ; `CoupleSpotFree` même comportement, texte du bloqueur changé | **PASS-STATIC** | `.cpp` l. 478-500, 526, 530 ; `LPShowCoupleSubsystem.cpp:56-59` |
| P-H3 | G-e : tracé `ECC_Pawn`, `OutSurface` nul pour le sol, E2 conservé, racine la plus basse à surface + 3 cm, ligne de prise ; écart SPEC (racine la plus basse) | **PASS-STATIC**, écart **jugé acceptable** | `.cpp` l. 133-134, 465-476, 1210-1241, 1296-1306, 1361-1369 |
| P-H4 | G-f : sol et lits seuls en pièce connue ; couple immobile sinon ; une ligne par changement ; remise à vide à la prise ; pièce cachée sans G-f | **PASS-STATIC** | `.cpp` l. 1288, 1364-1395 ; `.h` l. 121-122 |
| P-H5 | G-g : `TickPanelClick` pendant le sexe hors menu seulement ; hors sexe rien ne change ; 0,2 s / 5 px ; `FindFunction` + `NumParms == 0` + `ProcessEvent(…, nullptr)` ; appui né dans un menu | **PASS-STATIC** (réserve N1) | `.cpp` l. 176-178, 987-992, 998-1003, 1027-1044, 1122-1155 ; `.h` l. 137-144 |
| R1 (IMPL § 3) | Le tracé Pawn peut toucher la tête de lit (même maillage) : couple posé dessus | **reporté** V7 (CAM-81) | géométrie des assets non lisible en statique ; code conforme à la règle de la SPEC (aucun plafond de hauteur, « non fait » annoncé) |
| R2 | Capsules des PNJ sur le canal Pawn : place refusée | **PASS-STATIC** | une capsule est un `UCapsuleComponent` : `ComponentMesh` rend nul (l. 481-482), `IsAdmittedBed` faux (l. 489-491), refus l. 1376-1385 |
| R3 | Curseur sans position pendant le clic molette : seul le délai compte | **PASS-STATIC** | `.cpp` l. 1127, 1132, 1139-1140 : le mouvement n'est mesuré que si `bMiddleHadMouse && bPanelMouse` |
| R4 | `ProcessEvent` de `LeftMouseSexOn` hors boucle du monde (FTSTicker) | forme de l'appel **PASS-STATIC** ; contexte d'appel **incertain** : reporté V7 | précédent d'appel identique `LPDirectControlSubsystem.cpp:595` (`Player->ProcessEvent(Function, nullptr)`) ; contexte (tick du monde ou ticker) non relu ; essai : CAM-83 |
| CAM-81 à CAM-84 | Essais en jeu (lit, commode, clics, hors sexe) | **reporté** V7 (build COMMUN-12) | aucune exécution possible ici |
| CAM-85 | Ligne `place refusee` avec maillage ; compilation sans C4456 / C4458 / C4459 | ligne : **PASS-STATIC** (l. 530 puis 1399) ; compilation : **BLOCKED-compilation** (BUILD) | § 3 contrôles 9 à 11 |
## 3. Critères rejoués (commande → résultat)
Les commandes PowerShell sont des fichiers de travail du dossier temporaire de session (`scratchpad`), pas des fichiers du dépôt.
| # | Contrôle | Commande / lecture | Résultat brut | Verdict |
|---|---|---|---|---|
| 1 | MD5 final | `Get-FileHash -Algorithm MD5` | `.cpp` `210B665AA8D5D628B8C1AC429A339B74` ; `.h` `239DA6094E7401E0E3B3699A635A8150` | PASS : égalent la fiche et l'IMPL |
| 2 | Lignes, fins de ligne, BOM | lecture des octets (comptage de 13 et de 10, sans `grep -c`) | `.cpp` size 73384, CR=0, LF=1687, premiers octets `2f2f20`, dernier `0a` ; `.h` size 8471, CR=0, LF=152, `2f2f20`, `0a` | PASS : 1687 et 152 lignes, LF pur, sans BOM, fin de ligne finale |
| 3 | MD5 et lignes des instantanés | idem sur `D:\Game\LifePlay_Backup_5.5\CAM-B2p-H1` à `H5` | H1 `A5F041FF…`/`00D43993…` (1575/141 lignes) ; H2 `747FA007…` (1576) ; H3 `756C95F0…` (1596) ; H4 `91FD48F0…` (1624), `.h` `BBB45AD0…` (142) ; H5 `48F20ED7…` (1637), `.h` `924B63F8…` (144) ; CR=0 partout | PASS : égalent le tableau de l'IMPL § 0 et chaque `manifest.csv` ; chaque dossier contient 3 fichiers (`manifest.csv`, `.cpp`, `.h`) |
| 4 | Diff brut et répartition | `git diff --no-index --numstat` puis recompte par `-U0` (lignes `+`/`-` hors en-têtes) | voir tableau ci-dessous | PASS : somme des points = global, à une ligne remplacée près, expliquée |
| 5 | `Reach` restant | `Grep \bReach\b` sur `Source/LifePlay` (`*.cpp`, `*.h`) | `LPCameraModeSubsystem.cpp:970` (commentaire) ; `LPDirectControlSubsystem.cpp:58`, `:1357`, `:1379` | PASS : plus aucun `Reach` **de code** dans CameraMode (l. 970 est un commentaire) |
| 6 | Symbole STG | `Read LPDirectControlSubsystem.cpp` l. 48-62 | l. 53 `namespace`, l. 54 `{`, l. 58 `const float Reach = 150.f;` : espace de noms anonyme | PASS : confirmé |
| 7 | Ordre des unités unifiées | `Grep` de `Module.LifePlay.cpp` (UnrealEditor Development, UnrealGame Shipping) | `LPDirectControlSubsystem.cpp` inclus (l. 37 / 36), `LPShowCoupleSubsystem.cpp` (l. 53 / 52) ; `LPCameraModeSubsystem.cpp` absent des unités générées (compilé seul quand il est modifié) | PASS : `ObstacleReach` écarte tout C4459 quel que soit l'ordre des unités |
| 8 | Signatures et symboles | `Grep` des 24 noms nouveaux sur `Source/LifePlay` ; `Grep namespace LPCameraModeLocal` sur `Source` | `TickPanelClick` : `.h:139` et `.cpp:1122` (même signature), un seul appel `.cpp:1043` ; `namespace LPCameraModeLocal` : un seul fichier | PASS : pas de doublon de symbole externe |
| 9 | C4456 / C4457 (locaux englobants) | lecture de `TickCoupleMouse` (l. 1166-1427), `TickSexCamera` (980-1121), `TickPanelClick`, `CoupleLowestRoot`, `IsAdmittedBed`, `DescribeComponent` | détail ci-dessous | PASS-STATIC |
| 10 | C4458 (membres de la classe) | lecture du `.h` en entier (152 lignes) contre chaque nouveau local et paramètre | détail ci-dessous | PASS-STATIC |
| 11 | C4459 (globaux et espaces anonymes de tous les .cpp) | `Grep` des noms nouveaux dans tout `Source/LifePlay` | seuls hits hors CameraMode : `Lowest` (`LPDebugSubsystem.cpp:508`, local de fonction), `Toggle` (`LPModInfoSubsystem.cpp:113, 288`, locaux), `Refused` (`LPHairstyleSubsystem.cpp:230`, local) | PASS-STATIC (résidu engine : N6) |
| 12 | Types, includes, API 5.8 | lectures du moteur (détail ci-dessous) | aucune erreur de type trouvée | PASS-STATIC |
| 13 | Formats `UE_LOG` / `Printf` | comptage `%` contre arguments (détail ci-dessous) | tous cohérents | PASS-STATIC |
| 14 | Non-régression B2p-C à B2p-G | liste des hunks (§ 4) contre les fonctions des lots précédents | aucun hunk dans `TickCoupleBounds`, `TickSceneCamera`, `ComputeLabel`, `NotifyCoupleTeleported` (G3 l. 1321-1335 hors diff) | PASS-STATIC |
| 15 | Fichiers STG / STE | `git status --short` et dates ; instantanés ne contenant que CameraMode | `LPDirectControlSubsystem.*` modifiés 12:05:58, `LPShowCoupleSubsystem.*` 12:43:20 ; CameraMode 14:08:43 ; instantanés H1 à H5 de 14:06:24 à 14:08:05 | PASS : aucune écriture STG/STE après le début de H (établi par les dates ; l'instantané ne les contient pas, donc pas de preuve par octets) |
### Contrôle 4 : bilan brut et répartition par point
| Point | Diff | `.cpp` ajoutées / retirées (numstat et `-U0` concordent) | Lignes `.cpp` avant vers après (LF) | `.h` |
|---|---|---|---|---|
| H1 | H1 vers H2 | +3 / −2 | 1575 vers 1576 (+1, ok) | 0 |
| H2 | H2 vers H3 | +26 / −6 | 1576 vers 1596 (+20, ok) | 0 |
| H3 | H3 vers H4 | +41 / −13 | 1596 vers 1624 (+28, ok) | +2 / −1 (141 vers 142) |
| H4 | H4 vers H5 | +15 / −2 | 1624 vers 1637 (+13, ok) | +2 / −0 (142 vers 144) |
| H5 | H5 vers final | +52 / −2 | 1637 vers 1687 (+50, ok) | +8 / −0 (144 vers 152) |
| Somme | | **+137 / −25** | 1575 vers 1687 (+112, ok) | **+12 / −1** |
| Global | H1 vers final | **+136 / −24** | +112, ok | **+12 / −1** |
- Écart de la somme au global : **+1 / −1** sur le `.cpp`, expliqué : la ligne `if (TraceDown(Target, SurfaceZ, Surface))` est ajoutée par H3 (en remplacement de `if (bGrabHasHeight && TraceDown(Target, SurfaceZ))`) puis retirée par H4 (remplacée par `const bool bSurface = …;` et `if (bSurface)`) : elle compte une fois en ajout et une fois en retrait dans la somme, et jamais dans le global. Net identique (+112). Le `.h` retombe exactement (12 / 1).
- Retraits de H3 (13), recomptés par nature : tracé vertical et E2, 6 (2 lignes de commentaire E2, signature du lambda, ligne du canal `ECC_Visibility`, `if (bOverFloor)`, `OutZ = FMath::Max(…)`) ; prise, 4 (`bGrabHasHeight = TraceDown(Pivot, SurfaceZ);`, `GrabHeight = …`, format du `UE_LOG`, ligne du ternaire `Printf`) ; glisser, 3 (commentaire, `if (bGrabHasHeight && TraceDown(…))`, `Target.Z = SurfaceZ + GrabHeight;`) : 6 + 4 + 3 = 13, conforme au diff lu. Les retraits de H2 (6) sont les 6 lignes de `CoupleSpotFree` déplacées dans `ComponentMesh` / `IsAdmittedBed` / `DescribeComponent` ou remplacées ; ceux de H4 (2) et de H5 (2) sont des lignes remplacées par une version plus longue (`if` de G1 devenu `else if`, commentaires).
- Sorties collées (recompte `-U0`) : `cpp H1 vers FINAL : +136 -24` et `h H1 vers FINAL : +12 -1` ; `numstat` : `136 24` et `12 1`.
### Contrôles 9 à 11 : ombrage (détail)
Nouveaux noms et leur portée :
| Nom | Portée | C4456/C4457 | C4458 | C4459 |
|---|---|---|---|---|
| `Lowest`, `Person`, `Character`, `Mesh` | `CoupleLowestRoot` (l. 466-476), `Lowest` hors boucle, les trois autres dans la boucle | aucun homonyme dans la fonction (paramètre `Couple` seul) | aucun membre (fonction de l'espace de noms) | `Lowest` seulement local ailleurs (`LPDebugSubsystem.cpp:508`) ; `Person`, `Character`, `Mesh` existaient déjà comme locaux compilés (l. 1057, 1268-1269) |
| `Comp`, `Mesh`, `MeshComp`, `MeshName` | `ComponentMesh`, `IsAdmittedBed`, `DescribeComponent` (l. 479-500) | aucun homonyme (un `Mesh` par fonction) | sans objet | `Comp`, `MeshComp`, `MeshName` existaient (code compilé de `CoupleSpotFree`) |
| `OutSurface` | paramètre du lambda `TraceDown` (l. 1218) | aucun `OutSurface` englobant | aucun membre | aucun hit hors CameraMode |
| `GrabSurface` | bloc de prise `CoupleGrabMode == 0` (l. 1298) | aucun homonyme (`SurfaceZ` voisin, nom distinct) | aucun membre | aucun hit |
| `Surface`, `bSurface`, `Refused` | bloc du glisser (l. 1364-1380) | locaux englobants relus (`Input`, `bLeft`…, `World`, `Query`, `TraceCursor`, `FloorComponent`, `RoomActor`, `bFloorKnown`, `FloorBox`, `TraceDown`, `MouseX`, `MouseY`, `bMouse`, `bHeld`, `Aim`, `Target`, `Interior`, `Area`, `Margin`, `Blocker`, `LeadCharacter`, `LeadMesh`, `RootOffset`) : aucun | `LastSurfaceRefused` est le membre, `Refused` en est distinct | `Refused` : local ailleurs (`LPHairstyleSubsystem.cpp:230`, indentation d'une tabulation = corps de fonction) |
| `bMiddle`, `PanelMouseX`, `PanelMouseY`, `bPanelMouse`, `Toggle` ; paramètres `PC`, `DeltaTime` | `TickPanelClick` (l. 1122-1155) | aucun doublon dans la fonction | membres `bMiddleWasDown`, `bMiddleHadMouse`, `bMiddleMoved`, `MiddleHeld`, `MiddleStart` : noms différents des locaux ; aucun membre `PC` ni `DeltaTime` | `Toggle` : locaux de fonction ailleurs (`LPModInfoSubsystem.cpp:113, 288`) |
| `ObstacleReach` | `RecenterSceneCamera` (l. 972) | aucun homonyme | aucun membre | aucun hit |
- Aucun nom **de portée globale ou d'espace anonyme** n'est ajouté : constantes et fonctions sont dans `LPCameraModeLocal` (espace nommé) ; les constantes `const` ont de plus un lien interne. `Grep` d'un `Toggle` / `Surface` / `Lowest` / `Refused` isolé sur une ligne dans `Engine/Classes/*.h` (5.8) : 0 résultat (limité à ce dossier, voir N6).
### Contrôle 12 : types, includes, API (sources 5.8 lues)
- `UObject::FindFunction(FName) const` : `CoreUObject/Public/UObject/Object.h:1258` ; `ProcessEvent(UFunction*, void*)` : `Object.h:1490` ; `UFunction::NumParms` (`uint8`) : `Class.h:2574` ; précédents du projet : `LPDirectControlSubsystem.cpp:582-595`, `LPDataCompat.cpp:149-164`.
- `FHitResult::GetComponent() const` rend `UPrimitiveComponent*` : `Engine/Classes/Engine/HitResult.h:227` ; `OutSurface = bHit ? Hit.GetComponent() : nullptr;` (l. 1230) : `UPrimitiveComponent*` vers `const UPrimitiveComponent*&`, aucun problème. Le lambda reçoit exactement `const UPrimitiveComponent*` (`GrabSurface` l. 1298, `Surface` l. 1364) : la référence se lie.
- `APlayerController::GetMousePosition(float&, float&) const` : `PlayerController.h:1623` (surcharge `double&` à côté, pas d'ambiguïté avec des `float`). `FVector2D::Distance`, `FVector2D(float, float)` : même emploi qu'à `.cpp` l. 1289 et 1344. `bMiddleMoved |= (bool && bool && bool)` : même forme que `bGrabMoved |=` l. 1344.
- Includes déjà présents (l. 5-28) : `UObject/UnrealType.h` (donc `UFunction`), `GameFramework/PlayerInput.h` (`EKeys::MiddleMouseButton`, comme `LeftMouseButton` déjà utilisé), `Components/StaticMeshComponent.h`, `Engine/StaticMesh.h`, `GameFramework/Character.h`, `Components/SkeletalMeshComponent.h`. Aucun include à ajouter. `TNumericLimits<double>::Max()` : `CoreMinimal` (même usage `LPDebugSubsystem.cpp:508`).
- Ternaires : `Mesh ? *Mesh->GetName() : TEXT("sans maillage statique")` (l. 499) = `const TCHAR*` / tableau, se réduisent en `const TCHAR*` ; l. 1305-1306 : `bGrabHasHeight ? *FString::Printf(…) : TEXT("? cm")` puis `!bGrabHasHeight ? TEXT("inconnue") : GrabSurface ? *DescribeComponent(GrabSurface) : TEXT("sol")` : imbriqué à droite, chaque branche finale `const TCHAR*` ; les `FString` temporaires vivent jusqu'à la fin de l'expression complète (l'appel `UE_LOG`), comme le motif déjà compilé qu'ils remplacent. `Comp ? Comp->GetOwner() : nullptr` : `AActor*` vers `const UObjectBaseUtility*` de `GetNameSafe` (précédent l. 1259).
- `const UStaticMesh* ComponentMesh(…)` : `MeshComp ? MeshComp->GetStaticMesh() : nullptr` : `UStaticMesh*` converti en `const UStaticMesh*`. `Mesh->GetName()` sur pointeur const : `UObjectBaseUtility::GetName() const`.
- `double`/`float` : `Target.Z = Pivot.Z + (SurfaceZ + CoupleRootAboveSurface − CoupleLowestRoot(Couple))` : `double` + (`double` + `float` − `double`) ; `FMath::Min(Lowest, cond ? double : double)` : deux `double` ; `Distance(…) > PanelClickMaxMove` : `double` contre `float` ; `MiddleHeld <= PanelClickMaxHold` : `float` contre `float`.
- `.h` : `void TickPanelClick(APlayerController* PC, float DeltaTime);` (`APlayerController` déclaré en avant l. 18) ; `FVector2D MiddleStart = FVector2D::ZeroVector;` même forme que `GrabMouseStart` (l. 97). Zone `private:` (l. 33).
- Non-ASCII : uniquement des commentaires (l. 494 neuve avec « », comme les l. 74, 146, 536, 1328 déjà compilées) ; tous les `TEXT(...)` neufs sont en ASCII.
### Contrôle 13 : formats
- Prise (l. 1303-1306) : `%s`, `%.0f` ×3, `%s`, `%s` = 6 ; arguments : ternaire `deplacement`/`rotation`, `Pivot.X`, `.Y`, `.Z` (double), ternaire `Printf`/`? cm`, ternaire surface = 6. `Printf` interne `%+.0f` avec `GrabHeight` (double) = 1/1.
- `DescribeComponent` (l. 498-499) : `%s/%s (%s)` = 3 ; arguments : 2 `*GetNameSafe(…)` + ternaire = 3.
- Refus de surface (l. 1384) : `%s` = 1, `*Refused`. Clic molette (l. 1146) : `%.2f` = 1, `MiddleHeld` (float promu en double). Absence de l'événement (l. 1151) : aucun argument, aucun `%`.
### Point par point : lecture du code
**H1 (N7), diff H1 vers H2, +3 / −2.** Le diff complet est de 3 lignes ajoutées (1 commentaire `// ObstacleReach, pas Reach : …` et les 2 lignes remplacées) et 2 retirées : `const double Reach = …` devient `const double ObstacleReach = …` (même expression, mêmes arguments) et `Desired = Target + Axis * Reach;` devient `… * ObstacleReach;` (`.cpp` l. 970-973). Aucun autre hunk. Verdict **PASS-STATIC** (contrôles 5 à 7 : STG:58 est bien dans l'espace anonyme l. 53-54 ; plus aucun `Reach` de code dans CameraMode ; `Axis` n'a pas changé).
**H2 (maillage dans les refus), diff H2 vers H3, +26 / −6.**
- `ComponentMesh` (l. 478-483), `IsAdmittedBed` (l. 485-492), `DescribeComponent` (l. 494-500) ajoutées avant `CoupleSpotFree`, dans `LPCameraModeLocal`.
- **Règle des lits identique à `IsBedMesh`** (`LPShowCoupleSubsystem.cpp:56-59`) : `Contains("bed", IgnoreCase)`, `Contains("fnt_ext011")`, `Equals("fnt_new045")`, `Equals("fnt_new235")` : mêmes quatre tests, mêmes littéraux, même ordre (`.cpp` l. 490-491). Mesh nul : `IsBedMesh` rend faux (`:40-43`), `IsAdmittedBed` aussi (`Mesh && (...)`, `MeshName` vide). Seule différence : STE met le résultat en cache par `FName` (`:44-60`), sans effet sur le résultat. Équivalence : établie par lecture caractère par caractère.
- **`CoupleSpotFree` même comportement** : le bloc d'origine (calcul de `MeshComp`, `Mesh`, `MeshName` puis les quatre tests) est remplacé par `if (IsAdmittedBed(Comp)) continue;` (l. 526-529) ; la condition, le `continue` et le `return false` sont inchangés ; seule la ligne `OutBlocker = Comp->GetName();` devient `OutBlocker = DescribeComponent(Comp);` (l. 530) : le texte du bloqueur passe de `Room_C_0/NODE_…` à `Propriétaire/composant (maillage)`. L'appelant compare `Blocker != LastSpotBlocker` (l. 1396) : même logique, la clé est plus fine (le maillage s'ajoute au nom). Verdict **PASS-STATIC**.
**H3 (G-e), diff H3 vers H4 (cpp +41 / −13, h +2 / −1).**
- Canal : `LineTraceSingleByChannel(…, ECC_Pawn, Query)` (l. 1223) ; `Query` est la même requête que le tracé du curseur (couple, vue, `Wall`, `CamRoom` ignorés, l. 1186-1196) ; `ECC_Pawn` déjà employé l. 517.
- `OutSurface` : `bHit ? Hit.GetComponent() : nullptr` (l. 1230), puis nul si E2 relève le point au sol (l. 1231-1235), puis nul si le composant est `FloorComponent` (l. 1236-1239) : nul pour le sol dans les deux cas (impact sous le dessus du sol, ou `Room.Box`).
- **E2 conservé** : l'ancien `OutZ = Max(OutZ, FloorBox.Max.Z)` (si au-dessus du sol connu) équivaut à `if (bOverFloor && OutZ <= FloorBox.Max.Z) OutZ = FloorBox.Max.Z` : identique pour toute valeur de `OutZ`. Les retours `false` (aucun impact et hors du sol) sont inchangés (l. 1225-1228).
- **Hauteur** : `Target.Z = Pivot.Z + (SurfaceZ + 3 − CoupleLowestRoot(Couple))` (l. 1368). `Pivot` est la moyenne des positions des acteurs, recalculée à chaque image (l. 1056-1064) et le déplacement est une translation rigide `Delta = Target − Pivot` appliquée à chaque personne (l. 1390-1394) : la racine la plus basse du couple arrive donc exactement à `SurfaceZ + 3 cm` (`CoupleRootAboveSurface`, l. 133-134). `CoupleLowestRoot` (l. 465-476) : Z du composant de maillage du `ACharacter`, acteur à défaut ; `Couple` non vide dans le glisser (`bScene` exige `Couple.Num() > 0`, l. 1177) ; même mesure que `RecenterSceneCamera` / E3 (`HighestRoot` l. 840 en est le pendant). Le garde `bGrabHasHeight &&` du glisser est retiré (l. 1365-1369) : sans impact ni sol, `Target.Z` reste `Pivot.Z` comme avant.
- **Ligne de prise** (l. 1303-1306) : `Sexe : couple pris (deplacement|rotation) a (x, y, z), racine a +N cm de la surface <sol | Propriétaire/composant (maillage) | inconnue>` ; `GrabHeight = CoupleLowestRoot − SurfaceZ` (l. 1300) ; `.h` l. 91-92 : commentaire mis à jour (journal seulement).
- **Écart avec la SPEC jugé** (§ 5.2 : « surface + écart racine-acteur + 3 cm » du meneur ; IMPL : racine la plus basse du couple). Lecture : pour la translation rigide d'un couple, les deux formulations donnent la même hauteur dès que la racine du meneur est la plus basse (cas du sol et de la plupart des poses). Si l'autre personne a la racine plus basse, la formulation de la SPEC la laisserait de ce côté **sous** la surface (couple enfoncé dans le matelas) alors que celle de l'IMPL la pose à surface + 3 cm ; si au contraire le meneur est plus bas, les deux coïncident. L'IMPL est donc au moins aussi sûre que la SPEC, cohérente avec E3 et le recadrage (mêmes racines), et l'écart est annoncé (IMPL § 1 H3). **Acceptable** (estimé, par lecture ; l'essai CAM-81 le confirmera sur le lit). Il est à porter à la connaissance de Big Boss, non à corriger.
- Verdict **PASS-STATIC**.
**H4 (G-f), diff H4 vers H5 (cpp +15 / −2, h +2 / −0).**
- Condition (l. 1376) : `bFloorKnown && bSurface && Surface && !IsAdmittedBed(Surface)`. `bFloorKnown` = sol valide **et** Room non cachée (l. 1216) : « pièce connue » ; `Surface` est nul pour le sol (H3) : le sol passe ; un lit admis passe ; tout le reste (commode, bureau, armoire, capsule de PNJ) est refusé.
- **Le couple ne bouge pas** : la branche G-f (l. 1377-1386) ne contient que le journal ; le déplacement (l. 1390-1394) est dans la branche `else if (CoupleSpotFree(…))` qui n'est pas atteinte ; `Blocker` reste vide et `else if (Blocker != LastSpotBlocker)` (l. 1396) n'est pas atteint non plus (chaîne `if / else if / else if`, une seule branche s'exécute).
- **Une ligne par changement** : `if (Refused != LastSurfaceRefused)` puis affectation et `UE_LOG` (l. 1381-1385) ; `.h` l. 121-122 (membre `FString`). **Remis à vide à chaque prise** : `LastSurfaceRefused.Reset();` (l. 1288), juste après `LastSpotBlocker.Reset();` (l. 1287), dans le bloc de sélection (`CoupleGrabMode == 0`).
- **Pièce cachée sans G-f** : `bFloorKnown` faux (Room cachée ou sol invalide) : la condition est fausse, on passe à `CoupleSpotFree` avec `Floor` nul, comme avant ; G-e s'applique si le tracé touche (`bSurface`).
- Sol sans impact au-dessus : `Surface` nul, G1 comme avant.
- Verdict **PASS-STATIC** (réserves N2 et N3).
**H5 (G-g), diff H5 vers final (cpp +52 / −2, h +8 / −0).**
- **Appel seulement pendant le sexe hors menu** : `TickPanelClick(PC, DeltaTime)` n'a qu'un appel, `.cpp` l. 1043, placé **après** le retour de la l. 1027-1037 (`if (!bSexOn || bMenuOnScreen) { …; return; }`) et conditionné par `PC` non nul. Chemins vérifiés : (1) hors sexe : `bSexOn` faux, retour avant l'appel (l. 1027) ; (2) menu à l'écran : idem ; (3) `GI` nul : `bSexOn` faux ; (4) `PC` nul : pas d'appel, retour l. 1045.
- **Hors sexe, rien ne change** (tous les chemins) :
- appel : voir ci-dessus, jamais hors sexe/menu ;
- **H1 étendu** : la détection du clic molette (l. 1001-1003) est dans le `else` de `if (!bSexOn)` (l. 987-993 / 993-1026) : évaluée pendant le sexe seulement ; hors sexe, le bloc H1/H2 n'est pas exécuté ; en sexe, `bClick` ne sert qu'au retour du focus (l. 1007-1011, 1019, 1020-1025) ;
- remises à faux de `bMiddleWasDown` : l. 991 (hors sexe) et l. 1031 (hors sexe ou menu) : écritures d'un membre qui n'a aucun autre lecteur que `TickPanelClick` ; aucune valeur de PC/GI/Room, aucun appel, aucune ligne de journal ;
- `TickPanelClick` ne lit ni n'écrit rien du PC hors la bascule du panneau (`ProcessEvent` conditionné par le clic) ;
- la fonction ne s'appelle pas depuis un autre ticker (`Grep TickPanelClick` : `.cpp` 1043, 1122 ; `.h` 139).
- **Seuil et mouvement** : appui = `bMiddle && !bMiddleWasDown` : `MiddleHeld = 0`, position de départ, `bMiddleHadMouse`, `bMiddleMoved = false` (l. 1128-1134) ; tenu ou relâché = `else if (bMiddleWasDown)` : `MiddleHeld += DeltaTime` (durée réelle, `DeltaTime` non borné ; le `Step` borné de `TickSexCamera` n'est pas utilisé) et `bMiddleMoved |= bMiddleHadMouse && bPanelMouse && Distance(...) > PanelClickMaxMove` (l. 1138-1140) ; bascule à `!bMiddle && MiddleHeld <= 0,2 && !bMiddleMoved` (l. 1141) : 0,2 s **inclus**, 5 px **exclus** (`>`), conformes à la SPEC § 5.4 (« moins de 0,2 s », « 5 px près »). `bMiddleWasDown = bMiddle;` en fin (l. 1155).
- **Appel de l'événement** : `PC->FindFunction(PCLeftMouseSexOn)` (l. 1143), `Toggle && Toggle->NumParms == 0` (l. 1144), `PC->ProcessEvent(Toggle, nullptr)` (l. 1147) précédé de la ligne `Sexe : panneau bascule (clic molette, %.2f s)` ; sinon la ligne `panneau non bascule, evenement LeftMouseSexOn absent du PC` (l. 1151). Aucun paramètre, aucun pointeur nul déréférencé (`Toggle` testé d'abord).
- **Un appui commencé dans un menu ne bascule rien** : pendant un menu, `bMiddleWasDown` est remis à faux à chaque image (l. 1031) et `TickPanelClick` n'est pas appelé ; un appui né pendant le menu n'a donc aucun `MiddleHeld` valide. Réserve N1 : si le bouton est encore enfoncé à la fermeture du menu, l'appui est compté comme neuf à cet instant.
- **H1 étendu, justification** : `MenuOnScreen` rend vrai quand un champ de saisie garde le focus (`.cpp` l. 340-341) ; sans retour du focus, `bMenuOnScreen` reste vrai et `TickSexCamera` sortirait avant `TickPanelClick`. Le retour du focus recalcule `bMenuOnScreen` dans la même image (l. 1024) avant le test de la l. 1027. Établi par lecture. Que `IsPressed(MiddleMouseButton)` soit vu par `PlayerInput` quand Slate a le focus : incertain (même hypothèse que pour les clics gauche et droit de H1), essai CAM-83.
- Verdict **PASS-STATIC** (réserve N1).
## 4. Diff lu en entier
Chaque diff a été lu en entier (`git diff --no-index -U2`, 5 diffs `.cpp` et 3 diffs `.h` non vides) ; hunks du diff global (lignes de l'état final) :
| Fichier | Hunks | Contenu | Point |
|---|---|---|---|
| `.cpp` | 133-134 | `CoupleRootAboveSurface` | H3 |
| `.cpp` | 176-178 | `PCLeftMouseSexOn`, `PanelClickMaxHold`, `PanelClickMaxMove` | H5 |
| `.cpp` | 465-500 | `CoupleLowestRoot` (H3), `ComponentMesh`, `IsAdmittedBed`, `DescribeComponent` (H2) | H2, H3 |
| `.cpp` | 526, 530 | `CoupleSpotFree` : `IsAdmittedBed`, `DescribeComponent` | H2 |
| `.cpp` | 969-973 | `ObstacleReach` | H1 |
| `.cpp` | 991, 998-1003, 1031, 1041-1044 | remises à faux, clic molette dans H1, appel | H5 |
| `.cpp` | 1122-1155 | `TickPanelClick` | H5 |
| `.cpp` | 1210-1241 | `TraceDown` | H3 |
| `.cpp` | 1288 | `LastSurfaceRefused.Reset()` | H4 |
| `.cpp` | 1296-1306 | prise : `GrabSurface`, `GrabHeight`, ligne de prise | H3 |
| `.cpp` | 1361-1395 | glisser : hauteur (H3), `bSurface` et branche G-f (H4) | H3, H4 |
| `.h` | 91-92 | commentaire de `GrabHeight` | H3 |
| `.h` | 121-122 | `LastSurfaceRefused` | H4 |
| `.h` | 137-144 | `TickPanelClick` et 5 membres | H5 |
- **Aucun ajout non demandé** : tout hunk se rattache à un point de la fiche (H1 à H5) ; ni STG ni STE touchés (contrôle 15).
- **Non-régression B2p-C à B2p-G** : G1 (`CoupleSpotFree`, `LastSpotBlocker`, `Blocker`) garde la condition et le déplacement ; E2 conservé (H3) ; E1/E3 (`TickCoupleBounds`) hors diff ; G-a à G-d (`TickSceneCamera`, `RecenterSceneCamera` hors le seul renommage), `ComputeLabel` et G3 (`NotifyCoupleTeleported`, l. 1321-1335) hors diff ; H1/H2 de B2p (focus clavier) élargis au clic molette (annoncé) ; B2p-D : le glisser perd sa dépendance à `bGrabHasHeight` (voulu, SPEC § 5.2) et `GrabHeight` ne sert plus qu'au journal (vérifié : seule lecture l. 1305). Aucune régression trouvée.
## 5. Verdict
| Point | Verdict |
|---|---|
| H1 (N7) | **PASS-STATIC** |
| H2 | **PASS-STATIC** |
| H3 (G-e) | **PASS-STATIC**, écart SPEC jugé acceptable |
| H4 (G-f) | **PASS-STATIC** |
| H5 (G-g) | **PASS-STATIC** |
| Contrôles transverses (types, includes, signatures, C4456/C4458/C4459 par lecture) | **PASS-STATIC** |
| **Global** | **PASS-STATIC** : aucun défaut bloquant. **Non couvert** : la compilation (BLOCKED-compilation, à confier à BUILD COMMUN-12) et tout comportement en jeu (reporté V7). |
## 6. Points à confirmer par l'utilisateur (cases)
- [ ] BUILD COMMUN-12 : `LifePlay` compile avec 0 erreur et 0 avertissement nouveau sous V7 ; `LPCameraModeSubsystem.cpp` doit figurer dans les unités compilées (C4456 / C4458 / C4459 et résidu N6).
- [ ] Accepter l'écart de H3 à la SPEC (racine la plus basse du couple plutôt que racine du meneur).
- [ ] CAM-81 : glisser sur le lit de face et d'en haut ; vérifier la ligne `couple pris … racine a +N cm de la surface …` et que la tête de lit ne porte pas le couple (R1) ; **vérifier qu'aucune ligne `surface refusee` ne désigne le plafond, un tapis ou un décor mince** quand on vise le sol (N3).
- [ ] CAM-82 : commode refusée, une seule ligne `surface refusee …` ; décider si une seconde visite de la même surface refusée dans la même prise doit réécrire la ligne (N2).
- [ ] CAM-83 : clics gauche et droit sans bascule (partie PC, MCP sous UE-GO) ; Espace et clic molette basculent ; ligne `panneau bascule (clic molette, …)` ; `ProcessEvent` hors tick du monde sans anomalie (R4) ; clic molette vu par `PlayerInput` quand le filtre a le focus.
- [ ] CAM-84 : hors sexe, rien ne change (Espace en pause, clics et molette comme avant).
- [ ] CAM-85 : ligne `place refusee … (maillage)` présente.
Tours : 25 / 40
## Constats
### Défauts bloquants
Aucun (preuves : contrôles 1 à 15 et lecture point par point).
### Défauts non bloquants
| # | Lieu | Constat | Niveau de preuve | État |
|---|---|---|---|---|
| N1 | `.cpp` l. 1031 et 1128, 1041-1045 | `bMiddleWasDown` est remis à faux à chaque image de menu : si le bouton est encore enfoncé à la fermeture du menu, l'appui est compté comme neuf et un relâcher en 0,2 s au plus bascule le panneau. De même, si `PC` est nul quelques images (l. 1041-1045), `bMiddleWasDown` garde sa dernière valeur. Un clic qui commence et finit dans la même image n'est pas vu (`IsPressed` est un état). Tous cas très rares. | estimé (lecture) | ouvert, négligeable |
| N2 | `.cpp` l. 1381-1385 | `LastSurfaceRefused` n'est pas remis à vide quand le curseur repasse sur une place admise : revenir sur la même surface refusée dans la même prise n'écrit pas de seconde ligne (même traitement que `LastSpotBlocker`, l. 1396-1399). Lu comme « une ligne par changement de surface refusée ». | établi (lecture) | ouvert, à confirmer (case CAM-82) |
| N3 | `.cpp` l. 1218-1241, 1376 | Avec le canal Pawn, toute surface touchée au-dessus du sol qui n'est pas un lit est refusée en pièce connue : un tapis ou décor mince plus haut que le dessus de `Room.Box`, un plafond ou volume bloquant Pawn sous `Pivot.Z + 150` (départ du tracé), ou la tête de lit (R1) peuvent refuser des places que le joueur juge valides. Le journal nomme le composant (`surface refusee …`), donc l'essai tranche d'emblée. | incertain (essai : CAM-81 et CAM-82) | ouvert |
| N4 | `.h` l. 91-93 | Le commentaire de `GrabHeight` est posé au-dessus de `bGrabHasHeight` (antérieur à H, B2p-D). Sans effet sur la compilation. | établi | ouvert, cosmétique |
| N5 | IMPL | Écart de gabarit : voir § 1 (tête commune, « Écarts à la SPEC », « Ce qui n'a pas été fait », « Points à confirmer en VAL », `Tours :` absents). Le contenu est exact : MD5, lignes, périmètre et noms recomptés. Le diff global (136 / 24, 12 / 1) n'est pas dans l'IMPL ; il est ici. | établi | non bloquant, à reporter à l'auteur |
| N6 | compilation | Le résidu C4459 vient des en-têtes moteur : un nom `Toggle`, `Surface`, `Lowest` ou `Refused` déclaré en portée globale par un en-tête inclus par CameraMode n'est excluable que par compilation. `Grep` sur `Engine/Classes/*.h` : rien. `Toggle` a un précédent de local compilé sous V7 (`LPModInfoSubsystem.cpp:113, 288`). | estimé (faible probabilité) | à trancher par BUILD |
### Non mesuré
Compilation et avertissements ; tout essai en jeu (CAM-81 à CAM-85, reportés V7) ; géométrie réelle des assets (tête de lit, tapis, plafond sur le canal Pawn) ; vue de `IsPressed(MiddleMouseButton)` par `PlayerInput` quand Slate a le focus ; contexte d'appel de `ProcessEvent` (R4) ; absence d'écriture dans STG / STE prouvée par les dates seulement (les instantanés ne contiennent pas ces fichiers) ; `LPTestStringCompat` et porte G-U3 (sans objet : aucun changement de chaîne, de configuration ni de compilation dans ce lot).
### Fichiers à commiter (VAL)
A `Docs/Migration/etat/CAM/CAM-B2p-H-VAL.md` (dépôt du jeu, branche `ue5.8`). Fichiers modifiés par la VAL : ce livrable seulement ; fichiers de travail dans le dossier temporaire de session (`scratchpad`), hors dépôt.
### À verser
- `lp-validate` § 1 : le crochet `ecriture-gardien` refuse le signe « supérieur à » même dans un `echo` entre guillemets (lu comme redirection, flèche `->` comprise) ; écrire « vers » dans les étiquettes. Un script `.ps1` du dossier temporaire, lancé par `pwsh -NoProfile -File`, évite les pièges de `$` dans Git Bash (`foreach($x in …)` y est mangé).
- `lp-validate` § 1 : pour des instantanés « d'avant » successifs, le point Hn est le diff `Hn` vers `H(n+1)` ; la somme brute des diffs par point peut dépasser le global d'une ligne ajoutée puis remplacée par un point suivant : le noter et le recompter (`+137 / −25` contre `+136 / −24` ici, écart expliqué), l'égalité tient sur le net (lignes LF) et sur le `.h`.
- `lp-ue-compat` / `lp-build` : `UObject::FindFunction(FName) const` (`Object.h:1258`), `ProcessEvent(UFunction*, void*)` (`Object.h:1490`), `UFunction::NumParms` (`Class.h:2574`), `FHitResult::GetComponent() const` (`HitResult.h:227`) vérifiés en 5.8.