PROP / ERR-043 : props d'animation retires avec le corps du PNJ (LPPersonPropGuardSubsystem) (CHG-244)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-08 01:40:50 +02:00
parent ebae9242a7
commit 7a0b3f3671
6 changed files with 540 additions and 0 deletions

View file

@ -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-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-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-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-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-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` | | 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` |

View file

@ -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-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-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-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` |

View file

@ -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 |

View file

@ -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_<stamp>.log`.
Résumés : `Docs/Migration/etat/PROP/PROP_LPDumpFunction_<stamp>.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` |

View file

@ -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<FArrayProperty>(Person->GetClass()->FindPropertyByName(TEXT("Props"))) : nullptr;
}
void ClearPropsArrayEntries(AActor* Person)
{
FArrayProperty* Arr = PropsArray(Person);
if (!Arr || !CastField<FObjectPropertyBase>(Arr->Inner))
{
return;
}
FScriptArrayHelper Helper(Arr, Arr->ContainerPtrToValuePtr<void>(Person));
for (int32 Index = Helper.Num() - 1; Index >= 0; --Index)
{
UObject* Entry = *reinterpret_cast<UObject**>(Helper.GetRawPtr(Index));
if (UStaticMeshComponent* SMC = Cast<UStaticMeshComponent>(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<AActor> 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<FObjectPropertyBase>(Person->GetClass()->FindPropertyByName(TEXT("Mesh"))))
{
if (USkeletalMeshComponent* FromProp = Cast<USkeletalMeshComponent>(Prop->GetObjectPropertyValue_InContainer(Person)))
{
return FromProp;
}
}
TInlineComponentArray<USkeletalMeshComponent*> 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<USceneComponent*> Children;
Mesh->GetChildrenComponents(true, Children);
for (USceneComponent* Child : Children)
{
UStaticMeshComponent* SMC = Cast<UStaticMeshComponent>(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<FObjectPropertyBase>(Arr->Inner))
{
return;
}
FScriptArrayHelper Helper(Arr, Arr->ContainerPtrToValuePtr<void>(Person));
bool bRemoved = false;
for (int32 Index = Helper.Num() - 1; Index >= 0; --Index)
{
UObject* Entry = *reinterpret_cast<UObject**>(Helper.GetRawPtr(Index));
UStaticMeshComponent* SMC = Cast<UStaticMeshComponent>(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);
}

View file

@ -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<TWeakObjectPtr<AActor>> ClearedWhileInvalid;
bool bLoggedOnce = false;
};