From 7a0b3f36719f01e47c258e6e2a3d2073324c1b7d Mon Sep 17 00:00:00 2001 From: edujiro5972 <336072401+edujiro5972@users.noreply.github.com> Date: Thu, 8 Oct 2026 01:40:50 +0200 Subject: [PATCH] PROP / ERR-043 : props d'animation retires avec le corps du PNJ (LPPersonPropGuardSubsystem) (CHG-244) Co-Authored-By: Claude Opus 5.5 --- Docs/Migration/Journal_Modifications.md | 1 + Docs/Migration/Registre_Erreurs.md | 1 + Docs/Migration/etat/PROP/PROP-BUG.md | 119 ++++++++++ Docs/Migration/etat/PROP/PROP-CART.md | 158 +++++++++++++ .../LifePlay/LPPersonPropGuardSubsystem.cpp | 223 ++++++++++++++++++ Source/LifePlay/LPPersonPropGuardSubsystem.h | 38 +++ 6 files changed, 540 insertions(+) create mode 100644 Docs/Migration/etat/PROP/PROP-BUG.md create mode 100644 Docs/Migration/etat/PROP/PROP-CART.md create mode 100644 Source/LifePlay/LPPersonPropGuardSubsystem.cpp create mode 100644 Source/LifePlay/LPPersonPropGuardSubsystem.h diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 63e8e47..02ae986 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -1131,6 +1131,7 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol | CHG-155 | 2026-10-05 | ShowCouple | Un lit est reconnu par son nom. Il ne bloque un déplacement que vers le bas, pour se poser ; le retour horizontal n'est plus arrêté et le contact ne renvoie plus la boîte | après avoir quitté le lit, la boîte ne pouvait plus y revenir | revenir au test `DoesPackageExist` et au filtre du flanc seul | | CHG-156 | 2026-10-05 | Clavier | `LPKeyboardSubsystem` : bouton « Clavier » ajouté à l'exécution dans `MainMenu.SettingsScrollBox` ; panneau pour changer, ajouter ou retirer une touche (actions et axes clavier). Écrit dans `Saved/Config/Windows`, pas dans `GameSettings.sav` | pouvoir réaffecter les touches, dont Q, sans modifier les Blueprints | retirer `LPKeyboardSubsystem.h/.cpp` et la dépendance `UMG` | | CHG-158 | 2026-10-05 | Réglages | `LPSettingsSubsystem` : onglets Audio, Video, Lewd, General, Keyboard, Misc au-dessus de `SettingsScrollBox`. Les lignes du Blueprint restent en place et sont montrées selon leur texte ; le reste va dans Misc. Keyboard affiche la liste des touches | la liste unique ne montrait pas le clavier | retirer `LPSettingsSubsystem.h/.cpp` | +| CHG-244 | 2026-10-08 | PROP / ERR-043 | `Docs/Migration/etat/PROP/PROP-BUG.md`, `PROP-CART.md` ; dumps `etat/PROP/PROP_LPDumpFunction_20261008-*.txt` ; `Source/LifePlay/LPPersonPropGuardSubsystem.h/.cpp` (nouveaux) | « seul le verre reste apparent » : `destroyDummy` sans `clearProps` + mesh vide. Garde runtime : si Person caché ou `Mesh` sans SkeletalMesh → `clearProps` + purge tableau `Props` + enfants SMC `NODE_AddStaticMeshComponent*` seulement (pas les vetements `*SM`) ; si visible → retire props orphelines (parent ≠ Mesh ou socket None/invalide). Fiche bug complète (symptôme, repro, cause, non-causes, acceptation). Compile `LifePlayEditor` PASS ; **PIE à valider** | — → créés | supprimer `etat/PROP/` et les deux sources | | CHG-243 | 2026-10-08 | ERR-008 | `Source/LifePlay/LPMapMarkersGuardSubsystem.h/.cpp` (nouveaux) | garde runtime : si `MapMarkers.WidgetComponents` est vide, clear `Vector2Ds`, reconstitue depuis les `UWidgetComponent` existants ou ajoute un slot de focus invisible — évite le spam `Array_Get` / `Set Input Mode Game And UI` (FocusOnMapMarkers sans Length). Corrélé à l'approche PNJ (`updateAllPersonButtons` → `SpawnMapMarkers`). Live Coding PASS (nouvelle classe) | — → créés | supprimer ces deux fichiers | | CHG-242 | 2026-10-08 | VQ-fix | `Source/LifePlay/LPSettingsSubsystem.cpp`, `LPSettingsSubsystem.h` | contraste UI Settings : `HardenSettingsControls` force un style Slate sombre/lisible sur toutes les cases et SpinBox (valeurs pâles sur fond flou) ; ticker Settings à 0 pour que `ApplyRows` gagne face au BP. Compile `LifePlayEditor` PASS | — → modifiés | retirer `HardenSettingsControls` / remettre ticker 0.5 | | CHG-241 | 2026-10-08 | VQ-fix | `Source/LifePlay/LPSettingsSubsystem.cpp`, `Source/LifePlay/LPVideoQualitySubsystem.cpp` | correctifs post-VQ : (1) `CategoryOf` ignore les infobulles (faux positifs `screen`/`quality` → Sex Description, Map Marker, NewAnimationsOnly hors Video) + classements par nom UMG (`ShowFPS`, etc.) ; (2) `MainMenu.GI` rattaché sur l'écran titre (spinbox DoF / cases) ; (3) `RepairShowFPS` (style + enabled) ; (4) `ApplyRows` à chaque tick ouvert ; (5) `ApplyToScalability` n'écrit plus que les 4 `sg.*` en `SetByConsole` (fin du spam `SetByScalability ignored` sur ScreenPercentage/PoolSize/SSS). Live Coding PASS | — → modifiés | revenir sur les deux `.cpp` | diff --git a/Docs/Migration/Registre_Erreurs.md b/Docs/Migration/Registre_Erreurs.md index 3271358..a74fb54 100644 --- a/Docs/Migration/Registre_Erreurs.md +++ b/Docs/Migration/Registre_Erreurs.md @@ -47,3 +47,4 @@ | ERR-040 | À la création du personnage (nouvelle partie), la femme proposée par défaut est chauve | retour utilisateur 2026-10-03 (test UI15) | à chaque nouvelle partie | faible (visuel) | — | **préexistant en V1.4** (confirmé par l'utilisateur) ; 4.27 et V1.3 non vérifiés. Piste non vérifiée : première entrée de `GI.HairstyleTable` = « NONE » pour une femme. **Ouvert** | | ERR-041 | Module absent de `GI.ModEnableds` tenu pour actif par `LPHairstyleSubsystem` (coiffures d'un mod décoché présentes) | retour utilisateur 2026-10-03 (test H3a) | toujours | moyenne | H3a | défaut introduit en H1, présent en V1.4 ; **corrigé (CHG-102)**, validé en jeu le 2026-10-03 | | ERR-042 | LifePlay Revival et le LifePlay d'origine partagent le dossier de sauvegardes `%LOCALAPPDATA%\LifePlay\Saved\SaveGames` (emplacements de partie et `GameSettings.sav` communs) | question d'un joueur sur Discord, 2026-10-03 (V1.5) | permanent | moyenne (risque d'écraser une partie du jeu d'origine ; choix des mods réécrit) | SAV1 | **ouvert, à faire** (lot SAV1, `04_Cahier_Des_Charges.md` §6) ; consigne aux joueurs d'ici là : copier le dossier `SaveGames` avant le premier lancement | +| ERR-043 | Prop d'animation (verre `drinkglass_prop`, etc.) reste visible alors que le corps du PNJ a disparu (« seul le verre reste apparent ») ; parfois verre flottant devant un PNJ encore visible | retour utilisateur 2026-10-08 (captures pub) ; PIE `LifePlay.log` ; fiche `etat/PROP/PROP-BUG.md` | 21 083 `No SkeletalMesh` CharacterMesh0 au `DestroyDummies` / session | moyenne (visuel) | PROP | **corrigé code (CHG-244)** : `destroyDummy` sans `clearProps` → `LPPersonPropGuardSubsystem` ; compile PASS ; **à valider en PIE** — détail cause / non-causes / acceptation : `Docs/Migration/etat/PROP/PROP-BUG.md` | diff --git a/Docs/Migration/etat/PROP/PROP-BUG.md b/Docs/Migration/etat/PROP/PROP-BUG.md new file mode 100644 index 0000000..78d279c --- /dev/null +++ b/Docs/Migration/etat/PROP/PROP-BUG.md @@ -0,0 +1,119 @@ +# ERR-043 — Seul le verre reste apparent + +**ID registre :** ERR-043 +**Lot :** PROP +**Date signalement :** 2026-10-08 +**Gravité :** moyenne (visuel) +**Statut :** corrigé en code (CHG-244) — **à valider en PIE** +**Fiches liées :** [PROP-CART](PROP-CART.md) (dumps / graphes), CHG-243 (ERR-008 MapMarkers, corrélé), ERR-011 (bruit `No SkeletalMesh`) + +--- + +## 1. Symptôme + +Dans un lieu intérieur (ex. pub `pub___vin01`), un PNJ en animation quotidienne avec accessoire (`drinkglass`, parfois suivi d’une autre pose) laisse **le verre (prop StaticMesh) visible** alors que : + +1. le **corps** du personnage a disparu (mesh skeletal vide / dummy recyclé), ou +2. le personnage est encore là mais un **verre flotte** à hauteur de torse / tête, détaché de la main (souvent pendant qu’il tient déjà un autre prop, ex. carnet `write`). + +Phrase utilisateur : *« seul le verre reste apparent »*. + +| Capture | Contenu | +|---|---| +| Verre orphelin + barmaid | Verre à pied opaque blanc flottant devant le buste ; PNJ visible avec bloc-notes | +| Cliente qui boit | État nominal : corps + verre à la bouche (référence du comportement attendu) | + +Chemins Cursor (session) : +`…/assets/…image-d4b12d9d-651a-4448-b327-8c6923b9a1e0.png`, +`…/assets/…image-c2441b41-47f4-4c3a-9cad-0120376c2833.png`. + +--- + +## 2. Reproduction + +1. Lancer PIE (carte `CentralLondon`), entrer dans un **pub**. +2. Laisser les PNJ play daily (`drinkglass` / occupations meubles). +3. S’approcher d’un PNJ **ou** changer de lieu (déclenche `DestroyDummies`). +4. Observer : spam log `GetSocketInfoByName(…): No SkeletalMesh for Component(CharacterMesh0)` sur `Person_C_*`, messages `test0204 DestroyDummies` / `test0304 DESTROY…`, et éventuellement verre flottant. + +**Preuve session :** `Saved/Logs/LifePlay.log` (PIE 2026-10-07→08) — premier warning mesh vide ~l.5331 (`22.39.21`), **~21 083** occurrences dans la session, acteurs `Person_C_0`…`Person_C_5`. + +--- + +## 3. Cause racine + +### 3.1 Cycle de vie des props + +| Étape | Blueprint | Comportement | +|---|---|---| +| Apparition | `Person::spawnProp` | `AddComponent` (`NODE_AddStaticMeshComponent-0`) → `AttachToComponent(Mesh, Socket)` → `SetStaticMesh` → ajout au tableau **`Props`** | +| Nettoyage prévu | `Person::clearProps` | détruit les SMC de `Props` + enfants SMC de `Mesh`, vide le tableau | +| Recycle dummy | `GI::destroyDummy` | `SetActorHiddenInGame(true)` → `ResetVars` → transform origine → `unOccupyFnt` — **n’appelle pas `clearProps`** | + +Le verre n’est **pas** un acteur séparé : c’est un composant du `Person`. Quand `CharacterMesh0` n’a plus de `SkeletalMesh`, le corps disparaît ; les SMC de props peuvent rester le seul rendu. + +### 3.2 Chaîne observée + +``` +DestroyDummies + → destroyDummy(Person) // sans clearProps + → CharacterMesh0.SkeletalMesh = None + → warnings GetSocketInfoByName(head|pelvis|…) + → verre (Props / SMC attaché) encore visible +``` + +Dumps : `etat/PROP/PROP_LPDumpFunction_20261008-010449` (`spawnProp`), `-010554` (`clearProps`), `-010647` (`destroyDummy`). Détail dans PROP-CART §3. + +### 3.3 Variante « PNJ encore visible + verre flottant » + +Après un glitch mesh / changement d’anim (`drinkglass` → `write`) sans clear : le prop peut rester parenté au `Mesh` avec **socket None ou invalide** → position relative à l’origine du mesh (torse), d’où l’aspect « flottant ». + +--- + +## 4. Non-causes (à ne pas re-investiguer pour ce symptôme) + +| Piste | Verdict | +|---|---| +| ERR-008 `MapMarkers.WidgetComponents[0]` | **Corrélée** à l’approche (`updateAllPersonButtons` → `SpawnMapMarkers`), **pas** la cause du verre. Atténuée CHG-243. | +| `Person::updateButtonSize` | Masque seulement **`ButtonSM`** (bouton de nom), pas le corps ni les props. | +| Réglages VQ / `LPSettingsSubsystem` | Aucune lecture de `Props` / `spawnProp` / mesh Person. | +| ERR-011 `No SkeletalMesh` « normal » | Bruit accepté en 4.27/5.5 ; ici le **pic** au DESTROY + props non clear = bug distinct. | +| Verres blancs opaques sur les étagères | Matériau / translucency — hors ERR-043. | + +--- + +## 5. Correctif + +| Élément | Détail | +|---|---| +| CHG | **CHG-244** | +| Code | `Source/LifePlay/LPPersonPropGuardSubsystem.h/.cpp` | +| Mécanisme | Sous-système d’instance de jeu, tick ~0,2 s | +| Si Person caché **ou** Mesh sans asset skeletal | `ProcessEvent(clearProps)` + purge C++ du tableau `Props` + destruction des enfants SMC nommés `NODE_AddStaticMeshComponent*` (pas les vêtements `*SM`) | +| Si Person visible | Retrait des entrées `Props` orphelines (mauvais parent, socket None / inexistant) | +| BP modifiés | Aucun | +| Compile | `LifePlayEditor` Win64 Development **PASS** (2026-10-08) | + +Alternative BP (non retenue pour ce lot) : appeler `clearProps` au début de `GI::destroyDummy` (et idéalement avant chaque nouveau `spawnProp` d’anim). + +--- + +## 6. Critères d’acceptation + +- [ ] Relancer l’éditeur avec la DLL CHG-244. +- [ ] Pub : PNJ qui boivent / changent d’occupation ; changer de lieu (DestroyDummies). +- [ ] Aucun verre flottant orphelin après DESTROY / disparition temporaire du corps. +- [ ] Log : au moins une ligne `PersonPropGuard : clearProps` ou `props orphelines` lors d’un recycle. +- [ ] Pas de régression vêtements (les SMC `*SM` ne doivent pas être détruits à tort). + +--- + +## 7. Assets / références + +| Objet | Chemin | +|---|---| +| Prop verre | `Content/Heavy/New3/Anims/Daily/Props/drinkglass_prop.uasset` | +| Anims | `…/Anims/Daily/drinkglass_F.uasset`, `drinkglass_M.uasset` | +| BP | `Content/BPs/Person.uasset` (`spawnProp`, `clearProps`), `Content/BPs/GI.uasset` (`destroyDummy`) | +| Registre | `Docs/Migration/Registre_Erreurs.md` → ERR-043 | +| Journal | `Docs/Migration/Journal_Modifications.md` → CHG-244 | diff --git a/Docs/Migration/etat/PROP/PROP-CART.md b/Docs/Migration/etat/PROP/PROP-CART.md new file mode 100644 index 0000000..e3014c7 --- /dev/null +++ b/Docs/Migration/etat/PROP/PROP-CART.md @@ -0,0 +1,158 @@ +# PROP-CART — Diagnostic technique (dumps / graphes) + +**Fiche :** PROP-T1 +**Date :** 2026-10-08 +**Workspace :** `d:\Game\LifePlay-master 5.5` +**Statut :** Correctif C++ compilé (`LPPersonPropGuardSubsystem`) — **PIE à valider** +**Fiche bug (symptôme, repro, cause, acceptation) :** [PROP-BUG.md](PROP-BUG.md) — **ERR-043** + +Complément dumps pour le bug « seul le verre reste apparent ». + +--- + +## 1. Contexte PIE (preuves journal) + +Source : `Saved/Logs/LifePlay.log` (session PIE 2026-10-07 → 08). + +| Observation | Détail | +|---|---| +| Premier `No SkeletalMesh` CharacterMesh0 | Ligne ~5331, `22.39.21` — **21 083** warnings dans la session | +| Déclencheur immédiat | `GI` : `test0204 DestroyDummies` puis `test0304 DESTROY…` (Jared / Jack / Stephanie / Isaac) | +| Acteurs concernés | `Person_C_0` … `Person_C_5` (pool dummies) | +| Corrélation approche | Juste avant : `customer_approaches.lpscene`, `pub___vin01`, spam ERR-008 `WidgetComponents` length 0 (atténué CHG-243) | +| ERR-011 | `GetSocketInfoByName … No SkeletalMesh` déjà **accepté** comme bruit (existe en 4.27) — ici le volume explose au `DestroyDummies` | + +Chaîne typique : + +``` +DestroyDummies → destroyDummy(Person) + → SetActorHiddenInGame(true) + → ResetVars() ← appelé ; graphe Person introuvable au dump (voir §3) + → transform origine + → unOccupyFnt + → (PAS de clearProps) +→ CharacterMesh0 sans SkeletalMesh → sockets head/pelvis/… en échec +→ StaticMesh props (verre) encore présents / visibles +``` + +--- + +## 2. Inventaire des dumps `LPDumpFunction` (lot PROP) + +Commande type : + +```powershell +.\Tools\RunCommandlet.ps1 -Lot PROP -Commandlet LPDumpFunction ` + -Args '-BP=Person -Fn=spawnProp -Pins' -ProjectRoot 'd:\Game\LifePlay-master 5.5' +``` + +Moteur : `D:\Game\Launcher\Engine\UE_5.5\Engine`. +Journaux bruts : `Saved/Logs/portage/PROP_LPDumpFunction_.log`. +Résumés : `Docs/Migration/etat/PROP/PROP_LPDumpFunction_.txt`. + +| Horodatage | Cible | Exit | Résultat | +|---|---|---:|---| +| `20261008-010449` | `Person` / `spawnProp` | 0 | 9 nœuds — voir §3.1 | +| `20261008-010554` | `Person` / `clearProps` | 0 | 17 nœuds — voir §3.2 | +| `20261008-010647` | `GI` / `destroyDummy` | 0 | 39 nœuds — voir §3.3 | +| `20261008-010803` | `Person` / `ResetVars` | **1** | Fonction **introuvable** dans `Person` (appelée pourtant par `destroyDummy`) | +| `20261008-010919` | `Person` (liste) | 0 | **134** graphes — `spawnProp`, `clearProps`, `ClearSMCs` présents ; **aucun** `ResetVars` / `PlayDaily` | +| `20261008-011027` | `Person` / `ClearSMCs` | 0 | 9 nœuds — voir §3.4 | +| `20261008-011115` | `Person` / `PlayDaily` | **1** | Fonction introuvable (appel GI EventGraph vers `Person_C::PlayDaily` ailleurs) | + +Dump antérieur utile (hors lot PROP) : + +| Fichier | Usage | +|---|---| +| `Docs/Migration/etat/DOC-MOD_LPDumpFunction_20261007-083554.txt` | `GI` EventGraph : appels `destroyDummy`, un appel `Person_C::clearProps` (ligne ~26340, autre chemin — pas dans `destroyDummy`) | + +--- + +## 3. Synthèse des graphes + +### 3.1 `Person::spawnProp` (9 nœuds) + +1. `AddComponent` template `NODE_AddStaticMeshComponent-0` +2. `K2_AttachToComponent(Parent = Person.Mesh, SocketName)` — KeepRelative +3. `SetStaticMesh(SM)` +4. `Array_Add` → variable **`Props`** +5. `SetCollisionEnabled(NoCollision)` + +Le verre (`drinkglass_prop`, etc.) est donc un **composant** du Person, attaché au skeletal mesh, référencé dans `Props` — pas un acteur séparé. + +### 3.2 `Person::clearProps` (17 nœuds) + +- Boucle sur `Props` : `SetVisibility(false)` + `K2_DestroyComponent` + `SetStaticMesh(None)` +- `Array_Clear(Props)` +- Puis `Mesh->GetChildrenComponents` → cast `StaticMeshComponent` → même nettoyage + +**Cette fonction nettoie correctement les verres.** Elle n’est **pas** appelée par `destroyDummy`. + +### 3.3 `GI::destroyDummy` (39 nœuds) — trou critique + +Ordre utile : + +| Étape | Appel | Effet | +|---|---|---| +| 1 | `Array_RemoveItem(DummyPersonsInUse)` | retire du pool « en service » | +| 2 | garde `isPlayer` | — | +| 3 | **`SetActorHiddenInGame(true)`** | cache l’acteur | +| 4 | **`Person::ResetVars()`** | dump Person : **fonction absente de la liste** | +| 5 | `SetActorTransform(0,0,0)` | téléporte à l’origine | +| 6 | `removeAlreadyGot` / `PersonsUsedThisScene` | cleanup mappings | +| 7 | `unOccupyFnt` | libère le meuble | +| 8 | `PrintString("test0304 DESTROY…")` | log observé en PIE | +| — | **`clearProps`** | **ABSENT** | + +Conséquence : quand le skeletal mesh est vidé (ResetVars / recyclage) ou l’acteur caché de façon incomplète côté composants dynamiques, les SMC de `Props` (verre) restent le seul rendu apparent. + +### 3.4 `Person::ClearSMCs` (9 nœuds) + +`GetChildrenComponents(Mesh, bIncludeAllDescendants=true)` → détruit les enfants dont le **nom se termine par `SM`**. +Complément éventuel à `clearProps`, **pas** branché dans `destroyDummy` non plus. + +### 3.5 Liste `Person` (134 graphes) — extraits props / pose + +Présents : `spawnProp`, `clearProps`, `ClearSMCs`, `UpdateDailyPose`, `PlayIdle`, `HideSlot`, `unOccupyFnt`, … +Absents au dump : `ResetVars`, `PlayDaily` (alors que GI les référence) — piste dette de nom / parent / graphe non listé ; à ne pas confondre avec un échec de compilation (le jeu tourne et imprime `DESTROY`). + +--- + +## 4. Hypothèses retenues + +| # | Hypothèse | Statut | +|---|---|---| +| H1 | `destroyDummy` sans `clearProps` → verre orphelin au recycle dummy | **retenue** (dump + log) | +| H2 | Mesh `CharacterMesh0` nul → corps invisible, prop SMC encore là | **retenue** (21k warnings au DESTROY) | +| H3 | ERR-008 MapMarkers cause la disparition du corps | **écartée** comme cause directe (corrélée à l’approche ; garde CHG-243). Confirmation [Investigate MapMarkers approach](8d7f20d3-0709-4655-bea7-b82196a94500) : `FocusOnMapMarkers` lit `WidgetComponents[0]` sans Length ; `HideAllMarkers` ne clear pas `Vector2Ds` (déjà compensé par la garde). Même chemin approche → `updateAllPersonButtons` → `SpawnMapMarkers`, mais `Person.updateButtonSize` ne masque que **`ButtonSM`** (bouton nom), pas le skeletal mesh ni les props `spawnProp`. | +| H4 | Verre flottant PNJ **encore visible** (barmaid + carnet) = prop d’anim précédente (`drinkglass`) non clear avant `write` / socket perdu après glitch mesh | **plausible** — garde orphelines (socket invalide / parent ≠ Mesh) | +| H5 | Matériaux verre opaques blancs (étagères) | **hors périmètre** PROP (qualité / translucency) | +| H6 | « Disparition » perçue à l’approche = seul `ButtonSM` qui se cache | **partielle** — explique un clignotement UI, pas le verre orphelin ni `No SkeletalMesh` au `DestroyDummies` | + +--- + +## 5. Correctif (CHG-244) + +`Source/LifePlay/LPPersonPropGuardSubsystem.h/.cpp` — compile `LifePlayEditor` Win64 Development **PASS** (2026-10-08) : + +- Tick ~0,2 s, acteurs `Person_C` du GI courant +- Si `IsHidden()` **ou** `Mesh` sans `SkeletalMeshAsset` → `ProcessEvent(clearProps)` + purge C++ du tableau `Props` + enfants SMC dont le nom contient `NODE_AddStaticMeshComponent` uniquement (évite de casser les SMC vêtements `*SM`) +- Sinon : retire les props orphelines (parent ≠ Mesh, ou socket None / inexistant — cas verre collé à l’origine du mesh) + +Aucun Blueprint modifié. + +**Validation PIE :** relancer l’éditeur (nouvelle DLL), entrer dans un pub, forcer un recycle dummies / changer de lieu ; chercher `PersonPropGuard :` dans le log ; plus de verre flottant après DESTROY. + +--- + +## 6. Index des artefacts + +| Artefact | Chemin | +|---|---| +| Fiche bug ERR-043 | `Docs/Migration/etat/PROP/PROP-BUG.md` | +| Résumés dumps | `Docs/Migration/etat/PROP/PROP_LPDumpFunction_*.txt` | +| Logs commandlets | `Saved/Logs/portage/PROP_LPDumpFunction_*.log` | +| PIE session | `Saved/Logs/LifePlay.log` | +| Captures utilisateur | workspace Cursor `assets/…image-d4b12d9d…png`, `…image-c2441b41…png` | +| Garde MapMarkers | CHG-243 / `LPMapMarkersGuardSubsystem` | +| Assets props | `Content/Heavy/New3/Anims/Daily/Props/drinkglass_prop.uasset`, anims `drinkglass_F/M` | diff --git a/Source/LifePlay/LPPersonPropGuardSubsystem.cpp b/Source/LifePlay/LPPersonPropGuardSubsystem.cpp new file mode 100644 index 0000000..8dd7f75 --- /dev/null +++ b/Source/LifePlay/LPPersonPropGuardSubsystem.cpp @@ -0,0 +1,223 @@ +// Voir LPPersonPropGuardSubsystem.h. + +#include "LPPersonPropGuardSubsystem.h" + +#include "Components/SkeletalMeshComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "EngineUtils.h" +#include "LifePlay.h" +#include "UObject/UnrealType.h" + +namespace LPPersonPropGuard +{ + /** Props d'anim (spawnProp) : template NODE_AddStaticMeshComponent-0 — pas les SMC vetements *SM. */ + bool IsAnimPropComponent(const UActorComponent* Comp) + { + if (!Comp) + { + return false; + } + const FString Name = Comp->GetName(); + return Name.Contains(TEXT("NODE_AddStaticMeshComponent"), ESearchCase::CaseSensitive); + } + + void DestroyProp(UStaticMeshComponent* SMC) + { + if (!IsValid(SMC)) + { + return; + } + SMC->SetVisibility(false, false); + SMC->SetHiddenInGame(true, false); + SMC->SetStaticMesh(nullptr); + SMC->DestroyComponent(); + } + + FArrayProperty* PropsArray(AActor* Person) + { + return Person ? CastField(Person->GetClass()->FindPropertyByName(TEXT("Props"))) : nullptr; + } + + void ClearPropsArrayEntries(AActor* Person) + { + FArrayProperty* Arr = PropsArray(Person); + if (!Arr || !CastField(Arr->Inner)) + { + return; + } + FScriptArrayHelper Helper(Arr, Arr->ContainerPtrToValuePtr(Person)); + for (int32 Index = Helper.Num() - 1; Index >= 0; --Index) + { + UObject* Entry = *reinterpret_cast(Helper.GetRawPtr(Index)); + if (UStaticMeshComponent* SMC = Cast(Entry)) + { + DestroyProp(SMC); + } + Helper.RemoveValues(Index); + } + } +} + +void ULPPersonPropGuardSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + TickHandle = FTSTicker::GetCoreTicker().AddTicker( + FTickerDelegate::CreateUObject(this, &ULPPersonPropGuardSubsystem::Tick), 0.2f); +} + +void ULPPersonPropGuardSubsystem::Deinitialize() +{ + FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); + Super::Deinitialize(); +} + +bool ULPPersonPropGuardSubsystem::Tick(float DeltaTime) +{ + (void)DeltaTime; + UGameInstance* GI = GetGameInstance(); + UWorld* World = GI ? GI->GetWorld() : nullptr; + if (!World || !World->IsGameWorld()) + { + return true; + } + + for (TActorIterator It(World); It; ++It) + { + AActor* Candidate = *It; + if (!IsValid(Candidate) || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) + { + continue; + } + if (Candidate->GetClass()->GetName() != TEXT("Person_C")) + { + continue; + } + if (Candidate->GetGameInstance() != GI) + { + continue; + } + GuardPerson(Candidate); + } + return true; +} + +USkeletalMeshComponent* ULPPersonPropGuardSubsystem::GetPersonMesh(AActor* Person) +{ + if (!Person) + { + return nullptr; + } + if (const FObjectPropertyBase* Prop = CastField(Person->GetClass()->FindPropertyByName(TEXT("Mesh")))) + { + if (USkeletalMeshComponent* FromProp = Cast(Prop->GetObjectPropertyValue_InContainer(Person))) + { + return FromProp; + } + } + TInlineComponentArray Meshes(Person); + return Meshes.Num() > 0 ? Meshes[0] : nullptr; +} + +void ULPPersonPropGuardSubsystem::CallClearProps(AActor* Person) +{ + if (!Person) + { + return; + } + if (UFunction* Fn = Person->FindFunction(TEXT("clearProps"))) + { + Person->ProcessEvent(Fn, nullptr); + } +} + +void ULPPersonPropGuardSubsystem::ForceHidePropComponents(USkeletalMeshComponent* Mesh) +{ + if (!IsValid(Mesh)) + { + return; + } + TArray Children; + Mesh->GetChildrenComponents(true, Children); + for (USceneComponent* Child : Children) + { + UStaticMeshComponent* SMC = Cast(Child); + if (!SMC || !LPPersonPropGuard::IsAnimPropComponent(SMC)) + { + continue; + } + LPPersonPropGuard::DestroyProp(SMC); + } +} + +void ULPPersonPropGuardSubsystem::ClearOrphanProps(AActor* Person, USkeletalMeshComponent* Mesh) // instance : log bLoggedOnce +{ + FArrayProperty* Arr = LPPersonPropGuard::PropsArray(Person); + if (!Arr || !CastField(Arr->Inner)) + { + return; + } + FScriptArrayHelper Helper(Arr, Arr->ContainerPtrToValuePtr(Person)); + bool bRemoved = false; + for (int32 Index = Helper.Num() - 1; Index >= 0; --Index) + { + UObject* Entry = *reinterpret_cast(Helper.GetRawPtr(Index)); + UStaticMeshComponent* SMC = Cast(Entry); + if (!IsValid(SMC)) + { + Helper.RemoveValues(Index); + bRemoved = true; + continue; + } + + // spawnProp attache toujours avec un SocketName ; socket None ou invalide = verre colle a l'origine du mesh. + const FName Socket = SMC->GetAttachSocketName(); + const bool bBadParent = SMC->GetAttachParent() != Mesh; + const bool bBadSocket = Socket.IsNone() || (Mesh && !Mesh->DoesSocketExist(Socket)); + if (!bBadParent && !bBadSocket) + { + continue; + } + + LPPersonPropGuard::DestroyProp(SMC); + Helper.RemoveValues(Index); + bRemoved = true; + } + + if (bRemoved && !bLoggedOnce) + { + bLoggedOnce = true; + UE_LOG(LogLifePlay, Display, TEXT("PersonPropGuard : props orphelines retirees (verre/animation sans socket)")); + } +} + +void ULPPersonPropGuardSubsystem::GuardPerson(AActor* Person) +{ + USkeletalMeshComponent* Mesh = GetPersonMesh(Person); + const bool bBodyGone = !IsValid(Mesh) || Mesh->GetSkeletalMeshAsset() == nullptr; + const bool bHidden = Person->IsHidden(); + + if (bHidden || bBodyGone) + { + if (!ClearedWhileInvalid.Contains(Person)) + { + CallClearProps(Person); + // Ceinture : clearProps BP peut etre absent / Props desync ; ne touche pas aux SMC vetements. + LPPersonPropGuard::ClearPropsArrayEntries(Person); + ForceHidePropComponents(Mesh); + ClearedWhileInvalid.Add(Person); + if (!bLoggedOnce) + { + bLoggedOnce = true; + UE_LOG(LogLifePlay, Display, + TEXT("PersonPropGuard : clearProps (Person cache=%d meshVide=%d) — corrige verre orphelin"), + bHidden ? 1 : 0, bBodyGone ? 1 : 0); + } + } + return; + } + + ClearedWhileInvalid.Remove(Person); + ClearOrphanProps(Person, Mesh); +} diff --git a/Source/LifePlay/LPPersonPropGuardSubsystem.h b/Source/LifePlay/LPPersonPropGuardSubsystem.h new file mode 100644 index 0000000..0fe41d5 --- /dev/null +++ b/Source/LifePlay/LPPersonPropGuardSubsystem.h @@ -0,0 +1,38 @@ +// Garde props d'animation (verre, carnet, etc.) : GI::destroyDummy masque le Person et +// ResetVars / mesh vide sans appeler Person::clearProps. Les StaticMeshComponent ajoutes +// par spawnProp restent alors visibles ("seul le verre reste apparent"). +// Ce sous-systeme appelle clearProps (et force le masquage des enfants SMC) quand le corps +// est cache ou sans SkeletalMesh, et detruit les props orphelines (socket invalide). + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Ticker.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "LPPersonPropGuardSubsystem.generated.h" + +class AActor; +class USkeletalMeshComponent; +class UStaticMeshComponent; + +UCLASS() +class LIFEPLAY_API ULPPersonPropGuardSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + +private: + bool Tick(float DeltaTime); + void GuardPerson(AActor* Person); + void ClearOrphanProps(AActor* Person, USkeletalMeshComponent* Mesh); + static USkeletalMeshComponent* GetPersonMesh(AActor* Person); + static void CallClearProps(AActor* Person); + static void ForceHidePropComponents(USkeletalMeshComponent* Mesh); + + FTSTicker::FDelegateHandle TickHandle; + TSet> ClearedWhileInvalid; + bool bLoggedOnce = false; +};