LifePlay-Revival/Docs/Migration/etat/REL/SAV/REL-SAV-SPEC.md
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

12 KiB

Fiche REL-SAV-T2 — Niveau N3 — Modèle prévu opus | utilisé claude-opus-5-5 (session REL, sans sous-agent) — Agent session REL — Skills lp-lot, lp-paths

REL-SAV-SPEC : garde B+ (relance avec -UserDir), choix « B+ seul », preuve attendue en jeu (2026-10-13)

Base : REL-SAV-DIAG.md (accepté par Big Boss le 2026-10-13). Décisions de l'utilisateur : U1-U4 (REL-SAV-TACHES.md), R0 (aucune reprise des anciennes sauvegardes), B+ avant l'alpha (échéance 2026-10-18). GO de Big Boss : (a) cette SPEC, (b) la preuve, (c) l'IMPL sans compiler, après acceptation de cette SPEC. Lecture seule jusqu'ici : rien n'est écrit dans Source\, Config\ ni Content\.

0. « B+ seul » ou « B + B+ » : recommandation B+ seul

Critère B+ seul B + B+
Points d'entrée un seul : LifePlay.exe (bootstrap), l'exe Binaries\Win64 lancé directement et tout raccourci passent par la garde deux : le lanceur et LifePlay.exe, qui font la même chose
Rôle du lanceur avec R0 — aucun : sans copie de sauvegardes, il ne ferait que passer -UserDir, ce que la garde fait déjà
Coût C++ seul (≈ 150 lignes, une fonction et un .ini lu par l'API Win32) le même, plus un lanceur, l'étape d'empaquetage et le README
Risque une seconde création de processus au démarrage (< 1 s, estimé) ; le parent reste en attente (quelques Mo, moteur non initialisé) le même, plus deux mécanismes à tenir cohérents

B n'apporterait rien de plus que B+ sans reprise. On le rouvrirait seulement si l'essai de B+ échouait. Aucun lanceur n'est donc livré.

1. Où accrocher la garde : un initialiseur statique, avant WinMain

Constat (établi) :

  • StartupModule du module LifePlay (Source\LifePlay\LifePlay.cpp:13-16) arrive trop tard : la pré-initialisation a déjà fixé ProjectSavedDir() (mis en cache : Paths.cpp:485-494), ouvert le journal et lu la configuration.
  • Aucun point d'accroche du moteur ne précède FCommandLine::Set. LaunchWindowsStartup relit ::GetCommandLineW() (E\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:198-200).

Choix : un objet statique global dans le module LifePlay. Son constructeur s'exécute pendant l'initialisation statique de l'exe, avant WinMain. Le moteur n'a alors rien écrit : ni journal, ni .ini, ni Saved.

  • En monolithique, le module est relié comme objet, pas comme bibliothèque : LifePlay-Win64-Shipping.exe.rsp cite LifePlay/Module.LifePlay.cpp.obj (établi). L'initialiseur ne peut donc pas être écarté par l'éditeur de liens. IMPLEMENT_PRIMARY_GAME_MODULE, dans le même module, repose déjà sur une inscription statique.
  • Fichier : Source\LifePlay\LPSaveRelaunch.cpp, nouveau. Pas d'en-tête public. La seule fonction exportée est LPSaveRelaunch::LogStartupState(), appelée par StartupModule (§ 6).

Contraintes du code d'initialisation statique (ordre d'initialisation non garanti entre unités de compilation) :

  • API Win32 et tampons wchar_t seulement. Pas de FString, FPaths, FParse, GConfig, UE_LOG ni FMemory : GMalloc n'est pas encore créé.
  • Aucune exception ; pas de std:: qui alloue.
  • Tampons fixes de 32768 caractères (taille maximale d'une ligne de commande Windows) : statiques, ou alloués par HeapAlloc(GetProcessHeap(), …).

2. Quand la garde agit (jamais dans l'éditeur)

À la compilation, le code n'est compilé que sous #if PLATFORM_WINDOWS && IS_MONOLITHIC && !WITH_EDITOR && UE_GAME (UE_GAME=1 pour une cible Game : TargetRules.cs:3479). L'éditeur, les commandlets et UnrealEditor.exe -game en sont exclus d'office.

À l'exécution, conditions dans l'ordre. Dès que l'une échoue, on sort sans rien faire et le démarrage reste inchangé :

  1. Le jeu tourne depuis un paquet : <dossier de l'exe>\..\..\Content\Paks\LifePlay-Windows.pak existe (GetFileAttributesW). Établi : présent dans le paquet Shipping_V2.0, absent de la copie de travail (Content\Paks inexistant). Une build autonome non cuite lancée depuis le projet n'est donc pas touchée.
  2. Pas de désactivation explicite : la ligne de commande ne contient pas -LPNoSaveRelaunch.
  3. Pas de boucle :
    • la ligne de commande ne contient pas -UserDir= (comparaison sans casse, précédée d'un espace ou d'un guillemet : cas de la relance et des Lancer_*_essai.bat) ;
    • la variable d'environnement LP_SAVE_RELAUNCHED est absente. Le parent la pose avant CreateProcessW, l'enfant en hérite. Deux verrous indépendants : il ne peut y avoir qu'une relance.
  4. Configuration :
    • Shipping (UE_BUILD_SHIPPING) : toujours actif.
    • Development empaqueté : actif seulement si LPSaveLocation.ini existe (adhésion volontaire). Ainsi les outils de débogage qui lisent <build>\Windows\LifePlay\Saved restent valides (CollectDebugArtifacts.ps1:18-20, Tools\debug\*), et l'essai en build commun Development n'a besoin que de poser le fichier.
    • DebugGame : comme Development.

3. LPSaveLocation.ini et le dossier cible

  • Emplacement : <dossier de l'exe>\..\..\LPSaveLocation.ini, c'est-à-dire <installation>\Windows\LifePlay\LPSaveLocation.ini, ou FPaths::ProjectDir() côté moteur (décision U2). Il n'est pas livré : s'il est absent, on prend le dossier par défaut.
  • Format, lu par GetPrivateProfileStringW et compatible avec FConfigFile pour le futur menu (U4) :
[SaveLocation]
SaveDir=
  • Calcul de la cible :
    1. SaveDir est lu, débarrassé de ses espaces et guillemets de bord, puis passé à ExpandEnvironmentStringsW (%LOCALAPPDATA% permis).
    2. Si le résultat est vide, ou n'est pas un chemin absolu (X:\… ou \\serveur\…), on prend le défaut : SHGetKnownFolderPath(FOLDERID_LocalAppData) + \LifePlay-Revival. C'est la même base que le moteur (WindowsPlatformProcess.cpp:1467 ; le cas « intégrité basse » est ignoré, comme dans la décision U3).
    3. On retire les \ ou / de fin : un \" final serait lu comme un guillemet échappé.
    4. On crée le dossier s'il manque (SHCreateDirectoryExW). En cas d'échec, pas de relance (OutputDebugStringW seulement, décision U3). Le jeu démarre alors comme aujourd'hui, et la ligne de § 6 le dit.
  • Le moteur range ensuite Saved sous la cible (Paths.cpp:453-457, puis :86-115) : par défaut %LOCALAPPDATA%\LifePlay-Revival\Saved\{SaveGames,Config,Logs,…}.

4. Relance et transmission des arguments d'origine

  1. Lire GetModuleFileNameW(nullptr). C'est l'exe réel Binaries\Win64\LifePlay-Win64-<Config>.exe, et non le bootstrap.
  2. Lire GetCommandLineW(), sauter argv[0] selon la règle Windows (s'il commence par ", jusqu'au " suivant, sinon jusqu'au premier blanc), puis les blancs. Le reste (queue brute) est recopié tel quel, sans redécoupage ni requote. Tous les arguments sont ainsi gardés, dans leur ordre et avec leurs guillemets : ..\..\..\LifePlay\LifePlay.uproject ajouté par le bootstrap, -ScriptStackOnWarnings, -LPPersonsLog, etc.
  3. Nouvelle ligne : "<exe>" <queue brute> -UserDir="<cible>".
  4. SetEnvironmentVariableW(L"LP_SAVE_RELAUNCHED", L"1"), puis CreateProcessW(exe, ligne, …, bInheritHandles = FALSE, 0, nullptr /*environnement hérité*/, nullptr /*dossier courant hérité*/, &si, &pi). si est initialisé avec GetStartupInfoW, pour transmettre wShowWindow.
  5. Le parent attend l'enfant (WaitForSingleObject(pi.hProcess, INFINITE)), lit son code de sortie (GetExitCodeProcess), puis se termine par TerminateProcess(GetCurrentProcess(), code). Raisons :
    • le bootstrap et les scripts qui attendent LifePlay.exe (Tools\debug\LifePlay_debug_ui.ps1) gardent le même comportement et le même code de sortie (estimé : le comportement du bootstrap n'est pas lisible, ses sources manquent dans l'installation Launcher) ;
    • TerminateProcess plutôt qu'ExitProcess évite d'appeler les destructeurs des objets statiques du moteur déjà construits.
  6. Si CreateProcessW échoue : on retire LP_SAVE_RELAUNCHED, on n'attend rien, et le démarrage normal continue (ancien comportement, signalé en § 6).

5. Ce qui reste hors de cette SPEC

  • La ligne du menu Misc (U4 : écrire LPSaveLocation.ini, bouton « Default », choix du dossier) : fiche suivante, après l'essai de B+. Le format de § 3 est fixé ici pour elle.
  • README (nouveau dossier, LPSaveLocation.ini, aucune reprise automatique : R0), Tools\TracesRacines.ps1:90 (ajouter LifePlay-Revival), lp-build l. 71-72 et lp-validate l. 96-100 : DOC du lot.

6. Trace dans le journal (preuve)

StartupModule appelle LPSaveRelaunch::LogStartupState(), qui écrit une ligne Display dans LogLifePlay : Dossier de sauvegarde : <FPaths::ProjectUserDir()> (relance=<oui|non>, source=<ini|defaut|-UserDir|inactif>, motif=<…>).

  • relance=oui si LP_SAVE_RELAUNCHED vaut 1.
  • motif : raison de l'inaction (editeur, non-empaquete, desactive, Development-sans-ini, echec-creation, echec-relance).

La garde statique mémorise son verdict dans une variable statique du même fichier : un enum et des wchar_t, sans FString.

7. Preuve attendue en jeu (build commun COMMUN-4, Development ; Shipping en fin d'essai si Big Boss l'accorde)

Préparation, sans jeton UE (inventaire seulement) :

  • relevé Get-ChildItem -Recurse + Get-FileHash MD5 de <build>\Windows\LifePlay\Saved (ancien dossier en Development) ;
  • même relevé pour %LOCALAPPDATA%\LifePlay (ancien dossier en Shipping, sauvegardes réelles de l'utilisateur : lecture seule).
Cas Montage Attendu
E1 Development, sans LPSaveLocation.ini comportement inchangé : Saved à côté de l'exe ; une seule instance de LifePlay-Win64-Development.exe ; journal source=inactif, motif=Development-sans-ini
E2 Development, LPSaveLocation.ini avec SaveDir=D:\Game\LifePlay_Backup_5.5\REL\essai_userdir deux processus le temps du jeu : le parent attend, l'enfant a -UserDir="…\essai_userdir" (Get-CimInstance Win32_Process, CommandLine) ; …\essai_userdir\Saved\{Logs,Config,SaveGames} créé ; journal relance=oui, source=ini ; relevé de l'ancien Saved identique avant et après, journal compris (attendu : 0 fichier créé ni modifié, car la garde agit avant toute écriture du moteur ; tout écart, même Logs\LifePlay.log, est mesuré et rapporté) ; en partie : nouvelle partie, sauvegarde, Continuer, mods
E3 E2 + lancement avec -ScriptStackOnWarnings -LPPersonsLog les deux arguments présents dans la ligne de l'enfant et dans LogInit: Command Line: de son journal, dans l'ordre d'origine
E4 E2 + lancement avec un -UserDir= explicite aucune relance (une instance), source=-UserDir
E5 SaveDir=relatif\x, puis SaveDir= vide dossier par défaut %LOCALAPPDATA%\LifePlay-Revival ; journal source=defaut
E6 -LPNoSaveRelaunch une instance, motif=desactive
E7 Fermeture du jeu par le menu code de sortie du bootstrap = code de l'enfant ($LASTEXITCODE ou Start-Process -Wait -PassThru) ; aucune instance restante
E8 (Shipping, sur accord) Shipping sans .ini %LOCALAPPDATA%\LifePlay-Revival\Saved créé ; relevé de %LOCALAPPDATA%\LifePlay identique ; partie neuve et Continuer dans le nouveau dossier

Verdict de l'essai : PASS si E1 à E7 sont conformes et si l'ancien dossier n'est pas modifié. Un journal de pré-initialisation écrit dans l'ancien dossier serait un écart à rapporter : il n'est pas attendu, puisque la garde précède WinMain.

8. IMPL (c), une fois cette SPEC acceptée

  • Instantané : Tools\Snapshot.ps1 -Lot REL-SAV -Path Source\LifePlay\LifePlay.cpp, Source\LifePlay\LPSaveRelaunch.cpp (le second est absent : Existed=False).
  • Fichiers : LPSaveRelaunch.cpp (nouveau) et LifePlay.cpp (1 #include + 1 appel dans StartupModule, après FLPModGuard::Run()). LifePlay.Build.cs : aucune dépendance nouvelle attendue (Shell32, Ole32 déjà reliées par le moteur ; estimé, à confirmer à la compilation de COMMUN-4). En-têtes Windows entre Windows/AllowWindowsPlatformTypes.h et HideWindowsPlatformTypes.h.
  • Sans compiler. VAL statique par un lp-validator distinct : relecture du diff ligne à ligne contre §§ 1-6 ; garde de compilation ; aucune API UE dans le chemin statique ; \" final ; deux verrous anti-boucle ; queue brute non retouchée ; TerminateProcess seulement après l'attente.