LifePlay-Revival/Docs/Migration/etat/REL/MAPDL1/MAPDL1-E1-FICHE.md

203 lines
14 KiB
Markdown
Raw Normal View History

Fiche MAPDL1-E1 — Niveau N3 — Modèle prévu sonnet (agent lp-impl ; escalade lp-impl-max) — Agent lp-impl — Skills sk-protocol, lp-city, lp-lot, ue-cpp-build
# MAPDL1-E1 : états des villes et menu (C1 à C5 de MAPDL1-SPEC-05 v3), en C++, sans Blueprint
Décision de l'utilisateur, citée par Big Boss : « Oui : SPEC-05 v3 approuvée (S5-1 à S5-5, D10 = téléchargement HTTP dans le jeu avec navigateur en recours, D11 = un zip par pack). Lance la réalisation avec les modèles réduits ; l'hébergeur et le domaine, je les choisis plus tard. »
**Hors moteur** :
- aucun build, aucun commandlet, aucun éditeur. La compilation est l'étape E2, sous jeton ;
- aucun commit ;
- rien sous `Mods\` ;
- aucun Blueprint modifié.
Le téléchargement HTTP (C6) n'est **pas** dans cette fiche : c'est E5.
Références à lire d'abord :
- `MAPDL1-SPEC-05.md` (v1, § 1-2), `MAPDL1-SPEC-05-v3.md` (§ 1), `MAPDL1-E0-INV.md` ;
- `MAPDL1-L1-CART-02.md` et `MAPDL1-L4-CART-01.md` (menu, lignes `City_C`, calcul « Installed ») ;
- modèle de code : `Source/LifePlay/LPModInfoSubsystem.h/.cpp`, `TickModManager`. Il trouve un `UScrollBox` par `TObjectIterator`, lit des variables de Blueprint par `FindPropertyByName` et modifie les lignes.
## 0. Avant toute écriture
1. Instantané, depuis pwsh, un chemin par appel :
- `.\Tools\Snapshot.ps1 -Lot MAPDL1-E1 -Path <chemin>` pour chacun des fichiers du § 1 ;
- les fichiers neufs sont notés absents.
2. Relever les MD5 (`Get-FileHash -Algorithm MD5`) des fichiers modifiés, et des fichiers de ville de STM1 non commités. Ces derniers ne doivent pas changer :
- `Source/LifePlay/LPCityWaterSubsystem.*`, `LPCityWaterMask.h`, `LPCityNamesSubsystem.*`, `LPCityHouses.h`, `LPCityLandmarks.h`, `LPAssetRegistryHelpers.*`.
3. `git status --short` sur les fichiers du § 1 : aucune modification en cours attendue, sinon arrêt et rapport.
## 1. Fichiers (et seulement ceux-là)
| Fichier | État | Contenu |
|---|---|---|
| `Plugins/ModdingExtra/Source/ModdingExtra/Public/ModdingExtraBPLibrary.h` | M | crochet du § 2.1 |
| `Plugins/ModdingExtra/Source/ModdingExtra/Private/ModdingExtraBPLibrary.cpp` | M | crochet du § 2.1 |
| `Source/LifePlay/LifePlay.Build.cs` | M | `PrivateDependencyModuleNames` : ajouter `"ModdingExtra"` et `"Json"`, avec un commentaire « Lot MAPDL1 (SPEC-05) » |
| `Source/LifePlay/LPCityMapsSubsystem.h` et `.cpp` | A | § 2.2 à 2.5 |
| `Content/Data/LPCityMaps.json` | A | § 2.6 |
| `Config/DefaultGame.ini` | M | **une ligne** après la l. 74 (`+DirectoriesToAlwaysStageAsNonUFS=(Path="Modules")`) : `+DirectoriesToAlwaysStageAsUFS=(Path="Data")`, pour livrer le JSON dans le pak |
| `Tools/IndexCartes.ps1` | A | § 2.7 |
Conventions :
- code dans le style des fichiers voisins (commentaires en français sans accents, `// Lot MAPDL1 (MAPDL1-SPEC-05) :`) ;
- `Tools\*.ps1` en ASCII et LF ;
- pas de `python -c`, pas de heredoc ;
- **outil Edit** pour modifier (jamais `sed` ni `-replace`).
## 2. Contenu
### 2.1 Crochet dans ModdingExtra (C2)
Dans `ModdingExtraBPLibrary.h`, hors de la classe, ajouter :
```cpp
/** Lot MAPDL1 (MAPDL1-SPEC-05) : filtre facultatif de la liste rendue par GetAllFilesFromFolder, pose par le module du jeu
* (ULPCityMapsSubsystem). Arguments : dossier demande et extension tels que recus, liste a modifier. Sans filtre : inchange. */
using FModdingExtraFileListFilter = TFunction<void(const FString& /*RootFolder*/, const FString& /*Ext*/, TArray<FString>& /*Files*/)>;
MODDINGEXTRA_API void ModdingExtraSetFileListFilter(FModdingExtraFileListFilter Filter);
```
Dans le `.cpp` :
- une variable statique de fichier `FModdingExtraFileListFilter GLPFileListFilter;` et la définition de `ModdingExtraSetFileListFilter` ;
- dans `GetAllFilesFromFolder`, **garder une copie de `Ext` tel que reçu au début** (la fonction le transforme en `*.uasset`) ;
- juste avant `return true;` (l. 224 actuelle) : `if (GLPFileListFilter) { GLPFileListFilter(RootFolderFullPath, ExtRecu, Files); }`.
Rien d'autre ne change. Le plugin ne dépend toujours pas du jeu.
### 2.2 `ULPCityMapsSubsystem` : index et états (C1)
`UCLASS() class LIFEPLAY_API ULPCityMapsSubsystem : public UGameInstanceSubsystem`
**Index.** Dans `Initialize`, lire le **premier** fichier présent parmi :
- (a) `FPaths::ProjectSavedDir()/LPCityMaps.json` : surcharge locale, pour les essais (`MAPDL1-ESSAI-FJ` F4) ;
- (b) `FPaths::ProjectContentDir()/Data/LPCityMaps.json` : copie livrée.
Lecture par `FFileHelper::LoadFileToString` puis `FJsonSerializer`.
- Champs (format 2, SPEC v3 § 1) : `base[]`, `licences{}`, `villes[]` avec `ville`, `pack`, `version`, `jeu_min`, `jeu_max`, `zip`, `octets`, `sha256`, `urls[]`, `page`, `licences[]`, `source`.
- Journal : `[LPCity] CARTES index <chemin> : base <n>, villes <n>` (catégorie `LogLPCity`, celle de `LPAssetRegistryHelpers.cpp`).
- Index absent ou illisible : avertissement, et aucune ville téléchargeable. Les villes « base » prennent alors pour valeur par défaut `CentralLondon` et `00BathCC`.
**État** d'une ville `V`, nom d'asset du jeu, calculé à la demande et mis en cache. Les packs sont montés au démarrage, et le cache de `ResolveCityPath` n'est jamais invalidé, ce qui est voulu :
- `Installed` si `V` est dans `base`, **ou** si, pour les deux chemins `/Game/Heavy/StreetMap/V.V` et `/Game/Heavy/StreetMap/Static/V.V`, `ULPAssetRegistryHelpers::ResolveCityPath(p) != p` ;
- sinon `Downloadable` si une entrée `ville == V` (casse ignorée) a au moins une URL, un `sha256` de 64 caractères hexadécimaux, et `jeu_min <= version du jeu <= jeu_max`. La version du jeu est `ProjectVersion` (`FLPModGuard::GetGameVersion()`) ; comparaison par composantes séparées par des points, numérique quand les deux composantes sont des nombres, sinon textuelle ;
- sinon `Unavailable`.
API (`UENUM(BlueprintType) ELPCityMapState { Installed, Downloadable, Unavailable }`) :
- `GetCityState(const FString& Ville)` ;
- `GetDownloadBytes(Ville)` ;
- `GetDownloadUrls(Ville)` (C++).
Une ligne de journal par ville au premier calcul : `[LPCity] ETAT <V> : <etat>`.
**Pose du crochet** dans `Initialize` : `ModdingExtraSetFileListFilter(...)` avec un `TWeakObjectPtr` sur le sous-système. Le retirer dans `Deinitialize` (filtre vide). Le filtre n'agit que si :
- le dossier, une fois normalisé (`\` devient `/`, sans `/` au début ni à la fin), vaut `Heavy/StreetMap` (casse ignorée) ;
- **et** l'extension reçue est `uasset` ou `.uasset`.
Il retire alors de `Files` **toutes** les entrées dont le nom de fichier sans extension est une ville dont l'état n'est pas `Installed`. Les entrées des villes installées ne sont **pas** touchées : le comptage d'origine du Blueprint `UI/City`, qui exige 2 entrées pour « Installed », doit rester intact. Journal, une fois par appel : `[LPCity] LISTE Heavy/StreetMap : <n> entrees, <m> retirees`.
E0 a établi que `GetAllFilesFromFolder` est appelé par 6 Blueprints (`GI`, `Room`, `Builder`, `CharGen`, `StatMenu`, `CityMenu`), mais que seul `CityMenu` porte le littéral `Heavy/StreetMap`. Le filtre ne touche que ce dossier.
### 2.3 Menu des villes (C3)
Un ticker `FTSTicker` de 0 s, comme `TickModManager`, actif tant que le sous-système existe. À chaque passage :
1. Trouver un `UScrollBox` nommé `CityScrollBox`, dont l'objet extérieur est de classe `CityMenu_C` et qui est à l'écran (`IsVisible()` et `GetCachedWidget().IsValid()`).
2. Pour chaque enfant de classe `City_C`, lire :
- la variable `City` (`FStructProperty`), et dans cette structure le champ dont le nom commence par `StreetMapName` (`FNameProperty` ; les champs d'une structure Blueprint ont un suffixe). Le nom de ville est la partie après le dernier `.` du chemin, ou après le dernier `/` s'il n'y a pas de `.` ;
- les variables `InstalledText` (`UTextBlock`) et `CityButton` (`UButton`).
3. Selon l'état :
- `Installed` : **ne rien toucher** (texte et clic d'origine) ;
- `Downloadable` :
- `InstalledText->SetText("Download (%d MB)")`, arrondi au Mo supérieur ;
- `SetColorAndOpacity` bleu `(0.2, 0.6, 1.0)` ;
- relais de clic (point 5) ;
- `Unavailable` :
- `InstalledText->SetText("Not available")`, gris `(0.5, 0.5, 0.5)` ;
- `CityButton->SetIsEnabled(false)` ;
- opacité de la ligne à 0,6.
- Réappliquer **à chaque passage**, car le Blueprint écrit `InstalledText` après un `Delay 0.2` (`MAPDL1-L4-CART-01`).
4. **Tri**, une fois par instance de liste, et de nouveau si le nombre d'enfants change :
- tri **stable** en trois groupes : `Installed`, puis `Downloadable`, puis `Unavailable` ; l'ordre d'origine est gardé dans chaque groupe ;
- retrait de tous les enfants, puis réinsertion dans le nouvel ordre ;
- relever le `Padding`, l'alignement horizontal et l'alignement vertical de chaque `UScrollBoxSlot` avant le retrait, et les remettre après.
- **`GI.Cities` n'est pas modifié.**
- Journal, une fois par instance : `[LPCity] MENU tri : <i> installees, <d> telechargeables, <u> indisponibles`.
5. **Relais de clic** des lignes `Downloadable` :
- un petit `UCLASS() ULPCityRowClick : UObject` (même fichier) avec `FString Ville`, un `TWeakObjectPtr` vers le sous-système et `UFUNCTION() void OnClicked();`. Lié par `CityButton->OnClicked.AddUniqueDynamic` ;
- le sous-système garde ces objets (`UPROPERTY() TArray<TObjectPtr<ULPCityRowClick>>`) et les vide quand la liste change d'instance ;
- **dans cette fiche**, `OnClicked` ouvre `urls[0]` (ou `page` si elle n'est pas vide) par `FPlatformProcess::LaunchURL`. C'est le recours (c), en attendant la fenêtre de C6 en E5. Journal : `[LPCity] CLIC <V> : navigateur <url>`.
### 2.4 Bandeau des parties sauvegardées (C4)
Dans `Initialize`, s'abonner à `ULPAssetRegistryHelpers::OnCityMapResolved()` (`LPAssetRegistryHelpers.h:19`, deux paramètres : demandé, résolu). Si :
- le demandé est `/Game/Heavy/StreetMap/V.V` (pas `Static/`) ;
- le résolu est **égal** au demandé (aucun pack) ;
- l'état de `V` n'est pas `Installed`.
Alors afficher pendant 10 s, par `GEngine->GameViewport->AddViewportWidgetContent` (Slate, ordre Z élevé, en haut au centre), le texte :
- `Map not installed - download it from the city menu` si l'état est `Downloadable` ;
- `Map not available` sinon.
Une seule fois par ville et par session. Journal : `[LPCity] BANDEAU <V> : <texte>`. Se désabonner dans `Deinitialize`.
### 2.5 Ce qu'il ne faut pas faire
- Ne pas modifier `ResolveCityPath`, `LPCityWaterSubsystem`, `LPCityNamesSubsystem`, ni aucun fichier de STM1.
- Ne pas toucher `GI.Cities`, les sauvegardes, `Content/Heavy`.
- Aucun texte de jeu en dur hors de ceux du § 2.3 et du § 2.4.
### 2.6 `Content/Data/LPCityMaps.json` livré
Format 2 :
- `"base": ["CentralLondon", "00BathCC"]` ;
- `"licences"` :
- `osm` : `titre` « Map data (c) OpenStreetMap contributors, ODbL 1.0 », `fichier` « Licenses/OpenStreetMap_Geodata_License.txt » ;
- `lifeplay_ancienne` : `titre` = le texte validé (S5-5) de `MAPDL1-SPEC-05-v2.md` § 4, mot pour mot, `fichier` « Licenses/Vinfamy_License.txt » ;
- **`"villes": []`** : aucun hébergeur n'est choisi (H1, H2). Une liste d'essai se pose dans `Saved/LPCityMaps.json`.
UTF-8 sans BOM, LF.
### 2.7 `Tools/IndexCartes.ps1` (C5)
Il écrit un `LPCityMaps.json` format 2. Paramètres :
- `-Villes A,B` ou `-Toutes` : toutes les archives `lpm_city_*-*-for-LifePlay-<jeu>.zip` de `<Publication>\Mods`, en prenant la **plus haute version** par pack ;
- `-Hebergeur Forgejo|GitHub|Modele`, avec :
- Forgejo : `-ForgejoHote` (défaut `forgejo.edujiro5972.fr`), `-Owner`, `-Repo`, `-Tag`, d'où `https://<hote>/<owner>/<repo>/releases/download/<tag>/<zip>` ;
- GitHub : `-Owner`, `-Repo`, `-Tag`, d'où `https://github.com/<owner>/<repo>/releases/download/<tag>/<zip>` ;
- Modele : `-Modele "https://…/{zip}"` ;
- `-Miroir` (facultatif) : un second modèle `{zip}`, ajouté en 2e URL ;
- `-JeuMax` (défaut `2026.12.99`) ;
- `-Sortie` (**défaut** `<Racine>\Saved\LPCityMaps.json` ; écrire `Content\Data\LPCityMaps.json` exige `-Livre`) ;
- `-ABlanc` : affiche le JSON, n'écrit rien.
Pour chaque pack :
- `ville` : le nom d'asset, tiré de `ModSources\<pack>\Content\StreetMap\<V>.uasset` ;
- `version` : `VersionName` du `.uplugin` source ;
- `zip`, `octets`, `sha256` calculés sur l'archive ;
- `jeu_min` : la version du jeu tirée du nom de l'archive ;
- `licences` : `["osm","lifeplay_ancienne"]` ;
- `source` : la ligne `- file … SHA-256 …` du CREDITS source.
Racines lues par `Get-LPProjet` et `Get-LPRacine`. Codes : 0 écrit ; 2 usage, archive ou source manquante. ASCII et LF ; en-tête `.SYNOPSIS`.
Essai à faire (sans réseau), avec des valeurs **fictives** : `-Villes Manila -Hebergeur Forgejo -Owner exemple -Repo exemple -Tag cartes-test-1 -ABlanc`. Recopier la sortie.
## 3. Contrôles hors moteur à faire
1. `git diff --stat` des fichiers M, et liste des fichiers A.
2. MD5 avant et après de chaque fichier du § 1, et MD5 des fichiers de STM1 inchangés.
3. Fins de ligne : les fichiers `.cpp`, `.h` et `.cs` gardent leur convention, vérifiée par `git ls-files --eol` pour les fichiers suivis (LF attendu).
4. Analyse syntaxique PowerShell de `IndexCartes.ps1` (`[System.Management.Automation.Language.Parser]::ParseFile`) : 0 erreur ; essai à blanc du § 2.7.
5. `python -I -X utf8 -c` est **interdit** ; pour valider le JSON, lancer `Test-Json` sous pwsh.
## 4. Livrable
`Docs\Migration\etat\REL\MAPDL1\MAPDL1-E1-IMPL.md` :
- en-tête sk-protocol, avec le modèle réellement utilisé ;
- fichiers, MD5 avant et après, diff résumé ;
- choix faits là où la fiche laisse une latitude ;
- points non sûrs, à vérifier à la compilation (E2) ;
- sorties des contrôles du § 3 ;
- compte rendu.
**Budget : 60 tours.** En cas de blocage (API introuvable, compilation impossible à anticiper), arrêt et rapport : on ne contourne pas.