Skip to content
160 changes: 139 additions & 21 deletions UAssetAPI/UAsset.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
Expand Down Expand Up @@ -154,6 +155,16 @@ public FGenerationInfo(int exportCount, int nameCount)
}
}

public class FAssetRegistryRecord
{
/// <summary> Path relative to the package. </summary>
public string Path;
/// <summary> Asset object name. </summary>
public string ClassName;
/// <summary> Asset registry tags. </summary>
public Dictionary<string, string> TagMap;
}

/// <summary>
/// Represents an Unreal Engine asset.
/// </summary>
Expand Down Expand Up @@ -253,6 +264,8 @@ public bool HasUnversionedProperties
/// </summary>
public bool IsFilterEditorOnly => PackageFlags.HasFlag(EPackageFlags.PKG_FilterEditorOnly);

public bool IsPreDependencyFormat => IsFilterEditorOnly || ObjectVersion < ObjectVersion.VER_UE4_ASSETREGISTRY_DEPENDENCYFLAGS;

[JsonIgnore]
internal volatile bool isSerializationTime = false;

Expand Down Expand Up @@ -1344,15 +1357,44 @@ internal ECustomVersionSerializationFormat CustomVersionSerializationFormat
public List<FString> SoftPackageReferenceList;

/// <summary>
/// Uncertain
/// Offset to dependencies. This only appears in uncooked asset.
/// </summary>
public long AssetRegistryDependencyDataOffset = -1;

/// <summary>
/// Asset registry data.
/// </summary>
public List<FAssetRegistryRecord> AssetRegistryRecords;

/// <summary>
/// Number of valid bits in `ImportBits'. This only appears in uncooked asset.
/// </summary>
public int ImportBitsCount = 0;

/// <summary>
/// Bits indicating if imports used in game are contained in import map. This only appears in uncooked asset.
/// </summary>
public BitArray ImportBits;

/// <summary>
/// Number of valid bits in `SoftPackageBits'. This only appears in uncooked asset.
/// </summary>
public int SoftPackageBitsCount = 0;

/// <summary>
/// Bits indicating if soft packages used in game are contained in soft package reference list. This only appears in uncooked asset.
/// </summary>
public byte[] AssetRegistryData;
public BitArray SoftPackageBits;

/// <summary>
/// Any bulk data that is not stored in the export map.
/// </summary>
public byte[] BulkData;

public byte[] AdditionalFiles;

public byte[] Trailer;

/// <summary>
/// Some garbage data that appears to be present in certain games (e.g. Valorant)
/// </summary>
Expand Down Expand Up @@ -1993,25 +2035,40 @@ public virtual void Read(AssetBinaryReader reader, int[] manualSkips = null, int
}
}

// AssetRegistryData
AssetRegistryData = [];
if (AssetRegistryDataOffset > 0)
{
AssetRegistryDependencyDataOffset = -1;
reader.BaseStream.Seek(AssetRegistryDataOffset, SeekOrigin.Begin);
/*
if (!IsPreDependencyFormat)
{
AssetRegistryDependencyDataOffset = reader.ReadInt64();
}

int numAssets = reader.ReadInt32();
AssetRegistryRecords = [];
for (int i = 0; i < numAssets; i++)
{
throw new NotImplementedException("Asset registry data is not yet supported. Please let me know if you see this error message");
FAssetRegistryRecord record = new FAssetRegistryRecord();

record.Path = reader.ReadString();
record.ClassName = reader.ReadString();

int tagNum = reader.ReadInt32();
record.TagMap = [];
for (int j = 0; j < tagNum; ++j)
{
string key = reader.ReadString();
string value = reader.ReadString();
record.TagMap.Add(key, value);
}
AssetRegistryRecords.Add(record);
}
*/

// For now: read binary data until next offset
int nextOffset = this.WorldTileInfoDataOffset;
if (this.PreloadDependencyOffset >= 0 && nextOffset <= 0) nextOffset = this.PreloadDependencyOffset;
if (SectionSixOffset > 0 && Exports.Count > 0 && nextOffset <= 0) nextOffset = (int)Exports[0].SerialOffset;
if (nextOffset <= 0) nextOffset = (int)this.BulkDataStartOffset;
AssetRegistryData = reader.ReadBytes(nextOffset - AssetRegistryDataOffset);
if (!IsPreDependencyFormat)
{
ImportBits = ReadBitArray(reader, out ImportBitsCount);
SoftPackageBits = ReadBitArray(reader, out SoftPackageBitsCount);
}
}
else
{
Expand All @@ -2035,12 +2092,18 @@ public virtual void Read(AssetBinaryReader reader, int[] manualSkips = null, int
SeaOfThievesGarbageData = null;
}

BulkData = [];
AdditionalFiles = [];
if (BulkDataStartOffset > 0 && reader.LoadUexp)
{
long before = reader.BaseStream.Position;
reader.BaseStream.Seek(BulkDataStartOffset, SeekOrigin.Begin);
BulkData = reader.ReadBytes((int)(reader.BaseStream.Length - BulkDataStartOffset));
bool hasPayload = PayloadTocOffset > 0;
long end = hasPayload ? PayloadTocOffset : reader.BaseStream.Length;
AdditionalFiles = reader.ReadBytes((int)(end - BulkDataStartOffset));
if (hasPayload)
{
Trailer = reader.ReadBytes((int)(reader.BaseStream.Length - reader.BaseStream.Position));
}
reader.BaseStream.Seek(before, SeekOrigin.Begin);
}

Expand Down Expand Up @@ -2236,6 +2299,22 @@ public virtual void Read(AssetBinaryReader reader, int[] manualSkips = null, int
}
}

private static BitArray ReadBitArray(AssetBinaryReader reader, out int bitCount)
{
bitCount = reader.ReadInt32();
int length = ComputeBitArrayDataLenth(bitCount);
return new BitArray(reader.ReadBytes(length));
}
private static int BitsToNumWords(int bitCount)
{
return (int)Math.Ceiling(bitCount / 32.0);
}

private static int ComputeBitArrayDataLenth(int bitCount)
{
return sizeof(Int32) * BitsToNumWords(bitCount);
}

/// <summary>
/// Serializes the initial portion of the asset from memory.
/// </summary>
Expand Down Expand Up @@ -2691,13 +2770,36 @@ public virtual MemoryStream WriteData()
{
this.AssetRegistryDataOffset = (int)writer.BaseStream.Position;

/*writer.Write(this.AssetRegistryData.Count);
for (int i = 0; i < this.AssetRegistryData.Count; i++)
if (!IsPreDependencyFormat)
{
throw new NotImplementedException("Asset registry data is not yet supported. Please let me know if you see this error message");
}*/
writer.Write(AssetRegistryDependencyDataOffset);
}

writer.Write(AssetRegistryData);
writer.Write(AssetRegistryRecords.Count);
foreach (FAssetRegistryRecord record in AssetRegistryRecords)
{
writer.Write(record.Path);
writer.Write(record.ClassName);

writer.Write(record.TagMap.Count);
foreach (KeyValuePair<string, string> pair in record.TagMap)
{
writer.Write(pair.Key);
writer.Write(pair.Value);
}
}

if (!IsPreDependencyFormat)
{
AssetRegistryDependencyDataOffset = writer.BaseStream.Position;
writer.BaseStream.Seek(AssetRegistryDataOffset, SeekOrigin.Begin);
writer.Write(AssetRegistryDependencyDataOffset);
writer.BaseStream.Seek(AssetRegistryDependencyDataOffset, SeekOrigin.Begin);

WriteBitArray(writer, ImportBitsCount, ImportBits);

WriteBitArray(writer, SoftPackageBitsCount, SoftPackageBits);
}
}
else
{
Expand Down Expand Up @@ -2803,7 +2905,12 @@ public virtual MemoryStream WriteData()
if (SeaOfThievesGarbageData != null && SeaOfThievesGarbageData.Length > 0) writer.Write(SeaOfThievesGarbageData);

this.BulkDataStartOffset = (int)writer.BaseStream.Position;
writer.Write(BulkData);
writer.Write(AdditionalFiles);
if (PayloadTocOffset > 0)
{
this.PayloadTocOffset = writer.BaseStream.Position;
writer.Write(Trailer);
}

// Rewrite Section 3
if (this.Exports.Count > 0)
Expand Down Expand Up @@ -2845,6 +2952,17 @@ public virtual MemoryStream WriteData()
return stre;
}

private static void WriteBitArray(AssetBinaryWriter writer, int count, BitArray bitArray)
{
writer.Write(count);
if (count > 0)
{
byte[] data = new byte[ComputeBitArrayDataLenth(count)];
bitArray.CopyTo(data, 0);
writer.Write(data);
}
}

/// <summary>
/// Serializes and writes an asset to two split streams (.uasset and .uexp) from memory.
/// </summary>
Expand Down