LifePlay-Revival/Docs/Migration/etat/U58/U7/U7-T6-CART.md
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

27 KiB
Raw Blame History

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\Source et UE 5.5 D:\Game\Launcher\Engine\UE_5.5\Engine\Source (présentes : Runtime, Developer, Programs ; chemins cités ci-dessous relatifs à Engine/Source).
  • Conteneurs lus (ouverture rb seulement) :
    • 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\ (dossiers precedent_* 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.*.
  • 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 .pak v11/v12, .utoc, .ucas), cityhash.py (portage de Runtime/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 par oo2core.dll livré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 des AssetRegistry.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 prend Num = 0xC1640000, NumStringBytes = 0, puis réserve 8*Num + 2*Num + 0 octets 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, pile FNameBatchLoader::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 ; ZenVersion lu 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 :

  1. Lisibles avant tout chargement de paquet, à l'initialisation du GameInstance (mods montés au PreInit).
  2. API publiques de Core et CoreUObject uniquement, déjà dépendances de LifePlay (LifePlay.Build.cs:17) ; aucune structure de Core/Internal recopiée ; pas de dépendance PakFile à ajouter.
  3. Auto-étalonnées : FAssetRegistryVersion::LatestVersion et EIoContainerHeaderVersion::Latest sont les valeurs que le cuiseur du même moteur écrit (AssetData.cpp:969-992 ; IoContainerHeader.cpp:58-59 en é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.
  4. 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).
  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 UAT BuildMod.ps1:235 -pak -iostore -compressed). Essai d'acceptation : python -I m6_all.py après ajout de son chemin, avant publication.
  • (b) Tampon écrit par BuildMod : ajouter au .uplugin livré, dans le bloc qui écrit déjà VersionName/CreatedByURL (BuildMod.ps1:270-276), EngineVersion = version du moteur qui a cuit (lue dans Engine/Build/Build.version du moteur utilisé). Le moteur ne l'applique pas en jeu monolithique (P7) ; le jeu le lit par IPlugin::GetDescriptor().EngineVersion et le compare à FEngineVersion::Current(). Détecte aussi un changement de version sans changement de format ; texte modifiable à la main, comme MODULE_GAME_VERSION.
  • (c) Cuisson versionnée : -VersionCookedContent (met UnversionedCookedContent = false, Programs/AutomationTool/AutomationUtils/ProjectParams.cs:852, 2101, donc pas de -unversioned, Scripts/CookCommand.Automation.cs:51-63). Chaque paquet porte alors FZenPackageVersioningInfo (version d'objet UE5 : estimé 1013 pour 5.5, 1018 pour 5.8, par comptage de Runtime/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 (branche VERSE_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 -VersionCookedContent et 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)

  1. 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).
  2. Module AssetRegistry : AssetRegistry.bin du mod chargé sans erreur (version 17 lue par 5.8 ; clips trouvés, T4_LifePlay.log:929).
  3. UGameInstance::Init : ULPModInfoSubsystem puis ULPModDailiesSubsystem / ULPHairstyleSubsystem (:922-931) ← la garde binaire s'insère ici.
  4. 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 garde MODULE_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 ContainerHeader par FIoDispatcher aprè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 bloc ContainerHeader est 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/Public en section public:), Core/Internal inaccessible à 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