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:
parent
b982bd7d58
commit
d443257bf7
7 changed files with 391 additions and 6 deletions
|
|
@ -530,4 +530,28 @@ Conséquences pour la suite :
|
||||||
- **Étapes 2 et 3, réunies** (un fichier compilé lu une fois, puis `LoadTxt` servi depuis la mémoire) : environ −5 à
|
- **Étapes 2 et 3, réunies** (un fichier compilé lu une fois, puis `LoadTxt` servi depuis la mémoire) : environ −5 à
|
||||||
−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.
|
||||||
|
|
@ -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` |
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
277
Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.cpp
Normal file
277
Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.cpp
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue