396 lines
12 KiB
C++
396 lines
12 KiB
C++
// 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%\<nom du projet>\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="<cible>", 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 (<projet>\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 <projet>\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 <shlobj.h>
|
|
#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-<Config>.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 : "<exe>" <arguments d'origine tels quels> -UserDir="<cible>".
|
|
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);
|
|
}
|