// Lot REL-SAV (REL-SAV-SPEC) : dossier de sauvegarde du jeu empaquete, %LOCALAPPDATA%\LifePlay-Revival par defaut. // // Le moteur calcule Saved une seule fois, a partir de la seule ligne de commande (UE 5.8 : Paths.cpp:451-471, 485-494) : // sans -UserDir, un Shipping ecrit dans %LOCALAPPDATA%\\Saved. Le nom du projet (LifePlay) ne peut pas // changer, et UE_SAVED_DIR_OVERRIDE est sans effet avec le moteur Launcher (Core precompile ; REL-SAV-DIAG). // // Garde : un objet statique de ce fichier s'execute a l'initialisation statique de l'exe, avant WinMain, donc avant que // le moteur n'ecrive quoi que ce soit (journal, .ini, Saved). Si le jeu doit changer de dossier, il se relance avec les // arguments d'origine recopies tels quels, plus -UserDir="", attend la fin de l'enfant et sort avec son code. // - Compile seulement pour un jeu Windows monolithique hors editeur (jamais l'editeur, les commandlets ni -game). // - Actif seulement dans un paquet (\Content\Paks\LifePlay-Windows.pak present). // - Shipping : toujours ; Development et DebugGame : seulement si LPSaveLocation.ini existe (outils de debogage qui // lisent Saved a cote de l'exe). // - Inactif avec -LPNoSaveRelaunch, avec un -UserDir deja donne, ou si LP_SAVE_RELAUNCHED est posee (deux verrous : // une seule relance possible). // - Cible : SaveDir de \LPSaveLocation.ini, section [SaveLocation] (variables %...% developpees, chemin absolu // seulement), sinon %LOCALAPPDATA%\LifePlay-Revival. Le moteur y range Saved\{SaveGames,Config,Logs...}. // - Tout echec (dossier, relance) : demarrage normal, sans relance. // Chemin statique : API Win32 et tampons wchar_t seulement (GMalloc, FString, FPaths, GConfig, UE_LOG pas encore prets). #include "LPSaveRelaunch.h" #include "LifePlay.h" #include "HAL/PlatformMisc.h" #include "Misc/Paths.h" #if PLATFORM_WINDOWS && IS_MONOLITHIC && !WITH_EDITOR && defined(UE_GAME) && UE_GAME #define LP_SAVE_RELAUNCH_ENABLED 1 #else #define LP_SAVE_RELAUNCH_ENABLED 0 #endif #if LP_SAVE_RELAUNCH_ENABLED #include "Windows/WindowsHWrapper.h" // windows.h puis PostWindowsApi.h (macros conflictuelles retirees) avant ShlObj.h #include "Windows/AllowWindowsPlatformTypes.h" #include #include "Windows/HideWindowsPlatformTypes.h" namespace LPSaveRelaunchPrivate { // Verdict de la garde, lu par LogStartupState. Valeurs constantes : initialisees avant tout code dynamique. enum EState : int { StateNotRun = 0, StateNotPackaged, StateDisabled, StateUserDirGiven, StateDevelopmentWithoutIni, StateCreateDirFailed, StateRelaunchFailed, }; constexpr DWORD BufferLen = 32768; // longueur maximale d'une ligne de commande Windows const wchar_t* const RelaunchedVar = L"LP_SAVE_RELAUNCHED"; int GState = StateNotRun; wchar_t GExe[BufferLen]; wchar_t GProjectDir[BufferLen]; wchar_t GIniPath[BufferLen]; wchar_t GRaw[BufferLen]; wchar_t GTarget[BufferLen]; wchar_t GCommandLine[BufferLen]; DWORD Len(const wchar_t* S) { DWORD N = 0; while (S[N]) { ++N; } return N; } // Ajoute Src a Dst (capacite BufferLen) ; false si la place manque. bool Append(wchar_t* Dst, const wchar_t* Src) { DWORD N = Len(Dst); for (DWORD I = 0; Src[I]; ++I) { if (N + 1 >= BufferLen) { return false; } Dst[N++] = Src[I]; } Dst[N] = 0; return true; } wchar_t Lower(wchar_t C) { return (C >= L'A' && C <= L'Z') ? wchar_t(C - L'A' + L'a') : C; } // Mot (minuscules) present dans Line, sans casse, precede de Before (ou en debut si AtStart) et, si // bWholeWord, suivi d'une fin, d'un blanc ou d'un guillemet. bool HasToken(const wchar_t* Line, const wchar_t* Word, const wchar_t* Before, bool bWholeWord) { const DWORD WordLen = Len(Word); for (DWORD Pos = 0; Line[Pos]; ++Pos) { DWORD I = 0; while (I < WordLen && Line[Pos + I] && Lower(Line[Pos + I]) == Word[I]) { ++I; } if (I != WordLen) { continue; } bool bBeforeOk = (Pos == 0); for (DWORD B = 0; !bBeforeOk && Before[B]; ++B) { bBeforeOk = (Line[Pos - 1] == Before[B]); } const wchar_t After = Line[Pos + WordLen]; const bool bAfterOk = !bWholeWord || After == 0 || After == L' ' || After == L'\t' || After == L'"'; if (bBeforeOk && bAfterOk) { return true; } } return false; } bool FileExists(const wchar_t* Path) { const DWORD Attr = GetFileAttributesW(Path); return Attr != INVALID_FILE_ATTRIBUTES && !(Attr & FILE_ATTRIBUTE_DIRECTORY); } bool DirExists(const wchar_t* Path) { const DWORD Attr = GetFileAttributesW(Path); return Attr != INVALID_FILE_ATTRIBUTES && (Attr & FILE_ATTRIBUTE_DIRECTORY); } // Coupe Path apres son dernier separateur (le separateur compris) ; false s'il n'y en a pas. bool CutLastComponent(wchar_t* Path) { for (DWORD I = Len(Path); I > 0; --I) { if (Path[I - 1] == L'\\' || Path[I - 1] == L'/') { Path[I - 1] = 0; return true; } } return false; } // Arguments apres argv[0], recopies tels quels (regle Windows : argv[0] entre guillemets ou jusqu'au premier blanc). const wchar_t* SkipProgramName(const wchar_t* Line) { const wchar_t* P = Line; if (*P == L'"') { ++P; while (*P && *P != L'"') { ++P; } if (*P) { ++P; } } else { while (*P && *P != L' ' && *P != L'\t') { ++P; } } while (*P == L' ' || *P == L'\t') { ++P; } return P; } bool IsAbsolute(const wchar_t* P) { const bool bDrive = ((P[0] >= L'A' && P[0] <= L'Z') || (P[0] >= L'a' && P[0] <= L'z')) && P[1] == L':' && (P[2] == L'\\' || P[2] == L'/'); const bool bUnc = P[0] == L'\\' && P[1] == L'\\'; return bDrive || bUnc; } // GTarget = SaveDir de l'ini (blancs et guillemets de bord retires, variables developpees) s'il est absolu ; sinon // %LOCALAPPDATA%\LifePlay-Revival. Source dans OutSource ; false si aucun dossier n'a pu etre calcule. bool ComputeTarget(bool bIniExists, const wchar_t*& OutSource) { GTarget[0] = 0; if (bIniExists) { GetPrivateProfileStringW(L"SaveLocation", L"SaveDir", L"", GRaw, BufferLen, GIniPath); wchar_t* Start = GRaw; while (*Start == L' ' || *Start == L'\t' || *Start == L'"') { ++Start; } DWORD End = Len(Start); while (End > 0 && (Start[End - 1] == L' ' || Start[End - 1] == L'\t' || Start[End - 1] == L'"')) { Start[--End] = 0; } if (*Start) { const DWORD Needed = ExpandEnvironmentStringsW(Start, GTarget, BufferLen); bool bHasQuote = false; // un " fermerait -UserDir="..." (VAL R1) ; interdit dans un chemin Windows for (DWORD I = 0; !bHasQuote && I < BufferLen && GTarget[I]; ++I) { bHasQuote = (GTarget[I] == L'"'); } if (Needed == 0 || Needed > BufferLen || bHasQuote || !IsAbsolute(GTarget)) { GTarget[0] = 0; } } } if (GTarget[0]) { OutSource = L"ini"; } else { PWSTR LocalAppData = nullptr; const bool bOk = SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &LocalAppData)) && LocalAppData && Append(GTarget, LocalAppData) && Append(GTarget, L"\\LifePlay-Revival"); if (LocalAppData) { CoTaskMemFree(LocalAppData); } if (!bOk) { return false; } OutSource = L"defaut"; } // Pas de separateur final : un \" final serait lu comme un guillemet echappe. Racine X: -> X:\. DWORD N = Len(GTarget); while (N > 0 && (GTarget[N - 1] == L'\\' || GTarget[N - 1] == L'/')) { GTarget[--N] = 0; } if (N == 2 && GTarget[1] == L':') { return Append(GTarget, L"\\."); } return N > 0; } void Run() { // 1. Exe reel (Binaries\Win64\LifePlay-Win64-.exe, pas le bootstrap) et dossier du projet (..\..). const DWORD ExeLen = GetModuleFileNameW(nullptr, GExe, BufferLen); if (ExeLen == 0 || ExeLen >= BufferLen) { GState = StateNotPackaged; return; } GProjectDir[0] = 0; Append(GProjectDir, GExe); if (!CutLastComponent(GProjectDir) || !CutLastComponent(GProjectDir) || !CutLastComponent(GProjectDir)) { GState = StateNotPackaged; return; } GRaw[0] = 0; if (!Append(GRaw, GProjectDir) || !Append(GRaw, L"\\Content\\Paks\\LifePlay-Windows.pak") || !FileExists(GRaw)) { GState = StateNotPackaged; return; } // 2. Desactivation explicite ; 3. verrous anti-boucle. const wchar_t* const Tail = SkipProgramName(GetCommandLineW()); if (HasToken(Tail, L"-lpnosaverelaunch", L" \t\"", true)) { GState = StateDisabled; return; } if (HasToken(Tail, L"userdir=", L"-/", false) || GetEnvironmentVariableW(RelaunchedVar, GRaw, BufferLen) > 0) { GState = StateUserDirGiven; return; } // 4. Configuration : Shipping toujours, sinon seulement avec LPSaveLocation.ini. GIniPath[0] = 0; const bool bIniExists = Append(GIniPath, GProjectDir) && Append(GIniPath, L"\\LPSaveLocation.ini") && FileExists(GIniPath); #if !UE_BUILD_SHIPPING if (!bIniExists) { GState = StateDevelopmentWithoutIni; return; } #endif // Cible, creee si besoin. const wchar_t* Source = L"defaut"; if (!ComputeTarget(bIniExists, Source)) { GState = StateCreateDirFailed; return; } const int Created = SHCreateDirectoryExW(nullptr, GTarget, nullptr); if ((Created != ERROR_SUCCESS && Created != ERROR_ALREADY_EXISTS && Created != ERROR_FILE_EXISTS) || !DirExists(GTarget)) { OutputDebugStringW(L"LifePlay : dossier de sauvegarde impossible a creer, demarrage sans relance\n"); GState = StateCreateDirFailed; return; } // Relance : "" -UserDir="". GCommandLine[0] = 0; const bool bLineOk = Append(GCommandLine, L"\"") && Append(GCommandLine, GExe) && Append(GCommandLine, L"\" ") && Append(GCommandLine, Tail) && (Tail[0] == 0 || Append(GCommandLine, L" ")) && Append(GCommandLine, L"-UserDir=\"") && Append(GCommandLine, GTarget) && Append(GCommandLine, L"\""); if (!bLineOk) { GState = StateRelaunchFailed; return; } STARTUPINFOW ParentInfo = {}; ParentInfo.cb = sizeof(ParentInfo); GetStartupInfoW(&ParentInfo); STARTUPINFOW Info = {}; Info.cb = sizeof(Info); Info.dwFlags = STARTF_USESHOWWINDOW; Info.wShowWindow = (ParentInfo.dwFlags & STARTF_USESHOWWINDOW) ? ParentInfo.wShowWindow : WORD(SW_SHOWDEFAULT); PROCESS_INFORMATION Process = {}; SetEnvironmentVariableW(RelaunchedVar, Source); if (!CreateProcessW(GExe, GCommandLine, nullptr, nullptr, /*bInheritHandles*/ 0, 0, nullptr, nullptr, &Info, &Process)) { SetEnvironmentVariableW(RelaunchedVar, nullptr); OutputDebugStringW(L"LifePlay : relance avec -UserDir impossible, demarrage sans relance\n"); GState = StateRelaunchFailed; return; } // Le parent attend l'enfant et sort avec son code, sans destructeurs statiques du moteur (TerminateProcess). CloseHandle(Process.hThread); WaitForSingleObject(Process.hProcess, INFINITE); DWORD ExitCode = 1; GetExitCodeProcess(Process.hProcess, &ExitCode); CloseHandle(Process.hProcess); TerminateProcess(GetCurrentProcess(), ExitCode); } struct FGuard { FGuard() { Run(); } }; FGuard GGuard; } #endif // LP_SAVE_RELAUNCH_ENABLED void LPSaveRelaunch::LogStartupState() { FString Relaunched = TEXT("non"); FString Source = TEXT("inactif"); FString Reason = TEXT("-"); #if LP_SAVE_RELAUNCH_ENABLED using namespace LPSaveRelaunchPrivate; switch (GState) { case StateNotPackaged: Reason = TEXT("non-empaquete"); break; case StateDisabled: Reason = TEXT("desactive"); break; case StateDevelopmentWithoutIni: Reason = TEXT("Development-sans-ini"); break; case StateCreateDirFailed: Reason = TEXT("echec-creation"); break; case StateRelaunchFailed: Reason = TEXT("echec-relance"); break; case StateUserDirGiven: { // Enfant relance : LP_SAVE_RELAUNCHED porte la source (ini ou defaut) ; sinon -UserDir donne au lancement. const FString Var = FPlatformMisc::GetEnvironmentVariable(RelaunchedVar); Relaunched = Var.IsEmpty() ? FString(TEXT("non")) : FString(TEXT("oui")); Source = Var.IsEmpty() ? FString(TEXT("-UserDir")) : Var; break; } default: Reason = TEXT("non-execute"); break; } #elif WITH_EDITOR Reason = TEXT("editeur"); #else Reason = TEXT("non-compile"); #endif UE_LOG(LogLifePlay, Display, TEXT("Dossier de sauvegarde : %s (relance=%s, source=%s, motif=%s)"), *FPaths::ConvertRelativePathToFull(FPaths::ProjectUserDir()), *Relaunched, *Source, *Reason); }