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 ` 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& /*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 : base , villes ` (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 : `. **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 : entrees, 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 : installees, telechargeables, 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>`) 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 : navigateur `. ### 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 : `. 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-.zip` de `\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://///releases/download//` ; - GitHub : `-Owner`, `-Repo`, `-Tag`, d'où `https://github.com///releases/download//` ; - Modele : `-Modele "https://…/{zip}"` ; - `-Miroir` (facultatif) : un second modèle `{zip}`, ajouté en 2e URL ; - `-JeuMax` (défaut `2026.12.99`) ; - `-Sortie` (**défaut** `\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\\Content\StreetMap\.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.