265 lines
27 KiB
Markdown
265 lines
27 KiB
Markdown
|
|
# 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).
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// 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
|
|||
|
|
```
|