Fiche CAM-B2d — Niveau N3 — Modèle prévu opus | utilisé claude-opus-5-5 (Opus 5.5) — Agent lp-mcp-bp — Skills sk-protocol, sk-change-safety, lp-mcp, lp-lot # CAM-B2d-IMPL : C2 sur la rotation de contrôle (B2d-1 à B2d-4) (2026-10-13) Consignes : `CAM-B2d-FICHE.md` § « Consignes de l'agent » ; rubriques communes de `CAM-B2b-FICHES.md` (l. 23-63). SPEC : `CAM-SPEC.md` § « CAM-B2d » (l. 384-393). UE-GO de Big Boss pour le passage Room B2d + B2e (2026-10-13, cité par l'orchestrateur). Éditeur headless PID 22060, port 8000, connexion `unreal-mcp` partagée (MCPA-T1) ; Room chargé par la session (état B2b F1-F7 enregistré). Consigne de l'orchestrateur, **différente de la fiche** : pas de `save_assets` en B2d, B2e enregistre Room dans le même éditeur. Un seul `call_tool` par message. **53 appels MCP** : 4 `find_node_types`, 13 `create_node` (dont 1 en erreur voulue, § 2 E1), 6 `delete_node`, 21 `connect_pins`, 1 `break_pins`, 5 `get_node_infos` (dont 1 en erreur), 2 `find_nodes`, 1 `compile_blueprint`. Aucun `set_pin_value`, `save_assets`, `write_graph_dsl` ni `ProgrammaticToolset`. Aucune écriture par HyperAIStudio. ## 1. Fichiers modifiés - `Content/BPs/Room.uasset` : modifié **en mémoire de l'éditeur seulement**, non enregistré (consigne : B2e enregistre). Le MD5 du disque est celui de l'instantané (§ 3). - Graphe `/Game/BPs/Room.Room:EventGraph` : `ComputeC2Target` (zone de 454, x 34450 y 1850), `CircleCamera` (96, x 5650 y 4450), `LeftRight` (97, x 5250 y 2950), `PlaceC2` (CE48, x 34000 y 3000). ### 1.1 Identifiants trouvés (`find_node_types`) - `ControlRotation`, contexte = sortie 0 de `VariableGet_262` : `Pion|SetControlRotation`, `Pion|GetControlRotation`. - `Rotator` : `Math|Rotateur|BreakRotator`, `Math|Rotateur|MakeRotator`. - `GetForwardVector` : `Transformation|GetForwardVector`, `Math|Vecteur|GetForwardVector` (version rotateur, relue : entrée `InRot` Rotator). - `GetActorRotation` : `Transformation|GetActorRotation`. ### 1.2 Lecture avant câblage (`get_node_infos` groupé, 11 créés + 15 existants), conforme à la fiche - 453 `Transformation|GetActorForwardVector` : self ← 261.0 ; out 0 → 454.A 0. 454 `Normalize2D(Vector)` : A 0, Tolerance 1 ; out 0 → PO17.A. - 262 `|GetPC` (out 0 `PC Référence d'objet`, self ← `VariableGet_257` GI) → 394.1, 434.1, 439.0, 466.1. - 261 `|GetPlayerCharacter` (out 0 `Person Référence d'objet`) → 264, 265, 266, 434.2, PO0.1, 451, 452, 453, MakeArray_5. - 96 : execute ← CE1.1 ; Condition ← PO8 ; then 0 → 67.0 ; else 1 → IfThenElse_26. PO28 `float+float` : A ← 292, B ← CE1.2 (DeltaSeconds) ; out → 67.1. 67 `|SetC2Yaw` : then libre. - 97 : execute ← CE4.1 ; Condition ← PO8 ; then 0 → 68.0 ; else 1 → IfThenElse_9. PO30 : A ← 293, B ← PO29 ; out → 68.1. 68 : then libre. - CE48 `AddEvent|Custom|PlaceC2` : then (out 1) → 461.execute 0. 461 `|EnsureCamRoom` : then → `CallFunction_462`. - `K2Node_CallFunction_521` : **nœud existant**, `MakeRotator` (8240, 5872 ; sortie → `CallFunction_90`.2, Pitch ← `CommutativeAssociativeBinaryOperator_12`, Yaw ← `CallFunction_84`.2). Lu comme sonde (mon 3e `MakeRotator` a reçu le n° 522) ; non touché. ### 1.3 Nœuds créés (préfixe `refPath` : `/Game/BPs/Room.Room:EventGraph.`) | Rôle | `refPath` (suffixe) | `type_id` | Broches relues (index) | Position | |---|---|---|---|---| | CR = `GetControlRotation` (Controller) | `K2Node_CallFunction_524` | `Pion\|GetControlRotation` + `declaring_class` (§ 2 E1) | self in 0 (`Controller Référence d'objet`) ; ReturnValue out 0 (Rotator) | 33700, 2200 | | `GetForwardVector(CR)` | `K2Node_CallFunction_513` | `Math\|Vecteur\|GetForwardVector` | InRot in 0 ; ReturnValue out 0 (Vector) | 34000, 2050 | | BR = `BreakRotator(CR)` (partagé) | `K2Node_CallFunction_514` | `Math\|Rotateur\|BreakRotator` | InRot in 0 ; Roll out 0, Pitch out 1, Yaw out 2 (simple précision) | 34000, 1900 | | `MakeRotator` (CircleCamera) | `K2Node_CallFunction_515` | `Math\|Rotateur\|MakeRotator` | Roll in 0, Pitch in 1, Yaw in 2 (simple précision) ; out 0 | 6050, 4250 | | `SetControlRotation` (CircleCamera) | `K2Node_CallFunction_516` | `Pion\|SetControlRotation` | execute in 0, self in 1 (`Controller`), NewRotation in 2 (Rotator par référence) ; then out 0 | 6300, 4400 | | `MakeRotator` (LeftRight) | `K2Node_CallFunction_517` | `Math\|Rotateur\|MakeRotator` | idem 515 | 5900, 2700 | | `SetControlRotation` (LeftRight) | `K2Node_CallFunction_518` | `Pion\|SetControlRotation` | idem 516 | 6150, 2900 | | `GetActorRotation(MC)` (PlaceC2) | `K2Node_CallFunction_519` | `Transformation\|GetActorRotation` | self in 0 (`Acteur`) ; ReturnValue out 0 (Rotator) | 37000, 3000 | | `BreakRotator` (PlaceC2) | `K2Node_CallFunction_520` | `Math\|Rotateur\|BreakRotator` | idem 514 | 37250, 3000 | | `MakeRotator` (PlaceC2) | `K2Node_CallFunction_522` | `Math\|Rotateur\|MakeRotator` | idem 515 | 37500, 3000 | | `SetControlRotation` (PlaceC2) | `K2Node_CallFunction_523` | `Pion\|SetControlRotation` | idem 516 | 37750, 3100 | Roll et Pitch des trois `MakeRotator` : défaut `0.0`, non liés (SPEC : `MakeRotator(0, 0, Yaw)`). ### 1.4 Nœuds supprimés (`delete_node`, 6, tous `{"returnValue":null}`) | Nœud | Rôle | Ligne SPEC | |---|---|---| | `K2Node_CallFunction_512` | mon premier `GetControlRotation`, version **Pawn** (self `Pion`), jamais relié | § 2 E1 | | `K2Node_CallFunction_453` | `GetActorForwardVector(MC)` | B2d-1 | | `K2Node_VariableSet_67` puis `K2Node_VariableGet_292` | `Définir C2Yaw` / `Obtenir C2Yaw` (CircleCamera) | B2d-2 | | `K2Node_VariableSet_68` puis `K2Node_VariableGet_293` | idem (LeftRight) | B2d-3 | ### 1.5 Liens coupés - Par `break_pins` (1) : C1 `CustomEvent_48.then 1` → `CallFunction_461.execute 0` (B2d-4). - Par `delete_node` (§ 2 E2) : 453.0 → 454.A 0 et 261.0 → 453.self ; 292.0 → PO28.A 0 ; 96.then 0 → 67.execute 0 et PO28.0 → 67.C2Yaw 1 ; 293.0 → PO30.A 0 ; 97.then 0 → 68.execute 0 et PO30.0 → 68.C2Yaw 1. ### 1.6 Liens faits (`connect_pins`, 21, tous `{"returnValue":null}`) | # | Sortie | Entrée | Ligne SPEC | |---|---|---|---| | L1 | VariableGet_262.0 (PC) | CallFunction_524.self 0 | B2d-1 | | L2 | CallFunction_524.0 | CallFunction_513.InRot 0 | B2d-1 | | L3 | CallFunction_513.0 | CallFunction_454.A 0 | B2d-1 | | L4 | CallFunction_524.0 | CallFunction_514.InRot 0 | BR | | L5 | CallFunction_514.Yaw 2 | PromotableOperator_28.A 0 | B2d-2 | | L6 | PromotableOperator_28.0 | CallFunction_515.Yaw 2 | B2d-2 | | L7 | CallFunction_515.0 | CallFunction_516.NewRotation 2 | B2d-2 | | L8 | VariableGet_262.0 | CallFunction_516.self 1 | B2d-2 | | L9 | IfThenElse_96.then 0 | CallFunction_516.execute 0 | B2d-2 | | L10 | CallFunction_514.Yaw 2 | PromotableOperator_30.A 0 | B2d-3 | | L11 | PromotableOperator_30.0 | CallFunction_517.Yaw 2 | B2d-3 | | L12 | CallFunction_517.0 | CallFunction_518.NewRotation 2 | B2d-3 | | L13 | VariableGet_262.0 | CallFunction_518.self 1 | B2d-3 | | L14 | IfThenElse_97.then 0 | CallFunction_518.execute 0 | B2d-3 | | L15 | VariableGet_261.0 (MC) | CallFunction_519.self 0 | B2d-4 | | L16 | CallFunction_519.0 | CallFunction_520.InRot 0 | B2d-4 | | L17 | CallFunction_520.Yaw 2 | CallFunction_522.Yaw 2 | B2d-4 | | L18 | CallFunction_522.0 | CallFunction_523.NewRotation 2 | B2d-4 | | L19 | VariableGet_262.0 | CallFunction_523.self 1 | B2d-4 | | L20 | CustomEvent_48.then 1 | CallFunction_523.execute 0 | B2d-4 | | L21 | CallFunction_523.then 0 | CallFunction_461.execute 0 | B2d-4 | `516.then` et `518.then` laissés libres (fin de l'événement côté C2, comme 67 et 68 avant). ### 1.7 Relecture (`get_node_infos` groupé final, 20 nœuds : 11 créés + 454, PO28, PO30, 96, 97, CE48, 461, 262, 261) Les 21 liens apparaissent des deux côtés. 454.A ← 513 seul ; PO28 : A ← 514.Yaw, B ← CE1.DeltaSeconds, out → 515 seul (`float+float`, double précision conservée) ; PO30 : A ← 514.Yaw, B ← PO29, out → 517 seul ; 96 : then → 516 seul, else → IfThenElse_26 (inchangé), Condition ← PO8 ; 97 : then → 518 seul, else → IfThenElse_9 (inchangé), Condition ← PO8 ; CE48.then → 523 seul ; 461 : execute ← 523, then → 462 (inchangé) ; 262 → 394, 434, 439, 466 (inchangés) + 524, 516, 518, 523 ; 261 → liens d'origine sans 453, + 519. Aucune divergence. Absence : `find_nodes {title:"Forward"}` → `[K2Node_CallFunction_513]` seul (témoin positif : la recherche par sous-chaîne marche ; 453 n'est plus dans le graphe). `get_node_infos` sur 512 → `Parameter error: …K2Node_CallFunction_512 is not valid EdGraphNode`. Voir § 2 E5 pour 453 et § 5 pour 67, 68, 292, 293. Chaîne obtenue : - `ComputeC2Target` : direction arrière = `Normalize2D(GetForwardVector(PC.GetControlRotation()))` (au lieu de `GetActorForwardVector(MC)`), puis PO17 (× −1) → 497 (`C2Yaw`, désormais toujours 0) → PO3, inchangés. - `CircleCamera` en C2 : `PC.SetControlRotation(0, 0, ControlYaw + DeltaSeconds)`, fin. Hors C2 : `IfThenElse_26`. - `LeftRight` en C2 : `PC.SetControlRotation(0, 0, ControlYaw + AxisValue × 2)`, fin. Hors C2 : `IfThenElse_9`. - `PlaceC2` : `PC.SetControlRotation(0, 0, MC.GetActorRotation().Yaw)` → `EnsureCamRoom` (461) → suite inchangée. ### 1.8 Compilation `compile_blueprint {blueprint:{refPath:"/Game/BPs/Room.Room"}, warnings_as_errors:true}` → `{"returnValue":null}`, aucune exception. `hyper_blueprint_debug_inspect` non appelé : il était absent du serveur le 2026-10-10 (mémoire `unreal-mcp-reconnect`), non revérifié. Journal de l'éditeur non lu : chemin `-abslog` non fourni. Pas de `save_assets` (consigne de l'orchestrateur). ## 2. Écarts à la SPEC - **E1 (identifiant, résolu ; méthode hors fiche)** : `create_node` avec `Pion|GetControlRotation` a créé la version **Pawn** (512, self `Pion Référence d'objet`), qui n'accepte pas 262 (`PC Référence d'objet`). Le `find_node_types` avec contexte ne donnait qu'un seul `type_id` ; `create_node` ne prend pas ce contexte. Un appel `create_node` volontairement mal formé (`pos:"schema"`) a renvoyé `Function input params Json - {…,"declaring_class":null}`. Ce retour révèle un paramètre caché. `create_node {…, declaring_class:"/Script/Engine.Controller"}` a créé 524, relu `Controller Référence d'objet`. 512, jamais relié, a été supprimé. La cible est conforme à la SPEC : la marche lit `PC->GetControlRotation()` (`Source/LifePlay/LPDirectControlSubsystem.cpp:887`). `Pion|SetControlRotation` a donné d'emblée la version Controller (516, 518, 523 : self `Controller`). - **E2 (procédure, même résultat)** : les 5 coupures nommées par la fiche (453 → 454.A, 292 → PO28.A, 96.then → 67, 293 → PO30.A, 97.then → 68) n'ont pas été faites par `break_pins`. Elles résultent du `delete_node` du nœud à retirer, fait **avant** de relier le remplaçant : un nœud supprimé perd tous ses liens. Motif : le budget de tours (60), qui n'aurait pas couvert 5 appels de plus. Le résultat est vérifié au § 1.7 : chaque entrée n'a qu'un lien, celui du remplaçant. Seule coupure explicite : CE48.then → 461, puisque 461 reste en place. - **E3 (types)** : `BreakRotator.Yaw` (simple précision) → PO28.A / PO30.A (double) et PO28 / PO30 (double) → `MakeRotator.Yaw` (simple) : conversions implicites. PO28 et PO30 restent `float+float` en double. La compilation stricte les accepte. - **E4 (contrôle partagé)** : un seul `GetControlRotation` (524) et un seul `BreakRotator` (514), purs, servent B2d-1 à B2d-3, comme le permet la SPEC (l. 392). Ils sont reliés de loin : `ComputeC2Target` est en x ≈ 34000, CircleCamera et LeftRight en x ≈ 5000-6000. Effet cosmétique seulement. - **E5 (fait inattendu, sans effet sur le graphe)** : après sa suppression, `get_node_infos` sur 453 rend encore un objet (broches vides), alors que 512 est refusé (`not valid EdGraphNode`). `find_nodes` établit que 453 n'est plus dans `EventGraph`. Hypothèse estimée : l'objet reste en mémoire, tenu par le tampon d'annulation, jusqu'au ramasse-miettes ou à l'enregistrement. À trancher par le dump après enregistrement (§ 5.1). - Positions (cosmétique, non prescrites) : les nœuds de B2d-4 sont en x 37000-37750, y 3000-3100. Ils sont donc loin de CE48 (34000, 3000) et de 461 (34300, 3000), qui n'étaient pas connus à la création. - Garde `Accessed None` : aucune `IsValid` ajoutée. « Vide » n'est pas un état normal de `GI.PC` (262) ni de `GI.PlayerCharacter` (261) en C2, et ni la SPEC ni la fiche n'en demandent. Les lectures nouvelles sont au § 5.3. - Comportement : hors C2, `CircleCamera` et `LeftRight` sont inchangés (`else` relus). En C2, le changement est voulu par la SPEC : la rotation de contrôle remplace `C2Yaw`. `PlaceC2` pose désormais la rotation de contrôle avant `EnsureCamRoom`. ## 3. Instantané Fait par l'orchestrateur (non refait, consigne) : `D:\Game\LifePlay_Backup_5.5\CAM-B2d\`. Relevé : ``` date -Iseconds; md5sum "/d/Game/LifePlay-master 5.8/Content/BPs/Room.uasset"; grep -i "Room.uasset" "/d/Game/LifePlay_Backup_5.5/CAM-B2d/manifest.csv"; git -C "/d/Game/LifePlay-master 5.8" status --short -- Content/BPs/Room.uasset; git -C "/d/Game/LifePlay-master 5.8" rev-parse --short HEAD 2026-10-13T17:19:04+02:00 e0517c63541f249bd9dbda5c39815ba5 */d/Game/LifePlay-master 5.8/Content/BPs/Room.uasset "Content\BPs\Room.uasset","True","6318319","E0517C63541F249BD9DBDA5C39815BA5","2026-10-13T17:11:26" M Content/BPs/Room.uasset 0745917 ``` Le MD5 du disque après B2d est celui de l'instantané : rien n'a été enregistré (attendu). Le `M` vient de B2b, déjà enregistré et non commité. Retour arrière avant l'enregistrement de B2e : fermer l'éditeur sans enregistrer, ce qui perd B2d. Après : `Tools\Restore.ps1 -Lot CAM-B2d -Only Content\BPs\Room.uasset`, éditeur fermé, sur décision de l'orchestrateur (jamais lancé ici). ## 4. Ce qui n'a pas été fait - `save_assets` : non fait, sur consigne de l'orchestrateur (B2e enregistre Room une fois). B2d vit en mémoire de l'éditeur PID 22060 : une fermeture ou un plantage avant l'enregistrement de B2e le perd ; l'instantané reste intact. - Mesures de la fiche « MD5 de Room après l'enregistrement (≠ E0517C63) » : sans objet ici, à prendre après B2e. - Absence de 67, 68, 292 et 293 dans le graphe : non prouvée par `find_nodes`, faute de tours. Elle est établie seulement par `delete_node` → `null` et par l'absence de leurs liens sur PO28, PO30, 96 et 97 (§ 1.7). - `hyper_blueprint_debug_inspect` et journal de l'éditeur : non consultés (§ 1.8). - Comportement à l'exécution : non mesuré (pas de PIE ni de build), seulement déduit du câblage. La preuve en jeu (build empaqueté + `LogStats`) n'est pas de mon rôle. ## 5. Points à confirmer en VAL 1. V5 (`CAM-SPEC.md:393`), après l'enregistrement de B2e, par dump : `GetControlRotation` ≥ 1 (524, de classe `Controller`) ; `SetControlRotation` = 3 (516, 518, 523) ; aucun `GetActorForwardVector` dans `ComputeC2Target` (453 absent du fichier, cf. E5) ; `C2Yaw` écrite 0 fois (aucun `SetC2Yaw` : 67 et 68 absents) ; plus de `VariableGet_292` ni `_293` ; critères de B2b inchangés. 2. Hors C2 : `CircleCamera` → `IfThenElse_26` et `LeftRight` → `IfThenElse_9` sont inchangés (relu). À confirmer en jeu. 3. `LogStats` : aucun `Accessed None` nouveau sur `VariableGet_262`, `_261`, `CallFunction_519`, `_523`, `_524`. Lectures nouvelles : `GI.PC` (262) dans `ComputeC2Target` (524 ; avant, cette fonction lisait 261 par 453) et dans `PlaceC2` **avant** `EnsureCamRoom` (523 ; 262 était déjà lu plus loin, par 466) ; `GI.PlayerCharacter` (261) dans `PlaceC2` (519). Dans CircleCamera et LeftRight, 262 est déjà lu par la condition (PO8 → 439) avant 516 / 518 : aucun risque nouveau là. 4. Essai B2d de la fiche (l. 49-53) : Z s'éloigne de la caméra ; souris et flèches tournent la caméra ; la caméra est derrière le personnage en entrant en C2. Estimé, non lu : si `Person` utilise la rotation de contrôle en lacet (`bUseControllerRotationYaw`), `SetControlRotation` fera aussi tourner le personnage. À observer. 5. `SetControlRotation(0, 0, Yaw)` remet à 0 le tangage et le roulis de contrôle à chaque `CircleCamera`, `LeftRight` et `PlaceC2`. C'est voulu (SPEC : `MakeRotator(0, 0, …)`) ; la hauteur de C2 reste réglée par `C2Height`. 6. Le lacet de contrôle n'est pas borné (somme libre). `SetControlRotation` ne normalise pas (estimé, non lu). Sans effet sur `GetForwardVector`. 7. Compilation stricte jugée sur l'absence d'exception seulement : à recontrôler après B2e et par `CompileAllBlueprints` (V6). 8. E5 : vérifier que l'enregistrement de B2e n'écrit pas 453 (dump : aucun `GetActorForwardVector` dans Room, sauf d'autres nœuds éventuels hors `ComputeC2Target`, à lister). ## 6. Fichiers à commiter - `A Docs/Migration/etat/CAM/CAM-B2d-IMPL.md` (dépôt du jeu, branche `ue5.8`). - `Content/BPs/Room.uasset` : rien pour B2d seul (non enregistré). Il sera enregistré par B2e, puis commité après l'essai commun (fiche, § « Après »). Tours : 59 / 60 ## Constats - À verser : `lp-mcp` § 3. `create_node` accepte un paramètre caché `declaring_class` (ex. `"/Script/Engine.Controller"`) qui choisit la classe quand deux fonctions ont le même `type_id`. Sans lui, `Pion|GetControlRotation` donne la version Pawn. On le voit en provoquant une erreur de conversion (`pos` en chaîne) : la liste des paramètres est rendue. - À verser : `lp-mcp` § 3. Index de broches relus : - `GetControlRotation` (Controller) : self in 0, out 0 ; - `SetControlRotation` : execute in 0, self in 1, NewRotation in 2, then out 0 ; - `BreakRotator` : InRot in 0, Roll out 0, Pitch out 1, Yaw out 2 (simple précision) ; - `MakeRotator` : Roll in 0, Pitch in 1, Yaw in 2, out 0 ; - `Math|Vecteur|GetForwardVector` : InRot in 0, out 0 ; - `Transformation|GetActorRotation` : self in 0, out 0. - À verser : `lp-mcp` § 3. Un nœud supprimé peut encore se lire par son `refPath` (broches vides). Son absence se prouve par `find_nodes {title}`, qui cherche par sous-chaîne, avec un témoin positif. - Les numéros de nœud ne sont pas consécutifs : `CallFunction_521` existait déjà, et le `MakeRotator` suivant est 522. Toujours relire le `refPath` rendu par `create_node`.