LifePlay-Revival/Docs/Migration/etat/CAM/CAM-B2p-I-VAL.md
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

43 KiB
Raw Blame History

Fiche CAM-B2p-I-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-I-VAL : validation statique de N2 (reprise de B2p-H) et de l'aide des contrôles derrière l'icône « i » (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 -- Source : ??) : pas de tag CAM-B2p-I-avant. Chaque point est jugé sur le diff entre instantanés (git diff --no-index), comme CAM-B2p-H-VAL.md. Convention : CAM-B2p-I0 = état d'avant le point I0 ; I0 = diff I0 vers I1 ; I1 = diff I1 vers final. Niveaux de preuve : établi = lecture ou commande rejouée aujourd'hui ; estimé = déduit (raisonnement dit) ; incertain = non vérifié (essai nommé). Commandes : Read, Grep, Glob ; deux scripts .ps1 du dossier temporaire de session (scratchpad\v1.ps1, scratchpad\v2.ps1), lancés par pwsh -NoProfile -File, hors dépôt. v1.ps1 a échoué (chemins des instantanés sans le sous-dossier Source\LifePlay : « ABSENT », diffs vides, rien d'utilisé) ; v2.ps1 corrige les chemins et fait foi.

1. Fiches amont relues

  • Docs/Migration/etat/CAM/CAM-B2p-I-IMPL.md (lue en entier ; seule IMPL du lot).
  • Docs/Migration/etat/CAM/CAM-B2p-I-SPEC.md (lue en entier : DIAG § 1, SPEC § 2, risques § 3, essais § 4).
  • Docs/Migration/etat/CAM/CAM-B2p-H-VAL.md (lue en entier : forme d'une VAL statique C++ de ce lot ; son N2 à la l. 188).
  • Lus en lecture seule : CAM-B2p-G-PC-VAL_LPDumpFunction_20261014-135527.txt (l. 345-352, 1006-1014, 1163-1172, 3005-3043), CAM-B2p-E-IMPL.md l. 44-49, CAM-B2p-G-SPEC.md l. 11-14, Config/DefaultInput.ini l. 97, 118, 150-160, LPBonusRewardSubsystem.cpp l. 590-713, LPKeyboardSubsystem.cpp l. 155-180 et 599, LifePlay.Build.cs l. 12-52, Intermediate/Build/Win64/x64/*/*/LifePlay/Module.LifePlay.cpp (trois cibles).
  • Gabarit VAL lu : D:\Game\LifePlay-Sandbox\Tools\claude\skills\lp-doc\gabarits.md (section VAL, sections 1 à 6 dans l'ordre).
  • Gabarit de l'IMPL (écart, N5) : l'IMPL n'a ni tête commune (« Fiche … — Niveau … »), ni section « Ce qui n'a pas été fait », ni ligne Tours :. Ses sections sont numérotées 0 à 5 et non 1 à 6 ; les « points à confirmer » sont en § 4 (essais CAM-86 à CAM-90), les écarts en § 3, les fichiers à commiter en § 5. Le contenu est exact sauf un chiffre (voir § 3, contrôle 3).

2. Points à confirmer des IMPL (un par ligne)

# Point Verdict Preuve
I0 N2 : LastSurfaceRefused.Reset() dans la seule branche où le couple bouge, rien d'autre PASS-STATIC .cpp l. 1447-1448 (dans else if (CoupleSpotFree(…)), l. 1439-1449, après la boucle de déplacement l. 1443-1446) ; diff I0 vers I1 = +2 / −0 (§ 4)
C1 Consigne 1 : TextBlock_1 invisible seulement si son texte commence par « Click any mouse button », sinon intact PASS-STATIC .cpp l. 1778-1792 (§ 3.1)
C2 Consigne 2 : SearchAction ou parent introuvable : rien d'inséré, une ligne de témoin PASS-STATIC pour le cas décrit ; réserve D1 (AddChild rendant nul) .cpp l. 1851-1858, 1889-1908
C3 Consigne 3 : texture info absente, un « i » en texte PASS-STATIC .cpp l. 1868-1884, 1906-1908
C4 Consigne 4 : ligne de la molette retirée si CAM ne déplace pas le couple à la molette PASS-STATIC (règle respectée à la lettre) ; observation D2 .cpp l. 126, 1104, 1745-1746 ; LPKeyboardSubsystem.cpp:165 ; § 3.1
C5 Consigne 5 : hors du sexe, rien ne change PASS-STATIC ; observation D3 .cpp l. 1769, 1775, 1806-1811, 1820-1837
C6 Consigne 6 : aide en anglais, touches lues dans les mappages PASS-STATIC .cpp l. 295-317, 1743-1764 ; DefaultInput.ini l. 97, 118, 150-160
E1 (IMPL § 3) Icône en fin de rangée (AddChild), non juste après le champ PASS-STATIC, écart jugé acceptable PanelWidget.h:68-72, PanelWidget.cpp:184-198 (§ 3.2)
E2 (IMPL § 3) I-d par opacité de rendu et non par vidage du texte PASS-STATIC, écart jugé acceptable TextBlock.cpp:469-478 : SetText fait TextDelegate.Unbind()
E3 (IMPL § 3) Pas de lecture MCP : confirmation au premier essai reporté V7 aucune lecture possible ici
E4 (non annoncé) La SPEC § 2.1 I-c demandait un UBorder et un UTextBlock UMG avec contour ; l'IMPL fait une boîte Slate (SOverlay, SBorder, STextBlock) avec ombre PASS-STATIC, écart non annoncé (N6) .cpp l. 1919-1955 ; même technique que EnsureWidget (l. 1682-1714)
CAM-86 Ligne icone inseree (parent, classe), image info ; ligne 4 lignes d'origine masquees formats de lignes PASS-STATIC ; comportement reporté V7 .cpp l. 1784, 1906-1908
CAM-87 Survol = info-bulle ; clic ouvre et ferme la boîte ; la fermeture du panneau la ferme chemin de code PASS-STATIC ; comportement reporté V7 .cpp l. 1814, 1886, 1912-1917, 1820-1837
CAM-88 Touches du clavier (Z/S, Q/D) ; suivent un remappage PASS-STATIC (lecture à chaque 0,1 s) ; affichage reporté V7 .cpp l. 1803, 1811-1814, 1828-1830
CAM-89 Hors sexe : icône absente du Cursor, texte de jouissance affiché icône et boîte PASS-STATIC ; texte de jouissance : observation D3, reporté V7 .cpp l. 1806, 1822-1825
CAM-90 Deux passages sur la même commode écrivent deux lignes (N2) PASS-STATIC (code) ; reporté V7 .cpp l. 1433-1437, 1448
Compilation 0 erreur, 0 avertissement nouveau sous V7 BLOCKED-compilation (à BUILD) aucune compilation lancée ici

3. Critères rejoués (commande → résultat)

# Contrôle Commande / lecture Résultat brut Verdict
1 MD5, taille, fins de ligne des 6 fichiers pwsh -NoProfile -File scratchpad\v2.ps1 (octets lus par ReadAllBytes, CR et LF comptés, sans grep -c) final .cpp size=85842 CR=0 LF=1969 MD5=73CC3BD667059A903E44405C5E793DE3, .h size=9813 CR=0 LF=179 MD5=6EDBBCCF61759B8C6EE4890DF9BC5FE7 ; I0 .cpp 73384 / 1687 / 210B665AA8D5D628B8C1AC429A339B74, .h 8471 / 152 / 239DA6094E7401E0E3B3699A635A8150 ; I1 .cpp 73526 / 1689 / 3A11A3872378485DC64804AEC0A761EE, .h 8471 / 152 / 239DA609… PASS : égalent la fiche et l'IMPL § 0 ; LF pur (CR=0), sans BOM (premiers octets 2f2f20), dernier octet 0a
2 Manifestes des instantanés Get-Content …\CAM-B2p-I0\manifest.csv et I1 I0 : tailles 73384 / 8471 ; I1 : 73526 / 8471, MD5 identiques aux fichiers ; dossiers Source\LifePlay\ + manifest.csv, dates 14:40:05 et 14:40:13 PASS
3 Chiffres de l'IMPL recomptés (sk-recount) Grep des 11 constantes, 9 includes, 6 fonctions, membres du .h l. 148-171 9 includes, 11 constantes, 3 déclarations avancées, 6 fonctions : conformes. Membres : HelpCursor, HelpButton, FadedHelpLines, FadedLinesOpacity, bHelpBoxOpen, HelpTextShown, HelpRoot, HelpText, HelpViewport = 9 ; l'IMPL § 2.2 dit « 10 membres » un écart (N7), sans effet sur le code
4 Diff brut et répartition git diff --no-index -U0 puis --numstat (sorties dans § 4) I0 vers I1 .cpp +2/−0 ; I1 vers final .cpp +280/−0, .h +27/−0 ; I0 vers final .cpp +282/−0, .h +27/−0 PASS : la somme égale le global ; lignes 1687 + 282 = 1969 et 152 + 27 = 179
5 Aucune ligne retirée ou modifiée dans B2p-C à B2p-H lecture des 9 hunks (§ 4) : tous des ajouts purs, aucun retrait (numstat 0 retraits) hunks aux l. 6-14, 189-202, 293-318, 629, 635-636, 1447-1448, 1742-1969 (.cpp) ; 23-25 et 148-171 (.h) PASS-STATIC : aucune ligne de C à H n'est modifiée
6 Tick garde son comportement Read l. 633-685 TickControlsHelp(); en l. 636, avant ComputeLabel() (l. 637) ; la fonction rend void ; Tick n'a aucun retour avant l. 636 ; les retours l. 662 et 666 sont les anciens PASS-STATIC
7 Symboles uniques Grep de 24 noms neufs sur Source 90 occurrences, seulement dans LPCameraModeSubsystem.cpp (75) et .h (15) : aucun homonyme externe PASS
8 Dépendances du module LifePlay.Build.cs l. 17 et 38 InputCore (public) ; Slate, SlateCore, UMG (privés, l. 38) ; Engine public PASS
9 Includes suffisants .cpp l. 5-37 contre les types employés WidgetTree.h, Button.h, CanvasPanelSlot.h, HorizontalBoxSlot.h, Image.h, PanelWidget.h, TextBlock.h, Texture2D.h, SBorder.h ajoutés ; déjà présents : UserWidget.h, InputSettings.h (donc PlayerInput.h : FInputActionKeyMapping), CoreStyle.h, SOverlay.h, STextBlock.h, GameViewportClient.h, SlateApplication.h. Précédents du module pour les mêmes en-têtes : LPKeyboardSubsystem.cpp:5, LPSettingsSubsystem.cpp:6,9,12,13 PASS-STATIC
10 .generated.h et UFUNCTION privée .h l. 15, 27-30, 36, 156-157 #include "LPCameraModeSubsystem.generated.h" (l. 15, dernier include), UCLASS() + GENERATED_BODY() (l. 27-30), private: (l. 36), UFUNCTION() + void ToggleControlsHelp(); (l. 156-157). AddDynamic et AddUniqueDynamic sont appelés depuis une fonction membre de la classe (l. 1848, 1886) : l'accès au membre privé est licite. OnClicked est un DECLARE_DYNAMIC_MULTICAST_DELEGATE (Button.h:18, 76) : les deux macros existent. Précédent du module : LPKeyboardSubsystem.cpp:599 (AddDynamic, fonction publique). UHT génère le thunk et friend struct …_Statics pour une fonction privée (usage courant du moteur, estimé, sans exemple relu) PASS-STATIC (UHT et compilation : BUILD)
11 Unités compilées Grep de Module.LifePlay.cpp (UnrealGame Development, Shipping, UnrealEditor Development) LPCameraModeSubsystem.cpp est absent des trois unités unifiées ; seul UHT/LPCameraModeSubsystem.gen.cpp y figure (UnrealGame Development, l. 6) : le .cpp se compile seul quand il est modifié PASS : aucune collision d'unité avec un autre .cpp ; BUILD doit vérifier que le module figure dans les unités compilées
12 Texte d'aide : Printf lecture l. 1748-1763 ; moteur Platform.h:1322, 1332, 1348 ; précédent moteur ShaderCompiler.cpp:5978-5979 8 %s (l. 1750 : 1 ; l. 1751 : 4 ; l. 1756 : 2 ; l. 1758 : 1) et 8 arguments (l. 1759 : 1 ; 1760 : 2 ; 1761 : 2 ; 1762 : 2 ; 1763 : 1), dans l'ordre : SwitchCamera, MoveForward +1 et −1, MoveRight −1 et +1, LeftRight, DownUp, Pause. TEXT(x) vaut WIDETEXT(x) = L ## x : L"…" "…" est une concaténation licite (même idiome que ShaderCompiler.cpp:5978-5979) ; texte entièrement en ASCII PASS-STATIC
13 Formats UE_LOG lecture l. 1784, 1791, 1855-1857, 1862-1863, 1906-1908, 1915 1/1, 0/0, 2/2 (%s conditionnel de deux tableaux : décroissent en const TCHAR*), 2/2, 4/4 (%s, %s, %d entier de GetChildrenCount, %s), 1/1 PASS-STATIC
14 Non-ASCII hors commentaires Grep [^\x00-\x7F] sur le .cpp 10 lignes (83, 155, 189, 543, 585, 1157, 1380, 1663, 1745, 1878), toutes des commentaires (189, 1745 et 1878 sont neuves ; les 7 autres existaient dans le fichier déjà compilé) ; aucun TEXT(…) non ASCII PASS
15 Fichiers STG / STE Get-ChildItem … Sort LastWriteTime LPCameraModeSubsystem.cpp 14:43:20, .h 14:43:12 ; puis LPShowCoupleSubsystem.* 12:43:20 ; LPOutdoorRoomSubsystem.* et LPDirectControlSubsystem.* 12:05:58 ; aucun autre fichier de Source touché après 12:43 PASS (par les dates)
16 État git de la copie git status --short -- Source Docs/Migration/etat/CAM Config M : LPDebugSubsystem, LPDirectControlSubsystem, LPOutdoorRoomSubsystem, LPShowCoupleSubsystem, LifePlay.cpp (autres lots) ; ?? : LPCameraModeSubsystem.*, LPBonusRewardSubsystem.*, LPSaveRelaunch.* PASS : aucun fichier inattendu pour ce lot

3.1 Les six consignes de Big Boss, une à une

  1. TextBlock_1 invisible seulement si son texte commence par « Click any mouse button » — .cpp l. 1775-1792. SexUI = GI.SexInterface (dump du PC l. 3030-3038 : VariableGet_108 GI vers VariableGet_109 SexInterface vers TextBlock_1) ; Lines->GetText().ToString().StartsWith("Click any mouse button", CaseSensitive) (l. 1778). Opacité à 0 seulement si bOldHelp et opacité > 0 (l. 1779-1785) ; sinon rien, sauf le retour de l'opacité si ce même bloc a été masqué par ce code et porte un autre texte (l. 1786-1792). Un bloc dont le texte ne commence pas par la phrase n'est jamais modifié. UTextBlock::GetText() rend le texte du widget Slate quand il existe (donc la liaison évaluée), sinon Text (TextBlock.cpp:457-465) ; SetText appelle TextDelegate.Unbind() (TextBlock.cpp:469-472) : choisir l'opacité de rendu, qui laisse la liaison, est correct. UWidget::SetRenderOpacity met à jour la propriété et le widget Slate (Widget.cpp:454-463) ; GetRenderOpacity lit le widget Slate (Widget.cpp:443-452). PASS-STATIC.
  2. SearchAction ou son parent introuvable : rien d'inséré, une ligne — .cpp l. 1851-1858 : SearchBox nul, SearchParent nul ou WidgetTree nul : UE_LOG « icone non inseree, <SearchAction | le parent de SearchAction> introuvable dans » puis return nullptr avant tout ConstructWidget : aucun widget n'est créé. L'ancrage étiré dans un CanvasPanel est refusé de la même façon (l. 1859-1865 ; Anchors.h:78, 81 : IsStretchedVertical, IsStretchedHorizontal). PASS-STATIC ; réserve D1 : le cas où AddChild rend nul (parent à un seul enfant) n'est pas traité (l. 1889).
  3. Texture info absente : un « i » — LoadObject<UTexture2D>(nullptr, "/Game/Heavy/New/info.info") (l. 1868) ; si nul : UTextBlock « i », police Bold 18, blanc (l. 1877-1884) ; sinon UImage, SetBrushFromTexture, SetDesiredSizeOverride(28, 28) (l. 1871-1876 ; Image.h:81, 108). La ligne d'insertion nomme l'image (« absente, lettre i », l. 1906-1908). PASS-STATIC.
  4. Ligne de la molette retirée si CAM ne déplace pas le couple à la molette — bSexWheelZoom = false (.cpp l. 126, constante const bool). Seul lecteur de ScrollDownUp dans Source/LifePlay : .cpp l. 1104, Wheel = bSexWheelZoom ? AxisValue(PC, AxisScroll) : 0.f ; Wheel n'entre ensuite que dans des tests contre zéro (l. 1117, 1130) et un terme multiplié (l. 1132), donc nul. L'autre occurrence (LPKeyboardSubsystem.cpp:165) range le nom d'axe dans la catégorie « Camera » du menu Keyboard : elle ne lit aucune valeur. Aucune ligne « Mouse wheel » dans le texte (l. 1748-1758). PASS-STATIC. Observation D2 : la règle est respectée à la lettre, mais Room (Blueprint, hors C++) monte et descend bien le couple avec Maj + molette (CAM-B2p-E-IMPL.md:48 : « Maj + molette avait monté le couple … » ; CAM-B2p-G-SPEC.md:13 : « Z de 100 à 127 (molette …) » ; CAM-B2p-F-VAL.md:64) : l'aide omet un contrôle réel. Décision pour Big Boss.
  5. Hors du sexe, rien ne change — bSexOn (l. 1769) gouverne : SexUI nul hors sexe (l. 1775), donc TextBlock_1 n'est ni lu ni écrit ; l'insertion exige bSexOn (l. 1798) ; l'icône déjà insérée passe en Collapsed (l. 1806, 1807-1810) ; HelpBody est vide (l. 1803) ; bPanelOpen est faux (l. 1821) donc bHelpBoxOpen = false (l. 1822-1825) et la boîte est repliée (l. 1834-1837). PASS-STATIC. Observation D3 : l'opacité 0 posée pendant le sexe n'est pas rendue à la fin du sexe (le code de retour d'opacité est lui aussi derrière bSexOn).
  6. Aide en anglais, touches lues dans les mappages — texte en anglais (l. 1748-1758) ; touches par MappedKeys (l. 295-317), lu à chaque appel dans UInputSettings (même source que AxisValue, l. 272-291). Valeurs attendues avec DefaultInput.ini : SwitchCamera = C (l. 118) ; MoveForward +1 = Z, W, −1 = S (l. 155-157) ; MoveRight −1 = Q, A, +1 = D (l. 158-160) ; LeftRight = Right, Left (l. 150-151) ; DownUp = Up, Down (l. 152-153) ; Pause = SpaceBar (l. 97). PASS-STATIC.

3.2 Contrôles de lecture, fonction par fonction

  • MappedKeys (l. 295-317) : signes de l'axe : Sign == 0 || (Sign > 0 ? Scale > 0 : Scale < 0) (l. 302) ; avec DefaultInput.ini, MoveForward donne Z/W pour +1 et S pour −1, MoveRight donne Q/A pour −1 et D pour +1 : l'ordre des paramètres de Printf (l. 1760-1761) est « avant, arrière, gauche, droite », comme le texte « closer / away » et « orbit ». Sens de l'axe : en mode Scène, Distance - Forward * SexDollySpeed * Step (l. 1132) : Forward > 0 rapproche, donc « closer » = MoveForward +1 (établi). Filtres !IsGamepadKey() && !IsMouseButton() && !IsAxis1D() (l. 303, 310) : FKey les définit (InputCoreTypes.h:72-78) ; GetDisplayName(false) rend un FText (InputCoreTypes.h:89), puis .ToString() ; AddUnique écarte les doublons ; repli « ? » (l. 316). Action : FInputActionKeyMapping::ActionName, Key (PlayerInput.h:148-174) ; GetActionMappings() et GetAxisMappings() non dépréciées (InputSettings.h:316-318). PASS-STATIC. Non filtré : Key.IsValid() (D6, négligeable).
  • BuildControlsHelp (l. 1743-1764) : const, n'écrit rien ; contrôle 12. PASS-STATIC.
  • TickControlsHelp (l. 1766-1842) :
    • garde bSexOn (l. 1769) sur l'insertion (l. 1798), l'aide (l. 1803), la visibilité (l. 1806) ;
    • opacité gardée (FadedLinesOpacity, l. 1781) et rendue (l. 1789) ;
    • PC.CursorREF : ReadObject(PC, "CursorREF") (l. 1797 ; dump du PC l. 3012-3017, 1006-1007) ;
    • une tentative par instance : HelpCursor est affecté avant l'appel (l. 1800-1801), comparé par pointeur faible (l. 1798) : une instance ratée n'est pas retentée ;
    • visibilité : Visible en sexe, Collapsed hors sexe, écrite seulement si elle change (l. 1806-1810) ;
    • info-bulle : SetToolTipText quand le texte change (l. 1811-1814 ; Widget.cpp:526-536 met à jour le widget Slate vivant) ;
    • test du panneau comme IfThenElse_10 du PC : dans le dump, IfThenElse_10.Condition vient de CallFunction_79, Widget::IsVisible() sur CursorREF.BelowCursor (l. 3015-3023) ; le code fait PanelArea->IsVisible() sur le même widget (GetWidgetFromName("BelowCursor"), l. 1820-1821), et UWidget::IsVisible lit la visibilité du widget Slate (Widget.cpp:395-404) : même fonction, même résultat. La fermeture par le PC (ExitSexActions, dump l. 1163-1170) met BelowCursor en Collapsed : bPanelOpen devient faux et la boîte se ferme ;
    • boîte fermée avec le panneau (l. 1822-1825, 1834-1837) ; HelpText et HelpRoot sont valides quand EnsureHelpBox() rend vrai (l. 1919-1931). PASS-STATIC.
  • AttachHelpIcon (l. 1844-1910) : ActionCursor->WidgetTree est un TObjectPtr<UWidgetTree> public (UserWidget.h:1524) ; ConstructWidget<UButton>(UButton::StaticClass(), nom) (WidgetTree.h:106 ; TSubclassOf(UClass*) implicite, SubclassOf.h:33 ; précédent LPBonusRewardSubsystem.cpp:621 dans le même Cursor). Noms LPControlsHelp (l. 1866), LPControlsHelpIcon, LPControlsHelpLetter : une instance neuve a un arbre neuf (pas de collision) ; une icône déjà présente est reprise par nom avant (l. 1846-1850). AddChild, pas InsertChildAt : InsertChildAt appelle AddChild puis ShiftChild, qui ne réordonne que Slots ; l'en-tête le dit (« This does not update the live slate version, it requires a rebuild of the whole UI », PanelWidget.h:69-72, PanelWidget.cpp:184-198). AddChild passe par OnSlotAdded, qui construit le slot Slate vivant pour HorizontalBox (HorizontalBox.cpp:32-39) et CanvasPanel (CanvasPanel.cpp:33-40). L'icône vient donc en fin de rangée : l'écart E1 est cohérent. Slots : UHorizontalBoxSlot::SetVerticalAlignment, SetPadding (HorizontalBoxSlot.h:48, 63) ; UCanvasPanelSlot::GetAnchors/GetPosition/GetSize/GetAlignment/GetZOrder const et leurs Set… (CanvasPanelSlot.h:104-156) ; UButton::SetBackgroundColor(FLinearColor) (Button.h:129) ; UContentWidget::SetContent (ContentWidget.h:23) ; UWidget::Slot public (précédent LPBonusRewardSubsystem.cpp:694). PASS-STATIC, sauf D1 (AddChild nul) ; autres types de panneau (WrapBox, ScrollBox) : l'icône est ajoutée sans réglage de slot (acceptable).
  • ToggleControlsHelp (l. 1912-1917) : UFUNCTION() (.h l. 156) ; bascule bHelpBoxOpen, une ligne, puis TickControlsHelp(). TickControlsHelp n'appelle jamais ToggleControlsHelp ni ne déclenche OnClicked : récursion d'un seul niveau, sans boucle. Le clic peut survenir pendant un traitement Slate : TickControlsHelp y crée au plus la boîte du viewport (AddViewportWidgetContent), ce que fait déjà EnsureWidget depuis un ticker. PASS-STATIC.
  • EnsureHelpBox / RemoveHelpBox (l. 1919-1969) : même cycle de vie que EnsureWidget / RemoveWidget (l. 1682-1730) : retour vrai si la racine existe et le viewport est le même, sinon RemoveHelpBox() puis nouvelle racine ; AddViewportWidgetContent(…, HelpZOrder = 60) ; RemoveViewportWidgetContent. RemoveHelpBox() est appelé dans Deinitialize (l. 629, juste après RemoveWidget()). FCoreStyle::Get().GetBrush("WhiteBrush") : la brosse existe dans StarshipCoreStyle.cpp:213 et CoreStyle.cpp:301 (FSlateColorBrush blanc, teinte par BorderBackgroundColor à 75 %). PASS-STATIC.

3.3 API du moteur 5.8 lues (D:\Game\Launcher\Engine\UE_5.8\Engine\Source\Runtime)

API Source lue Résultat
UTextBlock::GetText() const, SetText(FText), liaison UMG/Private/Components/TextBlock.cpp:457-465, 469-483 GetText : texte du widget Slate (liaison évaluée) sinon Text ; SetText délie TextDelegate
UWidget::GetRenderOpacity() const, SetRenderOpacity(float), SetToolTipText(const FText&), IsVisible() const, GetVisibility() const UMG/Public/Components/Widget.h:556, 582, 586, 598, 602 ; Widget.cpp:395-404, 443-463, 526-536 conformes
UButton::SetBackgroundColor(FLinearColor), OnClicked Button.h:18, 76, 129 conformes ; dynamique multicast
UImage::SetDesiredSizeOverride(FVector2D), SetBrushFromTexture(UTexture2D*, bool) Image.h:81, 108 conformes (la variante dépréciée est SetBrushSize, non utilisée)
UPanelWidget::AddChild(UWidget*) ; InsertChildAt ; rend nul si le panneau est à enfant unique et non vide PanelWidget.h:59-79 ; PanelWidget.cpp:132-142, 184-198 conformes ; le retour nul existe (D1)
FAnchors::IsStretchedHorizontal/Vertical() const Slate/Public/Widgets/Layout/Anchors.h:78, 81 conformes
FKey::IsAxis1D/IsMouseButton/IsGamepadKey/IsDigital/GetDisplayName(bool) InputCore/Classes/InputCoreTypes.h:72-89 conformes
FInputActionKeyMapping, FInputAxisKeyMapping ; UInputSettings::GetActionMappings/GetAxisMappings Engine/Classes/GameFramework/PlayerInput.h:148-174, 216-230 ; InputSettings.h:316-318 conformes
FCoreStyle « WhiteBrush » SlateCore/Private/Styling/StarshipCoreStyle.cpp:213, CoreStyle.cpp:301 présente
UUserWidget::WidgetTree, GetWidgetFromName(const FName&) const ; UWidgetTree::ConstructWidget UserWidget.h:1426, 1524 ; WidgetTree.h:106 conformes
TSubclassOf(UClass*) CoreUObject/Public/Templates/SubclassOf.h:33 implicite

3.4 Compilation probable sous /we4456 /we4458 /we4459

  • C4456 / C4457 (locaux englobants) : chaque fonction neuve relue en entier. TickControlsHelp : GI, bSexOn, World, PC, SexUI, Lines (portée du if), bOldHelp, ActionCursor, HelpBody, HelpIcon (portée du if), IconVisibility, PanelArea, bPanelOpen : tous distincts. AttachHelpIcon : paramètre ActionCursor, locaux Existing (portée du if), SearchBox, SearchParent, SearchCanvasSlot, IconButton, IconTexture, IconImage, IconLetter (blocs distincts), IconSlot, RowSlot, CanvasSlot : distincts. EnsureHelpBox : GI, ViewportClient, NewHelpText, NewHelpRoot. RemoveHelpBox : ViewportClient (portée du if). MappedKeys : paramètres Name, Sign, locaux Names, Settings, Map (deux boucles, portées distinctes), bSign. BuildControlsHelp : une déclaration using (pas de variable). Aucun doublon.
  • C4458 (membres) : .h entier relu (l. 1-179). Membres de la classe : TickHandle, SwitchKeyHandle, bSwitchKeyDown, LastEmptyView, WitnessCooldown, OcclusionCooldown, bCharGenSeen, CharGenRepairTimeout, C1Cooldown, bMenuOnScreen, SexKeysCooldown, CoupleGrabMode, bLeftWasDown, bRightWasDown, bGrabMoved, bGrabHasHeight, GrabHeight, GrabOffset, GrabMouseStart, GrabLastMouseX, GrabTurned, GrabCamera, GrabCameraLocation, GrabCameraRotation, bCursorWarned, LastCouplePivot, bHasLastPivot, bHiddenRoomWarned, CoupleBoundsCooldown, CoupleMoveCooldown, bGrabJustEnded, bBoundsLogged, LastSpotBlocker, LastSurfaceRefused, bSceneFocusChecked, bFocusClickWasDown, GrabLeadStart, LastSexView, bSexC3Was, bMiddleWasDown, bMiddleHadMouse, bMiddleMoved, MiddleHeld, MiddleStart, les 9 membres neufs, Root, Text, Viewport, ShownLabel, bShown. Aucun nouveau local ni paramètre ne porte l'un de ces noms (les locaux HelpBody, HelpIcon diffèrent de HelpText, HelpButton).
  • C4459 (portée globale ou espace anonyme) : les constantes et MappedKeys sont dans LPCameraModeLocal (espace nommé, l. 42). Grep, sur Source/LifePlay hors CameraMode, des 25 noms locaux neufs (^\t?[A-Za-z_].*\b(Lines|Existing|CanvasSlot|Names|Sign|bSign|RowSlot|IconSlot|PanelArea|HelpBody|HelpIcon|SexUI|bOldHelp|bPanelOpen|IconVisibility|SearchBox|SearchParent|SearchCanvasSlot|IconButton|IconTexture|IconImage|IconLetter|ActionCursor|NewHelpText|NewHelpRoot)\b) : hits Existing (LPDebugSubsystem.cpp:257, paramètre de fonction), CanvasSlot (LPBonusRewardSubsystem.cpp:695-713, local), Names (LPDirectControlSubsystem.cpp:1023, LPSessionLogSubsystem.cpp:321, LPShowCoupleSubsystem.cpp:585 : locaux, les deux extrêmes relus aux l. 575-588 et 1012-1025), Lines (LPHairstyleSubsystem.cpp:284, LPKeyboardSubsystem.cpp:769, LPModGuard.cpp:515, LPModInfoSubsystem.cpp:221 et :385, LPModInfoSubsystem.h:58 : locaux ou paramètres). Aucune déclaration de portée globale. Les noms de membres, fonctions et constantes neufs (grep de 24 noms) : 90 occurrences, toutes dans CameraMode. CameraMode.cpp n'est de toute façon pas dans une unité unifiée (contrôle 11).
  • Types : UObject* ReadObject(const UObject*, FName) (l. 210) reçoit GI et PC ; ternaires X* : nullptr sans conversion ambiguë ; TWeakObjectPtr<UButton/UTextBlock/UUserWidget> déclarés avec des classes avancées (précédent : TWeakObjectPtr<UGameViewportClient> avec UGameViewportClient avancé, .h l. 22 et 175) et définies dans le .cpp (l. 5-13) ; SNew(SOverlay) … + SOverlay::Slot() … [ SNew(SBorder) … [ SAssignNew(…) ] ] de même forme que l. 1697-1706 ; FString::Join(TArray<FString>, const TCHAR*) et ternaire FString/FString(TEXT("?")) de même type. Aucune erreur de type trouvée.
  • Résidu (N8) : un nom (Names, Lines, Sign, Settings, Map) déclaré en portée globale par un en-tête moteur n'est excluable que par compilation ; improbable (Settings et Map existent déjà comme locaux compilés, l. 274, 280). À trancher par BUILD.

4. Diff lu en entier

Chaque diff a été lu en entier (git diff --no-index -U3, deux diffs .cpp et un diff .h ; -U0 et --numstat pour les comptes).

Diff .cpp ajoutées / retirées .h ajoutées / retirées Lignes avant vers après (LF)
I0 vers I1 +2 / −0 (numstat : 2 0 ; -U0 : +2 −0) 0 (diff vide) .cpp 1687 vers 1689
I1 vers final +280 / −0 (numstat : 280 0) +27 / −0 (numstat : 27 0) .cpp 1689 vers 1969 ; .h 152 vers 179
I0 vers final (global) +282 / −0 +27 / −0 .cpp 1687 vers 1969 ; .h 152 vers 179

Sommes : 2 + 280 = 282 (.cpp) et 0 + 27 = 27 (.h), égales au global ; 1687 + 282 = 1969 et 152 + 27 = 179, égales aux lignes comptées (contrôle 1).

Hunks de I1 vers final (lignes de l'état final) :

Fichier Lignes Contenu Point
.cpp 6-14 (+9) 9 includes I1
.cpp 189-202 (+14) 11 constantes d'aide, commentaires, ligne vide I1
.cpp 293-318 (+26) MappedKeys I1
.cpp 629 (+1) RemoveHelpBox(); dans Deinitialize I1
.cpp 635-636 (+2) commentaire et TickControlsHelp(); en tête de Tick I1
.cpp 1742-1969 (+228) BuildControlsHelp, TickControlsHelp, AttachHelpIcon, ToggleControlsHelp, EnsureHelpBox, RemoveHelpBox I1
.h 23-25 (+3) class UButton; class UTextBlock; class UUserWidget; I1
.h 148-171 (+24) 6 fonctions dont l'UFUNCTION, 9 membres I1
.cpp (I0 vers I1) 1447-1448 (+2) commentaire VAL B2p-H N2 et LastSurfaceRefused.Reset(); I0

9 + 14 + 26 + 1 + 2 + 228 = 280 ; 3 + 24 = 27. Aucun ajout non demandé : chaque hunk se rattache à I0 ou I1 de la fiche. Non-régression B2p-C à B2p-H : zéro ligne retirée, zéro ligne modifiée ; les fonctions de C à H (TickSexCamera, TickCoupleMouse, TickCoupleBounds, TickSceneCamera, TickPanelClick, CoupleSpotFree, ComputeLabel) ne reçoivent que le Reset() de I0 (l. 1448). TickSexCamera garde bSexWheelZoom (l. 1104) et l'appel de TickPanelClick (l. 1095).

5. Verdict

Point Verdict
I0 (N2) PASS-STATIC : Reset() dans la seule branche où le couple bouge, rien d'autre (§ 2, § 4)
I1, consigne 1 PASS-STATIC
I1, consigne 2 PASS-STATIC, réserve D1
I1, consigne 3 PASS-STATIC
I1, consigne 4 PASS-STATIC, observation D2
I1, consigne 5 PASS-STATIC, observation D3
I1, consigne 6 PASS-STATIC
I1, MappedKeys, BuildControlsHelp, TickControlsHelp, AttachHelpIcon, ToggleControlsHelp, EnsureHelpBox/RemoveHelpBox PASS-STATIC (§ 3.2)
Contrôles transverses (types, includes, dépendances, C4456/C4458/C4459 par lecture, non-régression) PASS-STATIC
Global PASS-STATIC : aucun défaut bloquant. Non couvert : la compilation (BLOCKED-compilation, à confier à BUILD) et tout comportement en jeu (reporté V7).

6. Points à confirmer par l'utilisateur (cases)

  • BUILD : LifePlay compile avec 0 erreur et 0 avertissement nouveau sous V7 ; LPCameraModeSubsystem.cpp doit figurer dans les unités compilées ; UHT régénère LPCameraModeSubsystem.generated.h avec execToggleControlsHelp (première UFUNCTION de la classe).
  • Décision sur D1 : faire traiter AddChild nul avant V7 (correction de quelques lignes, par l'auteur) ou laisser l'essai CAM-86 trancher ; sans correction, la ligne « icone inseree » peut être écrite alors que rien n'est inséré.
  • Décision sur D2 : garder l'aide sans la ligne « Maj + molette » (règle de Big Boss à la lettre) ou ajouter « Shift + mouse wheel: raise / lower the couple » (contrôle de Room, établi par COMMUN-7).
  • CAM-86 : ligne Aide sexe : icone inseree dans … (parent, classe, n enfants), image … ; icône à côté du champ de filtre ou des boutons, même image que FAQ ; ligne 4 lignes d'origine masquees ; les 4 lignes disparues.
  • CAM-87 : le survol montre l'info-bulle ; le clic ouvre et ferme la boîte ; la boîte est au bon endroit (la SPEC voulait « au même endroit que les 4 lignes » ; le code la met en bas à droite, marge basse de 96 px, l. 1936) ; la fermeture du panneau la ferme ; un clic gauche sur l'icône ne déclenche ni prise du couple ni bascule du panneau.
  • CAM-88 : touches affichées (Z/W, S, Q/A, D, Right/Left, Up/Down, SpaceBar, C), suivant un remappage fait par le menu Keyboard.
  • CAM-89 : hors du sexe, l'icône est absente du Cursor et la boîte fermée ; texte de jouissance de TextBlock_1 visible s'il apparaît (D3).
  • CAM-90 (N2) : deux passages sur la même commode écrivent deux lignes surface refusee.
  • Accepter l'écart non annoncé E4 (boîte Slate au lieu de UBorder/UTextBlock, ombre au lieu de contour).

Tours : 37 / 40

Constats

Défauts bloquants

Aucun (preuves : contrôles 1 à 16, § 3.1 à 3.4 et lecture point par point).

Défauts non bloquants

# Lieu Constat Niveau de preuve État
D1 .cpp l. 1889-1909 UPanelWidget::AddChild rend nullptr quand le parent de SearchAction est un panneau à enfant unique déjà occupé (PanelWidget.cpp:139-142, bCanHaveMultipleChildren faux). IconSlot est alors nul : aucun réglage de slot, mais le journal écrit quand même « icone inseree dans … » (l. 1906-1908) et la fonction rend le bouton orphelin, que TickControlsHelp traite comme une icône (l. 1801, 1804-1810). La consigne 2 de Big Boss (« ne rien insérer et écrire une ligne de témoin ») est tenue pour les cas qu'elle nomme (SearchAction ou parent introuvable), non pour celui-ci ; l'essai CAM-86 peut passer à tort sur la seule ligne. Correction possible : if (!IconSlot) vers une ligne « non inseree, n'accepte pas d'enfant » et return nullptr. établi (lecture du code et du moteur) ; probabilité en jeu : estimée faible (DIAG § 1.2 : le parent est probablement HorizontalBox_0 ou WrapBox) levé par la reprise D1 (voir la section « Reprise D1 » en fin de document)
D2 .cpp l. 1745-1758 L'aide ne cite pas la molette (règle de Big Boss respectée : CAM ne la lit plus, l. 126 et 1104). Mais Room monte et descend le couple avec Maj + molette (CAM-B2p-E-IMPL.md:48, CAM-B2p-G-SPEC.md:13, CAM-B2p-F-VAL.md:64) : contrôle réel absent de l'aide. La voie d'origine « Maj + bouton tenu + souris » (SPEC § 1.6, dernière ligne) est absente aussi (omission de la SPEC § 2.3). établi (documents) ; absence de lecture de la molette par le C++ : établi (Grep) ouvert, décision de Big Boss
D3 .cpp l. 1775-1792 L'opacité 0 de TextBlock_1 est posée en sexe et n'est jamais rendue hors sexe : SexUI est nul quand bSexOn est faux, donc la branche de restitution (l. 1786-1792) ne s'exécute pas. Si GI.SexOn passe à faux avant que TextBlock_1 affiche un autre texte (jouissance), ce texte reste invisible. Le code ne touche à rien hors sexe, ce qui respecte la consigne 5. Le texte de jouissance n'est montré qu'en sexe (DIAG § 1.1, estimé fort). estimé (lecture) ; essai : CAM-89 ouvert
D4 .cpp l. 1798-1802 Une seule tentative par instance de Cursor (consigne 2) : si PC.CursorREF est déjà non nul mais que son arbre n'est pas encore complet, l'instance est consommée sans retenter. Conforme à la consigne ; un témoin « introuvable » au premier essai révélerait le cas. estimé ouvert, négligeable
D5 .cpp l. 1803, 1859-1865 BuildControlsHelp() (8 parcours des mappages, allocations de FString) est appelée à chaque passage de TickControlsHelp, soit toutes les 0,1 s en sexe : coût négligeable. L'ancrage étiré est testé seulement quand SearchAction est dans un CanvasPanel (Cast<UCanvasPanelSlot> nul ailleurs : aucun refus). établi ouvert, cosmétique
D6 .cpp l. 303, 310, 1762 MappedKeys ne filtre pas Key.IsValid() ; l'ordre des touches suit DefaultInput.ini : LeftRight donne Right/Left, DownUp donne Up/Down (« Right/Left, Up/Down: move the couple »), lecture un peu inversée. établi ouvert, cosmétique
N5 IMPL Écart de gabarit (§ 1) : tête commune, « Ce qui n'a pas été fait », Tours : absents ; numérotation 0 à 5. établi non bloquant, à reporter à l'auteur
N6 IMPL § 3 L'écart E4 (boîte Slate au lieu de UBorder/UTextBlock de la SPEC § 2.1 I-c) figure en § 2.2 mais pas en § 3 « Écarts à la SPEC ». établi non bloquant
N7 IMPL § 2.2 « 10 membres » : le .h en a 9 (l. 161-171). Les autres chiffres (9 includes, 11 constantes, 6 fonctions, MD5, lignes) sont exacts. établi non bloquant
N8 compilation Résidu C4459 d'en-têtes moteur : non excluable sans compilation (voir § 3.4). estimé (faible probabilité) à trancher par BUILD

Non mesuré

Compilation et avertissements ; UHT (execToggleControlsHelp, première UFUNCTION de la classe) ; tout essai en jeu (CAM-86 à CAM-90, reportés V7) ; parent réel de SearchAction dans Cursor_C et classe de ce parent (aucune lecture MCP, décision de Big Boss) ; texte réel de TextBlock_1 (les 4 lignes sont dans un seul bloc : DIAG « estimé fort ») ; si GI.Cursor et PC.CursorREF désignent la même instance (le code n'emploie que PC.CursorREF, comme le PC) ; position visuelle de la boîte d'aide et de l'icône ; rendu des touches (GetDisplayName(false) : noms exacts de Z, W, SpaceBar, Right…) ; évaluation de la liaison Get_TextBlock_1_Text appelée aussi depuis le ticker de CAM (une évaluation de plus toutes les 0,1 s, estimée sans effet) ; LPTestStringCompat et porte G-U3 (sans objet : ni chaîne de compatibilité, ni configuration, ni compilation dans ce lot).

Fichiers à commiter (VAL)

A Docs/Migration/etat/CAM/CAM-B2p-I-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 : v1.ps1, v2.ps1, v3.ps1, I*vs*.diff), hors dépôt.

À verser

  • lp-validate § 1 : les instantanés de Snapshot.ps1 ont la forme <instantané>\Source\LifePlay\<fichier> (chemin relatif conservé) plus manifest.csv ; un script de diff doit viser ce sous-dossier (échec constaté avec v1.ps1).
  • lp-validate § 1 : le crochet ecriture-gardien refuse aussi une redirection > utilisée pour dériver un script par sed ; écrire le script neuf avec Write dans le dossier temporaire.
  • lp-ue-compat / lp-build : UPanelWidget::AddChild rend nullptr pour un panneau à enfant unique non vide (PanelWidget.cpp:139-142) et InsertChildAt ne réordonne pas le Slate vivant (PanelWidget.h:69-72) ; UTextBlock::SetText délie la liaison (TextBlock.cpp:469-472) ; UWidget::IsVisible lit le widget Slate lui-même (Widget.cpp:395-404) ; FCoreStyle possède « WhiteBrush » (StarshipCoreStyle.cpp:213) ; TEXT("a" "b") est licite (ShaderCompiler.cpp:5978-5979).

Reprise D1 (2026-10-14, VAL statique du correctif de D1)

Seul le tableau des défauts (état de D1) et la liste des fichiers de travail ont été touchés dans les sections ci-dessus ; tout le reste est inchangé. Les numéros de ligne des sections 2 à 5 décrivent l'état CAM-B2p-I2 (.cpp 73CC3BD6…, 1969 lignes) ; après la reprise, le code placé après la l. 1889 est décalé de +7.

# Contrôle Commande / lecture Résultat brut Verdict
R1 MD5, taille, fins de ligne pwsh -NoProfile -File scratchpad\v3.ps1 .cpp size=86153 CR=0 LF=1976 first3=2f2f20 last=0a MD5=0F383EF777DED71608347E17D4A0861F ; .h size=9813 CR=0 LF=179 MD5=6EDBBCCF61759B8C6EE4890DF9BC5FE7 PASS : égalent l'attendu (MD5, 1976 lignes, .h inchangé), LF pur, sans BOM
R2 Instantané CAM-B2p-I2 même script, manifest.csv et octets .cpp 85842 / CR=0 / LF=1969 / 73CC3BD667059A903E44405C5E793DE3, .h 6EDBBCCF… : identiques à mon état final validé PASS
R3 Diff I2 vers final git diff --no-index -U0 et --numstat .cpp +7 / −0 (numstat : 7 0) ; .h : diff vide ; un seul hunk @@ -1889,0 +1890,7 @@ dans AttachHelpIcon ; 1969 + 7 = 1976 PASS
R4 Contenu du hunk (lu en entier, -U4) .cpp l. 1890-1896 juste après UPanelSlot* IconSlot = SearchParent->AddChild(IconButton); (l. 1889) : if (!IconSlot) puis UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : icone non inseree, %s (%s) refuse un enfant de plus"), *SearchParent->GetName(), *SearchParent->GetClass()->GetName()); puis return nullptr; PASS : texte conforme à la demande (parent et classe)
R5 Aucun usage de IconSlot sans garde Grep IconSlot 3 occurrences : l. 1889 (définition), l. 1890 (garde), l. 1897 (Cast<UHorizontalBoxSlot>(IconSlot)) et l. 1902 (Cast<UCanvasPanelSlot>(IconSlot)) : les deux Cast suivent le retour de la l. 1895 ; la ligne « icone inseree » (l. 1913) et return IconButton (l. 1916) aussi PASS : chemin nul = journal « non inseree » puis nullptr, rien d'autre
R6 Format UE_LOG lecture l. 1893-1894 2 %s, 2 arguments (FString déréférencées) PASS-STATIC
R7 Effet de return nullptr en amont lecture de TickControlsHelp l. 1798-1810 HelpCursor est déjà affecté (une tentative par instance, pas de nouvel essai) ; HelpButton reçoit nullptr donc HelpButton.Get() est nul et le bloc de visibilité et d'info-bulle est sauté ; IconButton, sans référence ni parent, est ramassé par le GC (son lien OnClicked pointe vers le sous-système, sans effet) PASS-STATIC (ramassage : estimé)
R8 Homonymes nouveaux lecture du hunk aucun nouveau local, paramètre, membre ni constante (le hunk n'emploie que IconSlot, SearchParent, LogLifePlay, existants) : pas de C4456, C4458 ni C4459 PASS-STATIC
R9 Fichiers touchés Get-ChildItem .cpp 14:58:56 ; .h 14:43:12 (inchangé) ; instantané I2 14:58:51 PASS

D1 : levé (PASS-STATIC). Le cas « AddChild rend nul » écrit la ligne « icone non inseree, () refuse un enfant de plus » et rend nullptr avant tout usage du slot et avant la ligne « icone inseree » ; la consigne 2 de Big Boss est désormais tenue pour ce cas. L'essai CAM-86 ne peut plus passer à tort sur la seule ligne « icone inseree ».

Verdict de la reprise : PASS-STATIC, aucun défaut nouveau, aucun bloquant. Le verdict global de la § 5 reste PASS-STATIC (compilation à BUILD, comportement à V7). D2 (Maj + molette) et D3 restent ouverts, à Big Boss ; ils ne sont pas tranchés ici. Non mesuré en plus : compilation du nouveau .cpp ; passage réel par la branche nulle (nécessite un parent à enfant unique, non lu faute de MCP).

Correction de comptage : dans R5, les occurrences de IconSlot sont 4 (l. 1889, 1890, 1897, 1902), et non 3.

Tours de la reprise : 5 (écriture du script, exécution, 2 lectures et recherche, écriture du livrable) ; total du livrable : 42 (dépassement du budget de 40 de la fiche d'origine, la reprise étant une demande ultérieure).