Couche de donnees, etapes 2 et 3 : cache compile JSON par type, Modules charge une fois en arriere-plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-02 11:58:39 +02:00
parent b982bd7d58
commit d443257bf7
7 changed files with 391 additions and 6 deletions

View file

@ -531,3 +531,27 @@ Conséquences pour la suite :
−6 s au premier chargement d'une partie, gain faible ensuite. −6 s au premier chargement d'une partie, gain faible ensuite.
- **Étape 4** (analyse en C++) ou suppression des relances inutiles de `PopulateRaws` : jusqu'à −6 s **à chaque** - **Étape 4** (analyse en C++) ou suppression des relances inutiles de `PopulateRaws` : jusqu'à −6 s **à chaque**
passage. passage.
### 17.3 Étapes 2 et 3 : couche de données (2026-10-02)
Nouveau `Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.h/.cpp`, lancé au démarrage du module (hors
commandlets) :
- **relevé** : date et taille des fichiers texte de `Modules` (`.lp*`, `.txt`, `.config`, `.cfg`), sans ouverture ;
- **cache compilé** : un JSON par type dans `Saved/LPDataCache/<type>.json` (`version`, `type`, `files`). Pour chaque
fichier : `stamp`, `size`, `text` et, pour `.lpaction`, `.lpstat`, `.lpquest`, `.lpmod`, des `fields` (champs
« CLÉ: valeur » analysés). Le **calque** de valeurs par défaut, fichier optionnel
`Content/Modules/_lpdata_defaults.json`, complète les champs absents des enregistrements ;
- **mise à jour incrémentale** : seuls les fichiers nouveaux ou modifiés sont relus (mods ajoutés, éditions) ;
- **`LoadTxt`** sert le texte depuis la mémoire, identique à la lecture du disque ; **`SaveTxt`** met la mémoire à
jour ; la lecture se fait sur le disque avant la fin du chargement, ou hors `Modules` (sauvegardes) ;
- options : `-LPNoDataCache` pour désactiver, `-LPDataCacheVerify` pour comparer chaque texte servi au disque.
| Mesure (build `LifePlay_Build_5.5_DATA23`, en arrière-plan au menu) | Valeur |
|---|---:|
| 1ᵉʳ lancement : construction du cache (2 051 fichiers, images comprises) | 12,3 s |
| Lancements suivants : chargement depuis le cache (2 031 fichiers texte) | **0,14 s** |
| Relevé des dates et tailles | 0,014 s |
| Cache (11 fichiers JSON) | ≈ 8 Mo, dont `lpscene` 4,6 Mo |
Validation en jeu en attente : partie lancée avec `LifePlay_verif_cache.bat` (`-LPDataCacheVerify`), puis bilan
`LoadTxt` et absence d'écart dans le journal.

View file

@ -920,3 +920,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
| CHG-073 | 2026-10-02 | ERR-038 | `Source/LifePlay/LPRoomTransitionSubsystem.h/.cpp` supprimés (copie dans `D:\Game\LifePlay_Backup_5.5\ERR038_retrait`) ; build `LifePlay_Build_5.5_ERR038` reconstruit sans le sous-système (vérifié dans l'exécutable) | second test « plus problématique » : relevé `ERR038_apres2_20261002` = 2 déclenchements, tous deux à tort (clients du lieu d'arrivée cachés 3 s) ; le critère temporel ne distingue pas les personnages créés pendant le trajet puis retirés de ceux qui sont conservés ; retrait proposé et accepté par l'utilisateur ; ERR-038 reste ouvert | `git checkout ERR038-apres -- Source/LifePlay` | | CHG-073 | 2026-10-02 | ERR-038 | `Source/LifePlay/LPRoomTransitionSubsystem.h/.cpp` supprimés (copie dans `D:\Game\LifePlay_Backup_5.5\ERR038_retrait`) ; build `LifePlay_Build_5.5_ERR038` reconstruit sans le sous-système (vérifié dans l'exécutable) | second test « plus problématique » : relevé `ERR038_apres2_20261002` = 2 déclenchements, tous deux à tort (clients du lieu d'arrivée cachés 3 s) ; le critère temporel ne distingue pas les personnages créés pendant le trajet puis retirés de ceux qui sont conservés ; retrait proposé et accepté par l'utilisateur ; ERR-038 reste ouvert | `git checkout ERR038-apres -- Source/LifePlay` |
| CHG-074 | 2026-10-02 | O7 | O7a : `Content/Saves` (14 125 fichiers, parties de test) → `D:\Game\LifePlay_Backup_5.5\O7\Content_Saves` (vérifié), `Content/Developers` et `Content/Collections` (dossiers vides) retirés, 3 captures de la racine rangées ; O7b : `08_Guide_Developpeur.md` §2.1 (carte de `Content/Heavy`, dossiers à ne pas déplacer) | ménage sans risque et documentation, à la place d'un renommage des dossiers jugé trop risqué (préfixes de chemins en dur, contrat de modding) | déplacer en retour le contenu de `O7\Content_Saves` | | CHG-074 | 2026-10-02 | O7 | O7a : `Content/Saves` (14 125 fichiers, parties de test) → `D:\Game\LifePlay_Backup_5.5\O7\Content_Saves` (vérifié), `Content/Developers` et `Content/Collections` (dossiers vides) retirés, 3 captures de la racine rangées ; O7b : `08_Guide_Developpeur.md` §2.1 (carte de `Content/Heavy`, dossiers à ne pas déplacer) | ménage sans risque et documentation, à la place d'un renommage des dossiers jugé trop risqué (préfixes de chemins en dur, contrat de modding) | déplacer en retour le contenu de `O7\Content_Saves` |
| CHG-075 | 2026-10-02 | DATA1 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` : statistiques de `LoadTxt` par type de fichier (bilan à la fermeture, commande `ModdingExtra.LoadTxtStats`) ; `11_Audit_Optimisation.md` §17 (stratégie de couche de données) | étape 1 de la couche de données : mesurer avant de construire ; `PopulateRaws` = 12,6 s par chargement de partie (trace O2) ; build `LifePlay_Build_5.5_DATA1` | `git checkout DATA1-avant -- Plugins/ModdingExtra` | | CHG-075 | 2026-10-02 | DATA1 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` : statistiques de `LoadTxt` par type de fichier (bilan à la fermeture, commande `ModdingExtra.LoadTxtStats`) ; `11_Audit_Optimisation.md` §17 (stratégie de couche de données) | étape 1 de la couche de données : mesurer avant de construire ; `PopulateRaws` = 12,6 s par chargement de partie (trace O2) ; build `LifePlay_Build_5.5_DATA1` | `git checkout DATA1-avant -- Plugins/ModdingExtra` |
| CHG-076 | 2026-10-02 | DATA23 | nouveaux `Plugins/ModdingExtra/.../LPDataCache.h/.cpp` ; `ModdingExtra.cpp` (démarrage en arrière-plan), `ModdingExtraBPLibrary.cpp` (`LoadTxt` depuis la mémoire, `SaveTxt`, vérification, statistiques cache/disque), `ModdingExtra.Build.cs` (`Json`) | couche de données étapes 2 et 3 : `Modules` chargé une fois en arrière-plan, cache compilé JSON par type avec champs et calque ; 2 031 fichiers en 0,14 s depuis le cache ; build `LifePlay_Build_5.5_DATA23` ; validation en jeu en attente | `git checkout DATA23-avant -- Plugins/ModdingExtra` ; ou option `-LPNoDataCache` |

View file

@ -34,6 +34,7 @@ public class ModdingExtra : ModuleRules
{ {
"CoreUObject", "CoreUObject",
"AssetRegistry", // portage 5.5, lot O2b : enumeration des assets en conteneurs IoStore "AssetRegistry", // portage 5.5, lot O2b : enumeration des assets en conteneurs IoStore
"Json", // portage 5.5, couche de donnees : cache compile (Saved/LPDataCache)
"Engine", "Engine",
"Slate", "Slate",
"SlateCore", "SlateCore",

View file

@ -0,0 +1,277 @@
// Portage UE 5.5 - voir LPDataCache.h
#include "LPDataCache.h"
#include "Async/Async.h"
#include "Dom/JsonObject.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformTime.h"
#include "Misc/CommandLine.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPDataCache, Log, All);
namespace
{
constexpr int32 CacheVersion = 1;
const TCHAR* ModulesFolder = TEXT("Modules");
/** Formats "CLE: valeur" dont les champs sont analyses dans le cache (base du calque). */
bool IsKeyValueFormat(const FString& Ext)
{
return Ext == TEXT("lpaction") || Ext == TEXT("lpstat") || Ext == TEXT("lpquest") || Ext == TEXT("lpmod");
}
/** Champs "CLE: valeur" d'un texte (une ligne par champ, premiere occurrence retenue). */
TSharedRef<FJsonObject> ParseFields(const FString& Text, const TSharedPtr<FJsonObject>& Defaults)
{
TSharedRef<FJsonObject> Fields = MakeShared<FJsonObject>();
TArray<FString> Lines;
Text.ParseIntoArrayLines(Lines, true);
for (const FString& Line : Lines)
{
int32 Colon = INDEX_NONE;
if (Line.FindChar(TEXT(':'), Colon) && Colon > 0)
{
const FString Key = Line.Left(Colon).TrimStartAndEnd();
if (!Key.Contains(TEXT(" ")) && !Fields->HasField(Key))
{
Fields->SetStringField(Key, Line.Mid(Colon + 1).TrimStartAndEnd());
}
}
}
if (Defaults.IsValid())
{
for (const auto& Pair : Defaults->Values)
{
if (!Fields->HasField(Pair.Key))
{
Fields->SetField(Pair.Key, Pair.Value);
}
}
}
return Fields;
}
}
FLPDataCache& FLPDataCache::Get()
{
static FLPDataCache Instance;
return Instance;
}
FString FLPDataCache::MakeKey(const FString& ContentRelativePath)
{
FString Key = ContentRelativePath;
Key.ReplaceInline(TEXT("\\"), TEXT("/"));
while (Key.ReplaceInline(TEXT("//"), TEXT("/")) > 0) {}
Key.RemoveFromStart(TEXT("/"));
return Key.ToLower();
}
void FLPDataCache::StartAsync()
{
if (bStarted)
{
return;
}
bStarted = true;
bDisabled = FParse::Param(FCommandLine::Get(), TEXT("LPNoDataCache"));
bVerify = FParse::Param(FCommandLine::Get(), TEXT("LPDataCacheVerify"));
if (bDisabled)
{
UE_LOG(LogLPDataCache, Display, TEXT("Cache de donnees desactive (-LPNoDataCache)"));
return;
}
Async(EAsyncExecution::Thread, [this]() { LoadAll(); });
}
void FLPDataCache::LoadAll()
{
const double Start = FPlatformTime::Seconds();
const FString ContentDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
const FString ModulesDir = ContentDir / ModulesFolder;
const FString CacheDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()) / TEXT("LPDataCache");
// 1. Releve des fichiers de Modules : date et taille, sans ouverture.
struct FFound { FString Key; FString FullPath; FString Ext; FDateTime Stamp; int64 Size; };
TArray<FFound> Found;
IFileManager::Get().IterateDirectoryStatRecursively(*ModulesDir, [&](const TCHAR* Path, const FFileStatData& Stat)
{
if (!Stat.bIsDirectory)
{
FString Full(Path);
FString Relative = Full;
FPaths::MakePathRelativeTo(Relative, *(ContentDir + TEXT("/")));
FString Ext = FPaths::GetExtension(Full).ToLower();
// Formats texte seulement (les images .jpg des modules sont lues autrement, jamais par LoadTxt).
if (!(Ext.StartsWith(TEXT("lp")) || Ext == TEXT("txt") || Ext == TEXT("config") || Ext == TEXT("cfg")))
{
return true;
}
Found.Add({ MakeKey(Relative), Full, Ext, Stat.ModificationTime, Stat.FileSize });
}
return true;
});
const double ScanSeconds = FPlatformTime::Seconds() - Start;
// Calque de valeurs par defaut (optionnel).
TSharedPtr<FJsonObject> AllDefaults;
{
FString DefaultsText;
if (FFileHelper::LoadFileToString(DefaultsText, *(ModulesDir / TEXT("_lpdata_defaults.json"))))
{
FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(DefaultsText), AllDefaults);
}
}
// 2. Cache compile existant, par type.
TMap<FString, TSharedPtr<FJsonObject>> Cached;
TSet<FString> Exts;
for (const FFound& F : Found) { Exts.Add(F.Ext); }
for (const FString& Ext : Exts)
{
FString Json;
TSharedPtr<FJsonObject> Root;
int32 Version = 0;
const TSharedPtr<FJsonObject>* FilesPtr = nullptr;
if (FFileHelper::LoadFileToString(Json, *(CacheDir / (Ext + TEXT(".json"))))
&& FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Json), Root) && Root.IsValid()
&& Root->TryGetNumberField(TEXT("version"), Version) && Version == CacheVersion
&& Root->TryGetObjectField(TEXT("files"), FilesPtr))
{
Cached.Add(Ext, *FilesPtr);
}
}
// 3. Entrees : depuis le cache si date et taille identiques, sinon lecture du disque.
TMap<FString, FEntry> NewEntries;
TSet<FString> ChangedExts;
int32 FromCache = 0, FromDisk = 0;
for (const FFound& F : Found)
{
FEntry Entry;
Entry.Stamp = F.Stamp;
Entry.Size = F.Size;
bool bHit = false;
if (const TSharedPtr<FJsonObject>* Files = Cached.Find(F.Ext))
{
const TSharedPtr<FJsonObject>* Obj = nullptr;
FString CachedStamp, CachedText;
double CachedSize = -1.0;
if ((*Files)->TryGetObjectField(F.Key, Obj)
&& (*Obj)->TryGetStringField(TEXT("stamp"), CachedStamp) && CachedStamp == F.Stamp.ToIso8601()
&& (*Obj)->TryGetNumberField(TEXT("size"), CachedSize) && (int64)CachedSize == F.Size
&& (*Obj)->TryGetStringField(TEXT("text"), CachedText))
{
Entry.Text = MoveTemp(CachedText);
bHit = true;
}
}
if (!bHit)
{
FFileHelper::LoadFileToString(Entry.Text, *F.FullPath);
ChangedExts.Add(F.Ext);
++FromDisk;
}
else
{
++FromCache;
}
NewEntries.Add(F.Key, MoveTemp(Entry));
}
// Un fichier supprime change aussi son type.
for (const auto& Pair : Cached)
{
for (const auto& File : Pair.Value->Values)
{
if (!NewEntries.Contains(File.Key)) { ChangedExts.Add(Pair.Key); }
}
}
// 4. Reecriture des types modifies : texte, date, taille et, pour les formats "CLE: valeur", champs + calque.
IFileManager::Get().MakeDirectory(*CacheDir, true);
for (const FString& Ext : ChangedExts)
{
const TSharedPtr<FJsonObject>* DefaultsPtr = nullptr;
TSharedPtr<FJsonObject> Defaults;
if (AllDefaults.IsValid() && AllDefaults->TryGetObjectField(Ext, DefaultsPtr)) { Defaults = *DefaultsPtr; }
TSharedRef<FJsonObject> Files = MakeShared<FJsonObject>();
for (const FFound& F : Found)
{
if (F.Ext != Ext) { continue; }
const FEntry& E = NewEntries[F.Key];
TSharedRef<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetStringField(TEXT("stamp"), E.Stamp.ToIso8601());
Obj->SetNumberField(TEXT("size"), (double)E.Size);
Obj->SetStringField(TEXT("text"), E.Text);
if (IsKeyValueFormat(Ext))
{
Obj->SetObjectField(TEXT("fields"), ParseFields(E.Text, Defaults));
}
Files->SetObjectField(F.Key, Obj);
}
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetNumberField(TEXT("version"), CacheVersion);
Root->SetStringField(TEXT("type"), Ext);
Root->SetObjectField(TEXT("files"), Files);
FString Out;
FJsonSerializer::Serialize(Root, TJsonWriterFactory<>::Create(&Out));
FFileHelper::SaveStringToFile(Out, *(CacheDir / (Ext + TEXT(".json"))), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
}
Entries = MoveTemp(NewEntries);
bReady = true;
UE_LOG(LogLPDataCache, Display, TEXT("Cache de donnees pret : %d fichier(s) (%d depuis le cache, %d relus sur le disque), %d type(s) reecrit(s), releve %.3f s, total %.3f s"),
Entries.Num(), FromCache, FromDisk, ChangedExts.Num(), ScanSeconds, FPlatformTime::Seconds() - Start);
}
bool FLPDataCache::TryGet(const FString& ContentRelativePath, FString& OutText)
{
if (bDisabled || !bReady)
{
return false;
}
const FString Key = MakeKey(ContentRelativePath);
if (!Key.StartsWith(TEXT("modules/")) || WrittenBeforeReady.Contains(Key))
{
return false;
}
const FEntry* Entry = Entries.Find(Key);
if (!Entry)
{
return false;
}
OutText = Entry->Text;
return true;
}
void FLPDataCache::OnWritten(const FString& ContentRelativePath, const FString& Text)
{
const FString Key = MakeKey(ContentRelativePath);
if (!Key.StartsWith(TEXT("modules/")))
{
return;
}
if (!bReady)
{
WrittenBeforeReady.Add(Key);
return;
}
if (FEntry* Entry = Entries.Find(Key))
{
// Relu tel que LoadTxt le lirait (encodage d'ecriture et de lecture identiques a l'usage normal).
FFileHelper::LoadFileToString(Entry->Text, *(FPaths::ProjectContentDir() + ContentRelativePath));
}
else
{
FEntry NewEntry;
FFileHelper::LoadFileToString(NewEntry.Text, *(FPaths::ProjectContentDir() + ContentRelativePath));
Entries.Add(Key, MoveTemp(NewEntry));
}
}

View file

@ -0,0 +1,55 @@
// Portage UE 5.5 - couche de donnees, etapes 2 et 3 (Docs/Migration/11_Audit_Optimisation.md §17).
//
// Les fichiers texte de Content/Modules (jeu de base et mods) restent la source. Au demarrage du jeu, une tache
// d'arriere-plan :
// 1. releve la date et la taille de chaque fichier de Modules (sans les ouvrir) ;
// 2. charge le cache compile : un fichier JSON par type (Saved/LPDataCache/<type>.json), contenant le texte de chaque
// fichier et, pour les formats "CLE: valeur", ses champs analyses completes par un calque de valeurs par defaut
// (fichier optionnel Content/Modules/_lpdata_defaults.json, {"lpaction": {"CLE": "valeur"}, ...}) ;
// 3. relit sur le disque les seuls fichiers nouveaux ou modifies, puis reecrit le cache s'il a change.
// Ensuite, LoadTxt sert le texte de Modules depuis la memoire (texte identique a FFileHelper::LoadFileToString) et
// SaveTxt la met a jour. Avant la fin du chargement, ou pour tout chemin hors Modules, la lecture se fait sur le disque.
//
// Options de ligne de commande : -LPNoDataCache (desactive), -LPDataCacheVerify (compare chaque texte servi a une
// lecture du disque et journalise les ecarts).
#pragma once
#include "CoreMinimal.h"
#include <atomic>
class FLPDataCache
{
public:
static FLPDataCache& Get();
/** Lance le chargement en arriere-plan (une fois). */
void StartAsync();
/** Texte d'un fichier de Modules depuis la memoire. Faux si absent, cache pas pret ou desactive. Thread de jeu. */
bool TryGet(const FString& ContentRelativePath, FString& OutText);
/** Mise a jour apres une ecriture (SaveTxt). Thread de jeu. */
void OnWritten(const FString& ContentRelativePath, const FString& Text);
/** Vrai si -LPDataCacheVerify. */
bool IsVerifyEnabled() const { return bVerify; }
private:
struct FEntry
{
FDateTime Stamp;
int64 Size = 0;
FString Text;
};
void LoadAll();
static FString MakeKey(const FString& ContentRelativePath);
TMap<FString, FEntry> Entries; // cle : chemin relatif a Content, en minuscules, separateurs '/'
TSet<FString> WrittenBeforeReady; // ecrits par SaveTxt avant la fin du chargement : lus sur le disque
std::atomic<bool> bReady{false};
bool bStarted = false;
bool bDisabled = false;
bool bVerify = false;
};

View file

@ -1,13 +1,18 @@
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. // Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "ModdingExtra.h" #include "ModdingExtra.h"
#include "LPDataCache.h"
#include "Misc/CommandLine.h"
#define LOCTEXT_NAMESPACE "FModdingExtraModule" #define LOCTEXT_NAMESPACE "FModdingExtraModule"
void FModdingExtraModule::StartupModule() void FModdingExtraModule::StartupModule()
{ {
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module // Portage 5.5, couche de donnees : chargement de Modules en arriere-plan des le demarrage (pas dans les commandlets).
if (!IsRunningCommandlet())
{
FLPDataCache::Get().StartAsync();
}
} }
void FModdingExtraModule::ShutdownModule() void FModdingExtraModule::ShutdownModule()

View file

@ -5,6 +5,7 @@
#include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/AssetRegistryModule.h"
#include "HAL/FileManager.h" #include "HAL/FileManager.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "LPDataCache.h"
#include "HAL/IConsoleManager.h" #include "HAL/IConsoleManager.h"
#include "HAL/PlatformTime.h" #include "HAL/PlatformTime.h"
#include "Misc/CoreDelegates.h" #include "Misc/CoreDelegates.h"
@ -152,7 +153,7 @@ namespace LPLoadTxtStats
static FAutoConsoleCommand Command(TEXT("ModdingExtra.LoadTxtStats"), TEXT("Bilan des lectures LoadTxt par type de fichier."), static FAutoConsoleCommand Command(TEXT("ModdingExtra.LoadTxtStats"), TEXT("Bilan des lectures LoadTxt par type de fichier."),
FConsoleCommandDelegate::CreateLambda([]() { Dump(TEXT("commande console")); })); FConsoleCommandDelegate::CreateLambda([]() { Dump(TEXT("commande console")); }));
static void Record(const FString& FileName, int32 Len, double Seconds) static void Record(const FString& FileName, int32 Len, double Seconds, bool bCached)
{ {
static bool bExitHooked = false; static bool bExitHooked = false;
if (!bExitHooked) if (!bExitHooked)
@ -165,6 +166,7 @@ namespace LPLoadTxtStats
LastCall = Now; LastCall = Now;
FString Ext = FPaths::GetExtension(FileName).ToLower(); FString Ext = FPaths::GetExtension(FileName).ToLower();
if (Ext.IsEmpty()) { Ext = TEXT("(sans)"); } if (Ext.IsEmpty()) { Ext = TEXT("(sans)"); }
if (bCached) { Ext += TEXT(" (cache)"); }
FEntry& E = ByExt.FindOrAdd(Ext); FEntry& E = ByExt.FindOrAdd(Ext);
++E.Calls; E.Bytes += Len; E.Seconds += Seconds; ++E.Calls; E.Bytes += Len; E.Seconds += Seconds;
bool bAlready = false; bool bAlready = false;
@ -176,8 +178,24 @@ namespace LPLoadTxtStats
bool UModdingExtraBPLibrary::LoadTxt(FString FileNameA, FString& SaveTextA) bool UModdingExtraBPLibrary::LoadTxt(FString FileNameA, FString& SaveTextA)
{ {
const double StartTime = FPlatformTime::Seconds(); const double StartTime = FPlatformTime::Seconds();
const bool bOk = FFileHelper::LoadFileToString(SaveTextA, *(FPaths::ProjectContentDir() + FileNameA)); // Couche de donnees (etapes 2 et 3) : fichiers de Modules servis depuis la memoire quand le cache est pret.
LPLoadTxtStats::Record(FileNameA, SaveTextA.Len(), FPlatformTime::Seconds() - StartTime); FLPDataCache& Cache = FLPDataCache::Get();
const bool bFromCache = Cache.TryGet(FileNameA, SaveTextA);
const bool bOk = bFromCache || FFileHelper::LoadFileToString(SaveTextA, *(FPaths::ProjectContentDir() + FileNameA));
LPLoadTxtStats::Record(FileNameA, SaveTextA.Len(), FPlatformTime::Seconds() - StartTime, bFromCache);
if (bFromCache && Cache.IsVerifyEnabled())
{
FString FromDisk;
FFileHelper::LoadFileToString(FromDisk, *(FPaths::ProjectContentDir() + FileNameA));
if (!FromDisk.Equals(SaveTextA, ESearchCase::CaseSensitive))
{
UE_LOG(LogModdingExtra, Error, TEXT("LoadTxt CACHE ECART | '%s' : %d car. en cache, %d sur le disque"), *FileNameA, SaveTextA.Len(), FromDisk.Len());
}
else
{
UE_LOG(LogModdingExtra, Verbose, TEXT("LoadTxt CACHE identique | '%s'"), *FileNameA);
}
}
if (!bOk) if (!bOk)
{ {
@ -210,6 +228,10 @@ namespace LPLoadTxtStats
bool UModdingExtraBPLibrary::SaveTxt(FString SaveTextB, FString FileNameB) bool UModdingExtraBPLibrary::SaveTxt(FString SaveTextB, FString FileNameB)
{ {
const bool bOk = FFileHelper::SaveStringToFile(SaveTextB, *(FPaths::ProjectContentDir() + FileNameB)); const bool bOk = FFileHelper::SaveStringToFile(SaveTextB, *(FPaths::ProjectContentDir() + FileNameB));
if (bOk)
{
FLPDataCache::Get().OnWritten(FileNameB, SaveTextB);
}
if (!bOk) if (!bOk)
{ {
UE_LOG(LogModdingExtra, Warning, TEXT("SaveTxt ECHEC | '%s' (%d car.) : ecriture impossible"), *FileNameB, SaveTextB.Len()); UE_LOG(LogModdingExtra, Warning, TEXT("SaveTxt ECHEC | '%s' (%d car.) : ecriture impossible"), *FileNameB, SaveTextB.Len());