27 KiB
Fiche U7-T6 — Niveau N3 — Modèle prévu opus-xhigh | utilisé Opus 5.5 (claude-opus-5-5) — Agent sous-agent N3 (lancé par l'orchestrateur) — Skills sk-protocol, debug-verification
Cartographie, lecture seule : peut-on savoir, au démarrage du jeu 5.8 et avant tout chargement de paquet, avec quelle
version du moteur un mod d'assets (plugin Mod, conteneurs .pak/.utoc/.ucas) a été cuit ?
Rien n'a été modifié hors de ce livrable et du scratchpad. Ni jeu, ni éditeur, ni build lancés.
1. Résultat
Oui, pour distinguer « cuit par 5.5 » de « cuit par 5.8 » : quatre champs binaires discriminent, mesurés sur les octets réels de 7 conteneurs (4 mods 5.5, le jeu 5.5 DC4, le jeu 5.8 Development). Aucun n'est refusé par le moteur 5.8 au montage, ce qui explique T4 : tout est accepté, puis le chargeur Zen plante sur le premier paquet.
| Champ discriminant | 5.5 (mods et jeu) | 5.8 (jeu) |
|---|---|---|
Version du .pak (FPakInfo::Version) |
11 | 12 |
Version de l'en-tête de conteneur IoStore (EIoContainerHeaderVersion, dans le .ucas) |
4 | 5 |
Version de AssetRegistry.bin (FAssetRegistryVersion) |
17 | 24 |
| Disposition de l'en-tête de paquet Zen sans version (marqueur structurel) | 84+20+70+6 mods et 8876 jeu : 100 % « 5.5 » | 8915 : 100 % « 5.8 » |
Ne discriminent pas : version du TOC .utoc (8 des deux côtés), bibliothèque de shaders (absente des mods), .uplugin
(aucun champ moteur). Piste retenue (§ 5) : version de AssetRegistry.bin + version de l'en-tête de conteneur,
toutes deux comparées aux constantes du moteur en cours (LatestVersion / Latest), lues par l'API publique de
Core/CoreUObject depuis ULPModInfoSubsystem::Initialize. Coût : quelques kilo-octets lus par mod.
Limite principale : ces champs sont des versions de format ; ils ne changent pas à chaque version du moteur. Ils
séparent 5.5 de 5.8 (établi), pas forcément deux versions futures (voir § 7). La garde MODULE_GAME_VERSION (T5)
reste nécessaire.
2. Matériel et méthode
- Sources : UE 5.8
D:\Game\Launcher\Engine\UE_5.8\Engine\Sourceet UE 5.5D:\Game\Launcher\Engine\UE_5.5\Engine\Source(présentes : Runtime, Developer, Programs ; chemins cités ci-dessous relatifs àEngine/Source). - Conteneurs lus (ouverture
rbseulement) :- mod 5.5 de T4 :
D:\Game\LifePlay_Build_5.8_U7T4\Windows\LifePlay\Mods\lpr_anim_quaternius\Content\Paks\Windows\lpr_anim_quaterniusLifePlay-Windows.{pak,utoc,ucas}; - mods 5.5 publiés :
D:\Game\LifePlay_Publication\Mods\{lpr_anim_quaternius-1.0-for-LifePlay-2026.10.1.6, lpr_anim_freeanims-1.0-for-LifePlay-2026.10.1.6, lpr_hair_pocolov-1.0-for-LifePlay-2026.10.4, LPHairSample-for-LifePlay-2026.10.4}\LifePlay\Mods\<id>\Content\Paks\Windows\(dossiersprecedent_*exclus) ; - jeu 5.5 :
D:\Game\LifePlay_Build_5.5_DC4\Windows\LifePlay\Content\Paks\LifePlay-Windows.*; - jeu 5.8 :
D:\Game\LifePlay_Build_5.8_Development\Windows\LifePlay\Content\Paks\LifePlay-Windows.*.
- mod 5.5 de T4 :
- Scripts (Python 3.14,
-I) dans%USERPROFILE%\AppData\Local\Temp\claude\D--Game-LifePlay-master-5-5\ab8918a1-261f-45b4-904a-678ecfce98f7\scratchpad\t6\:ue_containers.py(lecteur.pakv11/v12,.utoc,.ucas),cityhash.py(portage deRuntime/Core/Private/Hash/CityHash.cpp),m1_headers.py,m2_container_header.py,m3_pak_list.py,m4_ar.py,m5_zen.py,m6_all.py(relevé complet). Décompression Oodle paroo2core.dlllivrée avec le moteur (UE_5.8\Engine\Binaries\DotNET\AutomationTool\AutomationScripts\BuildGraph\net10.0\runtimes\win-x64\native\oo2core.dll). Seules sorties écrites : copies extraites desAssetRegistry.bin(scratchpad\t6\outAR_*.bin). - Rejouer :
cd <scratchpad>\t6 ; python -I m6_all.py ; python -I m5_zen.py 100000 ; python -I m2_container_header.py ; python -I m1_headers.py.
2 bis. Recherche web (consigne de l'orchestrateur, 2026-10-09)
Contenu lu = donnée. Résultat : aucune source ne donne de table « version d'UE → versions de conteneur » pour 5.6/5.7/5.8, ni de méthode documentée pour dater un paquet cuit sans version ; les sources confirment les enums relevés localement.
| Source | Ce qu'elle apporte | Niveau |
|---|---|---|
| Documentation API Epic, page « Unreal Engine 5.8 Documentation » : https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Core/EIoContainerHeaderVersion | EIoContainerHeaderVersion en 5.8 : SoftPackageReferences = 4, SoftPackageReferencesOffset = 5, Latest = LatestPlusOne - 1 (= 5), en-tête Runtime/Core/Public/IO/IoContainerHeader.h |
établi (concorde avec la source locale et les octets) |
Documentation API Epic FPakInfo : https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/PakFile/FPakInfo_2 |
PakFile_Version_Utf8PakDirectory = 12 après Fnv64BugFix |
établi (concorde) |
CUE4Parse (bibliothèque de FModel), FIoContainerHeader.cs : https://raw.githubusercontent.com/FabianFG/CUE4Parse/master/CUE4Parse/UE4/IO/Objects/FIoContainerHeader.cs |
même enum jusqu'à 5 ; la version est lue dans le fichier ; pour le reste du format, l'outil se fie à la version de jeu choisie par l'utilisateur (Ar.Game >= GAME_UE5_0 …) : les outils de référence ne déduisent pas la version du moteur d'un paquet sans version |
établi (enum) ; estimé (pratique des outils) |
CUE4Parse, FPakInfo.cs : https://raw.githubusercontent.com/FabianFG/CUE4Parse/master/CUE4Parse/UE4/Pak/Objects/FPakInfo.cs |
versions de .pak au-delà de 12 : 13 SortedDirectoryIndex, 14 PakchunkIndex, 15 EncryptionMethod (lues seulement si Ar.Game >= GAME_UE6_0) ; aucune table 5.x → 12 |
estimé (bibliothèque tierce) : la version de .pak continue de bouger après 5.8 |
| repak (trumank) : https://github.com/trumank/repak | table moteur → version de .pak arrêtée à « 4.26-5.3 : 10 » et 11 sans moteur ; rien sur 12 ni 5.6-5.8 |
ne tranche pas |
| mod.io, dépannage UGC Unreal : https://docs.mod.io/unreal/modio-ugc/troubleshooting | la version du moteur qui construit l'UGC doit être celle du projet hôte ; le .pak doit contenir AssetRegistry.bin |
concorde avec T4 et UA-MODS S11 |
Requêtes sans résultat utile : moment d'introduction de Utf8PakDirectory et de SoftPackageReferencesOffset (5.6 ou
5.7), outil retoc, détection de version d'un paquet sans version. Conséquence : la méthode retenue (§ 5) ne s'appuie
sur aucune table externe, seulement sur l'égalité avec les constantes du moteur qui exécute le jeu.
3. Tableau des pistes
« Moment » : quand la valeur est lisible. Le montage des mods a lieu au PreInit (LogPluginManager: Mounting Mod plugin,
T4/T4_LifePlay.log:239-244), bien avant UGameInstance::Init (lignes LogLifePlay: … Modules, :922-931) et avant
le premier chargement d'un paquet du mod (LoadMap → FlushAsyncLoading, plantage T4).
| # | Champ | Source 5.5 | Source 5.8 | Valeur 5.5 (mesurée) | Valeur 5.8 (mesurée) | Discrimine ? | API runtime (Shipping) | Moment |
|---|---|---|---|---|---|---|---|---|
| P1 | FIoStoreTocHeader::Version (octet 16 du .utoc) |
Runtime/Core/Internal/IO/IoStore.h:22-35 (enum), :40-66 (en-tête, Version :45) |
Runtime/Core/Internal/IO/IoStore.h:25-38, :43-69 (Version :48) ; contrôle Runtime/Core/Private/IO/IoStore.cpp:1313-1321 (refus si < DirectoryIndex ou > Latest) |
8 (4 mods + jeu DC4) | 8 (jeu) | Non : enum identique, Latest = ReplaceIoChunkHashWithIoHash = 8 des deux côtés |
En-tête dans Core/Internal : non incluable par un module de projet (Programs/UnrealBuildTool/Configuration/UEBuildModule.cs:736-740, portée différente) ; lecture binaire directe possible |
Toujours (fichier sur disque) |
| P2 | EIoContainerHeaderVersion (2e uint32 du bloc ContainerHeader dans le .ucas, après la signature 0x496f436e) |
Runtime/Core/Public/IO/IoContainerHeader.h:85-95 (Latest = SoftPackageReferences = 4) ; chargeur 5.5 : refus si != Latest (Runtime/Core/Private/IO/IoContainerHeader.cpp:50-56) |
Runtime/Core/Public/IO/IoContainerHeader.h:96-107 (Latest = SoftPackageReferencesOffset = 5) ; chargeur 5.8 : accepte >= NoExportInfo (3), branche dédiée à 4 (IoContainerHeader.cpp:58-65, 86-89) ; version non conservée (variable locale :58) |
4 (octets 6e436f49 04000000, 4 mods + jeu DC4) |
5 (octets 6e436f49 05000000) |
Oui | Publique : FIoContainerId::FromName (Core/Public/IO/IoContainerId.h:25), CreateContainerHeaderChunkId (Core/Public/IO/IoChunkId.h:214), FIoDispatcher::Get().NewBatch() / FIoBatch::Read / IssueAndTriggerEvent (Core/Public/IO/IoDispatcher.h:225-241, 296-327), FIoContainerHeader::Signature (IoContainerHeader.h:113). Le bloc est toujours non compressé (bForceUncompressed = true : 5.5 Developer/IoStoreUtilities/Private/IoStoreUtilities.cpp:5357-5359, 5.8 :5384-5386 ; mesuré : méthode None dans les 7 conteneurs) : lecture directe aussi possible |
Par FIoDispatcher : après montage (PreInit). Par lecture directe : toujours |
| P3 | FPakInfo::Version (pied de .pak, 221 octets de fin ; Magic 0x5A6F12E1 puis Version) |
Runtime/PakFile/Public/IPlatformFilePak.h:139-154 (Latest = Fnv64BugFix = 11) |
Runtime/PakFile/Public/IPlatformFilePak.h:164-180 (Latest = Utf8PakDirectory = 12) ; écrit avec Latest (Developer/PakFileUtilities/Private/PakFileUtilities.cpp:3250) ; lecture des anciennes versions prévue (IPlatformFilePak.h:241-290) ; noms de fichiers UTF-8 sans zéro final en v12 (Runtime/PakFile/Private/PakFile.cpp:726-735, constaté au décodage) |
11 (octets e1126f5a 0b000000) |
12 (octets e1126f5a 0c000000) |
Oui | FPakPlatformFile::FindFileInPakFiles(Filename, &PakFile) (public, IPlatformFilePak.h:2281, section public: :2087) puis FPakFile::GetInfo().Version (:1314, section public: :994) ; demande la dépendance PakFile dans LifePlay.Build.cs (absente aujourd'hui : Source/LifePlay/LifePlay.Build.cs:17,20,34,38). Ou lecture binaire des 221 derniers octets |
Après montage (API) ; toujours (lecture directe) |
| P4 | FAssetRegistryVersion de Mods/<id>/AssetRegistry.bin (GUID 717F9EE7-E9B0493A-88B39132-1B388107 + int32) |
Runtime/CoreUObject/Public/AssetRegistry/AssetData.h:75-107 (LatestVersion = AssetPackageDataHasExtension = 17) |
AssetData.h:74-113 (LatestVersion = MemoryMappedTagDataStore = 24) ; GUID Runtime/CoreUObject/Private/AssetRegistry/AssetData.cpp:37 ; lecture :969-992 |
17 (4 mods + jeu DC4) | 24 (jeu) | Oui | FAssetRegistryVersion::SerializeVersion(FArchive&, Type&) (publique, AssetData.h:120) sur IFileManager::Get().CreateFileReader(Plugin->GetBaseDir() / "AssetRegistry.bin") : chemin et accès identiques à ceux du moteur (Runtime/AssetRegistry/Private/AssetRegistry.cpp:632-655, ActiveMountsLoader.cpp:411-421 : FindFileInPakFiles puis GetPlatformFile().OpenRead). Fichier présent dans les 4 mods (seul fichier de leur .pak) |
Après montage (le fichier est dans le .pak) |
| P5 | Bibliothèque de shaders | .ushaderbytecode : GShaderCodeArchiveVersion = 2 (Runtime/RenderCore/Private/ShaderCodeLibrary.cpp:69) ; IoStore : FIoStoreShaderCodeArchive::CurrentVersion = 1 (Runtime/RenderCore/Public/ShaderCodeArchive.h:689) |
.ushaderbytecode : 3 (Runtime/RenderCore/Private/ShaderLibrary/ShaderCodeLibraryUtilities.cpp:24) ; IoStore : 1 (ShaderCodeArchive.h:992) |
absente : aucun bloc de type 8/9 (ShaderCodeLibrary/ShaderCode) ni fichier *shader* dans les mods |
absente aussi dans le conteneur du jeu (seul GlobalShaderCache-PCD3D_SM5.bin dans le .pak) |
Non applicable : le projet ne partage pas le code des shaders (Config/DefaultGame.ini:34 bShareMaterialShaderCode=False). Si elle existait : IoStore 1 = 1 (ne discrimine pas) ; .ushaderbytecode 2 ≠ 3 |
— | — |
| P6 | Résumé de paquet Zen FZenPackageSummary (52 octets) puis ce qui suit |
Runtime/CoreUObject/Public/Serialization/AsyncLoading2.h:277-291 ; lecteur : table des noms juste après le résumé (Runtime/CoreUObject/Private/Serialization/ZenPackageHeader.cpp:27-37) ; écrivain Developer/IoStoreUtilities/Private/PackageStoreOptimizer.cpp:848-858 |
AsyncLoading2.h:302-322 (même taille ; CookedHeaderSize devenu _Unused) + FZenPackageCellOffsets (8 octets) avant la table des noms quand le paquet est sans version ou >= VERSE_CELLS (ZenPackageHeader.cpp:37-51, écrivain PackageStoreOptimizer.cpp:1168-1182) |
bHasVersioningInfo = 0 ; à l'octet 60 : HashVersion = 0xC1640000 (FNameHash::AlgorithmId, 5.5 Runtime/Core/Private/UObject/UnrealNames.cpp:858) ; 84/84, 20/20, 70/70, 6/6 paquets des mods, 8876/8876 du jeu DC4 |
bHasVersioningInfo = 0 ; 0xC1640000 à l'octet 68, et ExportMapOffset <= CellImport <= CellExport <= ExportBundleEntriesOffset ; 8915/8915 paquets |
Oui (marqueur de disposition, pas un numéro de version) ; aucun champ de version quand le paquet est cuit sans version | CreatePackageDataChunkId(FPackageId::FromName(...)) (IoChunkId.h:208, PackageId.h:28) + FIoBatch::Read(…, FIoReadOptions(0, 128), …) : lecture brute, sans instancier le paquet |
Après montage, avant chargement |
| P7 | .uplugin (EngineVersion) |
— | Champ lu (Runtime/Projects/Private/PluginDescriptor.cpp:304) mais vérifié seulement #if !IS_MONOLITHIC (Runtime/Projects/Private/PluginManager.cpp:1841-1865, :2688-2706) ; un jeu TargetType.Game est monolithique par défaut (Programs/UnrealBuildTool/Configuration/Rules/TargetRules.cs:2688) |
.uplugin du mod : aucun champ moteur (FileVersion, Version, VersionName, FriendlyName, Description, Category, CreatedBy, CanContainContent, IsBetaVersion, Installed, CreatedByURL) |
pas de mod 5.8 | Non aujourd'hui (champ absent ; BuildMod.ps1:270-276 n'écrit que VersionName et CreatedByURL) |
IPlugin::GetDescriptor().EngineVersion (lisible par le jeu, ignoré par le moteur en jeu empaqueté) |
Toujours |
Détail des identifiants de conteneur : l'identifiant du TOC vaut FIoContainerId::FromName(<nom de base du .utoc>)
(hachage CityHash64 du nom en minuscules, UTF-16 : Runtime/Core/Private/IO/IoContainerId.cpp:13-23). Vérifié sur les
6 conteneurs distincts : lpr_anim_quaterniusLifePlay-Windows → bbd89afd2edf8ea8 (= TOC = Id= du journal T4 l. 240),
LifePlay-Windows → 143145bb72a99118, pocolov 50c08c670938e99a, freeanims 6bd793052541c7fe, LPHairSample
c302a535cfbedbac : 6/6 égaux. Le jeu peut donc construire l'identifiant du bloc ContainerHeader par l'API publique,
sans lire la structure interne du .utoc.
4. Le plantage T4 expliqué octet par octet (piste P6)
- 5.5 écrit : résumé (52 o) puis table des noms
Num,NumStringBytes,HashVersion(8 o), … Le mod, premier paquet : octets 52-75 =02000000 44000000 | 000064c1 00000000 | 85404d95…(2 noms, 68 octets de chaînes,0xC1640000). - 5.8, paquet sans version : lit d'abord
CellImportMapOffset/CellExportMapOffset(8 o), donc prendNum = 0xC1640000,NumStringBytes = 0, puis réserve8*Num + 2*Num + 0octets en arithmétique 32 bits (UnrealNames.cpp:5331-5353, 5.8) :10 × 0xC1640000 mod 2^32 = 0x8DE80000 = 2380791808. - Le journal T4 dit :
Trying to resize TArray to an invalid size of 2380791808, pileFNameBatchLoader::Read ← FNameMap::Load ← FZenPackageHeader::MakeView. Valeur identique ; elle est la même pour tout paquet 5.5 sans version (le script la calcule pour chaque conteneur 5.5 :sim58_alloc_if_55_layout = 2380791808). - Le chargeur 5.8 n'a aucun contrôle de plage de version de paquet : il applique les versions du paquet si présentes,
sinon celles du moteur (
Runtime/CoreUObject/Private/Serialization/AsyncLoading2.cpp:8464-8473;ZenVersionlu mais jamais testé,:164-169).
5. Recommandation
5.1 Piste retenue
P4 (version de AssetRegistry.bin) comme signal principal, P2 (version de l'en-tête de conteneur) comme second
signal ; règle : compatible si et seulement si les deux égalent les constantes du moteur en cours.
Arguments :
- Lisibles avant tout chargement de paquet, à l'initialisation du GameInstance (mods montés au PreInit).
- API publiques de
CoreetCoreUObjectuniquement, déjà dépendances deLifePlay(LifePlay.Build.cs:17) ; aucune structure deCore/Internalrecopiée ; pas de dépendancePakFileà ajouter. - Auto-étalonnées :
FAssetRegistryVersion::LatestVersionetEIoContainerHeaderVersion::Latestsont les valeurs que le cuiseur du même moteur écrit (AssetData.cpp:969-992;IoContainerHeader.cpp:58-59en écriture). Pas de table 5.5/5.8 codée en dur ; la garde suit le moteur lors d'une future montée de version. - Indépendantes : l'une vient du registre du cuiseur, l'autre de l'écrivain IoStore ; P4 change le plus souvent (17 → 24, sept crans entre 5.5 et 5.8), P2 rarement (4 → 5).
- Coût mesuré en volume : P4 lit le premier bloc de
AssetRegistry.bin(mod quaternius : 29 409 octets décompressés depuis 4 318) ; P2 lit 8 octets d'un bloc non compressé (4 752 octets pour quaternius). Temps : estimé inférieur à la milliseconde par mod sur disque local (quelques ms sur le disque D: en USB) ; non mesuré en C++.
P3 (pak 11/12) reste un troisième signal possible mais demande la dépendance PakFile ou une lecture binaire du pied ;
P6 (disposition Zen) est la preuve directe de la cause du plantage 5.5 → 5.8, utile en diagnostic, mais elle n'est pas
générale (elle dépend du passage à VERSE_CELLS et des paquets sans version).
5.2 Esquisse C++ (non compilée, à valider par une fiche IMPL + BUILD)
Où : ULPModInfoSubsystem::Initialize (Source/LifePlay/LPModInfoSubsystem.cpp:29), qui lit déjà les .lpmod et dont
dépendent ULPModDailiesSubsystem (LPModDailies.cpp:48, InitializeDependency) et ULPHairstyleSubsystem. Calculer un
verdict par plugin EPluginType::Mod (plugin = module, convention T5), le mettre en cache, et l'ajouter au prédicat
utilisé en LPModDailies.cpp:65 et LPHairstyleSubsystem.cpp:254 (et dans la fenêtre d'avertissement, Tick :100).
// LPModCookCheck.h / .cpp (module LifePlay) — includes : "AssetRegistry/AssetData.h", "HAL/FileManager.h",
// "HAL/Event.h", "IO/IoDispatcher.h", "IO/IoChunkId.h", "IO/IoContainerId.h", "IO/IoContainerHeader.h",
// "Interfaces/IPluginManager.h", "Misc/Paths.h"
struct FLPModCookInfo
{
int32 AssetRegistryVersion = -1; // -1 : illisible
int32 ContainerHeaderVersion = -1; // -1 : illisible
bool IsKnown() const { return AssetRegistryVersion >= 0 && ContainerHeaderVersion >= 0; }
bool MatchesEngine() const
{
return AssetRegistryVersion == static_cast<int32>(FAssetRegistryVersion::LatestVersion)
&& ContainerHeaderVersion == static_cast<int32>(EIoContainerHeaderVersion::Latest);
}
};
FLPModCookInfo LPReadModCookInfo(const IPlugin& Plugin)
{
FLPModCookInfo Info;
// P4 : registre du plugin, dans son .pak (meme chemin que le moteur, AssetRegistry.cpp:642).
const FString ArPath = Plugin.GetBaseDir() / TEXT("AssetRegistry.bin");
if (TUniquePtr<FArchive> Ar(IFileManager::Get().CreateFileReader(*ArPath)); Ar)
{
FAssetRegistryVersion::Type Version;
if (FAssetRegistryVersion::SerializeVersion(*Ar, Version) && !Ar->IsError())
{
Info.AssetRegistryVersion = static_cast<int32>(Version);
}
}
// P2 : en-tete de conteneur IoStore ; identifiant = FromName(nom de base du .utoc) (verifie sur 6 conteneurs).
TArray<FString> Tocs;
IFileManager::Get().FindFilesRecursive(Tocs, *Plugin.GetContentDir(), TEXT("*.utoc"), true, false);
if (Tocs.Num() == 1 && FIoDispatcher::IsInitialized())
{
const FIoChunkId Chunk = CreateContainerHeaderChunkId(FIoContainerId::FromName(FName(*FPaths::GetBaseFilename(Tocs[0]))));
if (FIoDispatcher::Get().DoesChunkExist(Chunk))
{
FIoBatch Batch = FIoDispatcher::Get().NewBatch();
FIoRequest Request = Batch.Read(Chunk, FIoReadOptions(0, 8), IoDispatcherPriority_High);
FEventRef Done;
Batch.IssueAndTriggerEvent(Done.Get());
Done->Wait();
const FIoBuffer* Buffer = Request.GetResult(); // nul si echec (pas de GetResultOrDie : pas de Fatal)
if (Buffer && Buffer->DataSize() >= 8)
{
const uint32* Words = reinterpret_cast<const uint32*>(Buffer->Data());
if (Words[0] == static_cast<uint32>(FIoContainerHeader::Signature))
{
Info.ContainerHeaderVersion = static_cast<int32>(Words[1]);
}
}
}
}
return Info;
}
Dans ULPModInfoSubsystem::Initialize : pour chaque IPluginManager::Get().GetEnabledPluginsWithContent() de type
Mod, CookInfo.Add(Plugin->GetName(), LPReadModCookInfo(*Plugin)), journal une ligne par mod (AR=%d/%d CH=%d/%d).
Prédicat proposé : IsModuleCookCompatible(Module, FString* OutReason) = faux si IsKnown() && !MatchesEngine().
Le cas « illisible » (!IsKnown(), par exemple mod sans conteneur, fichier absent) est une décision utilisateur :
bloquer par prudence, ou charger avec avertissement comme la garde T5 le fait pour un module sans MODULE_GAME_VERSION.
Variante diagnostic (P6, facultative) : lire 128 octets du premier paquet du mod (nom donné par le registre,
CreatePackageDataChunkId(FPackageId::FromName(PackageName))) et signaler « disposition 5.5 » si
bHasVersioningInfo == 0 && *(uint64*)(Data + 60) == 0xC1640000.
5.3 Pour les futurs mods (options, sans décision)
- (a) Rien de plus : le premier mod cuit par 5.8 doit donner
pak 12 / TOC 8 / CH 5 / AR 24 / disposition 5.8(estimé : mêmes outils que le jeu 5.8, même commande UATBuildMod.ps1:235-pak -iostore -compressed). Essai d'acceptation :python -I m6_all.pyaprès ajout de son chemin, avant publication. - (b) Tampon écrit par BuildMod : ajouter au
.upluginlivré, dans le bloc qui écrit déjàVersionName/CreatedByURL(BuildMod.ps1:270-276),EngineVersion= version du moteur qui a cuit (lue dansEngine/Build/Build.versiondu moteur utilisé). Le moteur ne l'applique pas en jeu monolithique (P7) ; le jeu le lit parIPlugin::GetDescriptor().EngineVersionet le compare àFEngineVersion::Current(). Détecte aussi un changement de version sans changement de format ; texte modifiable à la main, commeMODULE_GAME_VERSION. - (c) Cuisson versionnée :
-VersionCookedContent(metUnversionedCookedContent = false,Programs/AutomationTool/AutomationUtils/ProjectParams.cs:852, 2101, donc pas de-unversioned,Scripts/CookCommand.Automation.cs:51-63). Chaque paquet porte alorsFZenPackageVersioningInfo(version d'objet UE5 : estimé 1013 pour 5.5, 1018 pour 5.8, par comptage deRuntime/Core/Public/UObject/ObjectVersion.h:47-93(5.5) et:47-108(5.8)). Ce que fait 5.8 d'un tel paquet : l'en-tête plus ancien est lu correctement (brancheVERSE_CELLS,ZenPackageHeader.cpp:38-47), puis les exports sont lus avec les versions du paquet ; aucun refus propre pour une version plus ancienne ni plus récente (AsyncLoading2.cpp:8464-8473). Donc : un champ exact de plus, mais pas une garde ; issue d'un paquet 5.5 versionné chargé par 5.8 incertaine (essai : cuire un mod 5.5 avec-VersionCookedContentet le monter en 5.8). Coût : en-têtes un peu plus gros ; incompatible avec l'incrémental « legacy iterative » (CookCommand.Automation.cs:53-58).
6. Où brancher sans rien casser (rappel de l'ordre établi)
- PreInit : montage des conteneurs des mods, en-tête de conteneur lu et accepté par 5.8 (
T4_LifePlay.log:239-244,LogFilePackageStore: Mounting container: Id=bbd89afd2edf8ea8, Order=0, NumPackages=84). - Module AssetRegistry :
AssetRegistry.bindu mod chargé sans erreur (version 17 lue par 5.8 ; clips trouvés,T4_LifePlay.log:929). UGameInstance::Init:ULPModInfoSubsystempuisULPModDailiesSubsystem/ULPHairstyleSubsystem(:922-931) ← la garde binaire s'insère ici.LoadMap→ premier paquet du mod → plantage (T4).
7. Limites
- Versions de format, pas version du moteur : P2 et P4 n'augmentent que lorsqu'un format change. Valeurs de 5.6 et 5.7 inconnues (sources locales absentes, recherche web sans résultat, § 2 bis) : la règle sépare 5.5 de 5.8 (établi), pas forcément deux versions futures (5.8 contre 5.9 si ni le registre ni l'en-tête de conteneur ne changent). L'option 5.3 (b) couvre ce trou.
- Schéma des classes : un paquet sans version est lu avec la liste de propriétés de la classe qui le charge
(
Runtime/CoreUObject/Private/Serialization/UnversionedPropertySerialization.h:11-12). Un changement de classe du jeu (ou d'une classe moteur sans changement de format de conteneur) n'est pas détecté par ces champs ; la gardeMODULE_GAME_VERSION(T5) reste nécessaire (estimé). - Échecs au montage : la garde au GameInstance ne protège que du chargement de paquets. Un format de conteneur futur qui ferait échouer le montage lui-même (PreInit) échapperait au jeu (incertain ; non observé pour 5.5 → 5.8).
- Mod 5.8 non mesuré : les valeurs « 5.8 » viennent du conteneur du jeu, pas d'un mod (aucun mod 5.8 n'existe encore).
- Code non exécuté : l'esquisse n'est ni compilée ni lancée ; la lecture du bloc
ContainerHeaderparFIoDispatcheraprès montage est déduite de l'API (le moteur lit ce bloc au montage), pas observée.
8. Niveaux de preuve
- Établi (source et octets) : P1 ne discrimine pas (8/8) ; P2 4 contre 5 ; P3 11 contre 12 ; P4 17 contre 24 ;
P6 disposition 5.5 sur 100 % des 9 056 paquets 5.5 (180 des 4 mods publiés + 8 876 du jeu, plus la copie T4 de quaternius, 84 ;
m5_zen.py,m6_all.py) et 5.8 sur 100 % des 8 915 paquets 5.8 ; P5 absente (aucun bloc 8/9,bShareMaterialShaderCode=False) ; P7 aucun champ moteur ; le blocContainerHeaderest non compressé ; identifiant de conteneur =FromName(nom de base)(6/6) ; aucun refus de 5.8 au montage pour P1-P4 (code et journal T4) ; valeur du plantage T4 retrouvée exactement (2 380 791 808) ; API citées publiques (Core/Public,CoreUObject/Public,PakFile/Publicen sectionpublic:),Core/Internalinaccessible à un module de projet. - Estimé : un mod cuit par 5.8 portera les valeurs du jeu 5.8 (mêmes outils) ; coût en temps (< 1 ms par mod) ;
versions d'objet UE5 1013/1018 (comptage d'énumération, pas d'octets : paquets sans version) ; nécessité de garder
MODULE_GAME_VERSION(schéma des classes). - Incertain : valeurs de 5.6/5.7 et des moteurs futurs ; comportement de 5.8 devant un paquet 5.5 versionné ; bon fonctionnement de l'esquisse au runtime (essai : build Development 5.8 avec la garde, mod 5.5 de T4 attendu « AR=17/24 CH=4/5 → ignoré », puis mod 5.8 attendu « AR=24/24 CH=5/5 → accepté »).
9. Rejouer la mesure
cd %USERPROFILE%\AppData\Local\Temp\claude\D--Game-LifePlay-master-5-5\ab8918a1-261f-45b4-904a-678ecfce98f7\scratchpad\t6
python -I m6_all.py # pak / utoc / en-tete de conteneur / registre / disposition Zen, 7 conteneurs
python -I m5_zen.py 100000 # disposition Zen detaillee + simulation du lecteur 5.8 (2380791808)
python -I m2_container_header.py
python -I m1_headers.py