LifePlay-Revival/Source/LifePlay/LPCityMapsDownload.h
2026-10-22 21:10:39 +02:00

182 lines
6.8 KiB
C++

// Lot MAPDL1 (MAPDL1-SPEC-05 v3, etape E5) : telechargement HTTP, controle SHA-256 et pose d'un pack de ville.
// Aucune dependance a l'interface : la fenetre (LPCityMapsWindow) lit l'etat et reagit a OnChanged.
//
// Deroule d'un travail (FLPCityMapsDownload), tout sur le fil de jeu sauf le calcul du SHA-256 et la pose (pool de fils) :
// 1. Saved/Downloads/<zip> deja present, de la bonne taille : SHA-256 recontrole, puis il sert directement.
// 2. Sinon Saved/Downloads/<zip>.part (vide apres un echec ou une annulation : deplace dans _rejet/), ecrit en flux par IHttpRequest::SetResponseBodyReceiveStream. Un .part non vide
// est repris par "Range: bytes=<taille>-" (206 attendu avec un Content-Range qui commence a la taille).
// Un 200 sur une reprise : le .part est DEPLACE dans Saved/Downloads/_rejet/ et le telechargement repart de zero.
// 3. 3 essais par URL (attentes 2, 4, 8 s), puis l'URL suivante ; toutes en echec : recours navigateur (NeedsBrowser),
// surveillance du dossier Telechargements de Windows et selecteur de fichier.
// 4. SHA-256 (OpenSSL, par blocs de 1 Mio, hors du fil de jeu). Ecart : le fichier est deplace dans _rejet/, URL suivante.
// 5. "Download only" : le .part devient Saved/Downloads/<zip>. "Full install" : en plus, le zip est controle (libzip), extrait
// dans Saved/Downloads/<pack>/_pose/, puis pose (Content/Modules/<pack> d'abord, Mods/<pack> en dernier) avec retour en
// arriere. Une ancienne installation est deplacee dans Saved/Downloads/_ancien/. Rien n'est jamais supprime.
#pragma once
#include "CoreMinimal.h"
#include "Containers/Ticker.h"
#include "HAL/ThreadSafeBool.h"
#include "Interfaces/IHttpRequest.h"
/** Ce que le joueur a demande. */
enum class ELPCityInstallMode : uint8
{
DownloadOnly,
FullInstall
};
enum class ELPCityJobPhase : uint8
{
/** Rien en cours : avant le depart, apres une annulation ou une erreur (Message dit laquelle). */
Idle,
Downloading,
WaitingRetry,
Verifying,
Installing,
/** Toutes les URL ont echoue : recours navigateur (ouvrir la page, surveiller Telechargements, choisir un fichier). */
NeedsBrowser,
Succeeded
};
/** Donnees de l'entree de l'index utiles au travail (copiees : le travail ne depend pas du sous-systeme). */
struct FLPCityDownloadJob
{
/** Nom d'asset de la ville (journal et messages). */
FString Ville;
/** Identifiant du pack : lpm_city_<ville>. */
FString Pack;
FString Zip;
/** 64 chiffres hexadecimaux. */
FString Sha256;
int64 Octets = 0;
TArray<FString> Urls;
/** Page a ouvrir dans le navigateur en dernier recours (sinon Urls[0]). */
FString Page;
};
struct FLPCityDownloadState
{
ELPCityJobPhase Phase = ELPCityJobPhase::Idle;
ELPCityInstallMode Mode = ELPCityInstallMode::DownloadOnly;
int64 BytesDone = 0;
int64 BytesTotal = 0;
double BytesPerSecond = 0.0;
int32 UrlIndex = 0;
int32 Attempt = 0;
/** Dernier message en clair (anglais, affiche tel quel par la fenetre). */
FString Message;
bool bError = false;
/** Download only : chemin du zip. Full install : dossier Mods/<pack>. */
FString ResultPath;
/** Vrai apres une pose reussie dans cette session. */
bool bInstalled = false;
};
class FLPCityMapsDownload : public TSharedFromThis<FLPCityMapsDownload>
{
public:
explicit FLPCityMapsDownload(const FLPCityDownloadJob& InJob);
~FLPCityMapsDownload();
/** Appele sur le fil de jeu a chaque changement d'etat ou de progression. */
TFunction<void()> OnChanged;
/** Demarre (ou redemarre apres une annulation ou une erreur) le travail. Sans effet si un travail est en cours. */
void Start(ELPCityInstallMode InMode);
/** Interrompt le telechargement ou le controle en cours. Le .part est garde pour une reprise. Sans effet pendant la pose. */
void Cancel();
/** Recours : surveille le dossier Telechargements de Windows toutes les 2 s (un .zip de la taille exacte, au SHA-256 conforme). */
void StartWatchingDownloads();
void StopWatchingDownloads();
/** Recours : fichier choisi par le joueur (taille, SHA-256, copie dans Saved/Downloads, puis comme "Download only" ou "Full install"). */
void UseLocalFile(const FString& SourcePath);
/** Telechargement, controle ou pose en cours ? */
bool IsBusy() const;
const FLPCityDownloadJob& GetJob() const { return Job; }
const FLPCityDownloadState& GetState() const { return State; }
/** Saved/Downloads, chemin complet. */
static FString GetDownloadsDir();
/** Dossier Telechargements de Windows (SHGetKnownFolderPath). Faux hors Windows ou en cas d'echec. */
static bool FindSystemDownloadsFolder(FString& OutFolder);
/** Selecteur de fichier Win32 (GetOpenFileNameW). Faux si le joueur annule, ou hors Windows. */
static bool PickZipFile(FString& OutPath);
/** Nom d'hote d'une URL (rien d'autre : ni chemin, ni port, ni identifiants). */
static FString HostOf(const FString& Url);
private:
/** D'ou vient le fichier controle. */
enum class EVerifySource : uint8
{
/** Le .part qui vient d'etre telecharge. */
Part,
/** Saved/Downloads/<zip> deja present. */
Cache,
/** Zip trouve dans Telechargements. */
Watched,
/** Zip choisi par le joueur. */
Local
};
FString GetPartPath() const;
FString GetFinalPath() const;
void Notify();
void SetMessage(const FString& Text, bool bIsError);
void StartAttempt();
void HandleRequestProgress(FHttpRequestPtr Request, uint64 BytesSent, uint64 BytesReceived);
void HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSucceeded);
void CloseWriter();
void AbortRequestQuietly();
void FailAttempt(const FString& Reason);
void ScheduleRetry(float DelaySeconds);
bool OnRetryTick(float DeltaTime);
void NextUrl(const FString& Reason);
void EnterBrowserRecourse();
void BeginVerify(const FString& Path, EVerifySource Source);
void OnVerified(const FString& Path, EVerifySource Source, bool bReadOk, const FString& Hex, const FString& Error);
void FinalizePart();
void CopyExternalToFinal(const FString& SourcePath);
void OnPackageReady();
void BeginInstall();
void OnInstalled(bool bOk, int32 FilesPlaced, const FString& Error, bool bRollbackOk);
bool OnWatchTick(float DeltaTime);
/** Deplace un fichier dans Saved/Downloads/_rejet/ (nom horodate). Renvoie la destination, ou vide si le deplacement a echoue. */
FString RejectFile(const FString& Path, const TCHAR* Why);
/** Ferme le flux ; un .part de 0 octet est deplace dans _rejet/. Vrai s'il a ete deplace. */
bool SetAsideEmptyPart();
FLPCityDownloadJob Job;
FLPCityDownloadState State;
bool bJobValid = true;
FHttpRequestPtr Request;
TSharedPtr<FArchive> PartWriter;
int64 ResumeFrom = 0;
bool bCancelRequested = false;
FString LastFailure;
double SpeedTime = 0.0;
int64 SpeedBytes = 0;
FTSTicker::FDelegateHandle RetryHandle;
FTSTicker::FDelegateHandle WatchHandle;
TSet<FString> ExaminedDownloads;
/** Partage avec les taches du pool de fils : arret du calcul du SHA-256. */
TSharedRef<FThreadSafeBool, ESPMode::ThreadSafe> CancelFlag;
};