Skip to content

Commit

Permalink
fix struct indexer by string for non-unversioned properties, remove n…
Browse files Browse the repository at this point in the history
…ame indexer from arrayproperty (what was i thinking?), extra code in docs
  • Loading branch information
atenfyr committed Aug 19, 2024
1 parent dd84ade commit e2cfb9d
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 57 deletions.
24 changes: 24 additions & 0 deletions UAssetAPI.Benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UAssetAPI.CustomVersions;
using UAssetAPI.ExportTypes;
using UAssetAPI.IO;
using UAssetAPI.Kismet.Bytecode;
using UAssetAPI.PropertyTypes.Objects;
using UAssetAPI.PropertyTypes.Structs;
using UAssetAPI.UnrealTypes;
using UAssetAPI.Unversioned;

Expand Down Expand Up @@ -358,6 +363,25 @@ public static void Run(string[] args)
new AssetBinaryWriter(testStrm, test).WriteNameBatch(test.HashVersion, (IList<FString>)test.GetNameMapIndexList());
Console.WriteLine(BitConverter.ToString(testStrm.ToArray()));
break;
case "abcd":
{
UAsset myAsset = new UAsset("C:\\my_asset.uasset", EngineVersion.VER_UE4_18);

// all StructExport exports can contain blueprint bytecode, let's pretend Export 1 is a StructExport
StructExport myStructExport = (StructExport)myAsset.Exports[0];

KismetExpression[] bytecode = myStructExport.ScriptBytecode;
if (bytecode != null) // bytecode may fail to parse, in which case it will be null and stored raw in ScriptBytecodeRaw
{
// KismetExpression has many child classes, one child class for each type of instruction
// as with PropertyData, you can access .RawValue for many instruction types, but you'll need to cast for other kinds of instructions to access specific fields
foreach (KismetExpression instruction in bytecode)
{
Console.WriteLine(instruction.Token.ToString() + ": " + instruction.RawValue.ToString());
}
}
}
break;
}
}

Expand Down
53 changes: 0 additions & 53 deletions UAssetAPI/PropertyTypes/Objects/ArrayPropertyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,59 +24,6 @@ public bool ShouldSerializeDummyStruct()
return Value.Length == 0;
}

/// <summary>
/// Gets or sets the value associated with the specified key. This operation loops linearly, so it may not be suitable for high-performance environments.
/// </summary>
/// <param name="key">The key associated with the value to get or set.</param>
public virtual PropertyData this[FName key]
{
get
{
if (Value == null) return null;

for (int i = 0; i < Value.Length; i++)
{
if (Value[i].Name == key) return Value[i];
}
return null;
}
set
{
if (Value == null) Value = [];
value.Name = key;

for (int i = 0; i < Value.Length; i++)
{
if (Value[i].Name == key)
{
Value[i] = value;
return;
}
}

var newValue = new PropertyData[Value.Length + 1];
Array.Copy(Value, newValue, Value.Length);
newValue[newValue.Length - 1] = value;
Value = newValue;
}
}

/// <summary>
/// Gets or sets the value associated with the specified key. This operation loops linearly, so it may not be suitable for high-performance environments.
/// </summary>
/// <param name="key">The key associated with the value to get or set.</param>
public virtual PropertyData this[string key]
{
get
{
return this[FName.DefineDummy(null, key)];
}
set
{
this[FName.DefineDummy(null, key)] = value;
}
}

public ArrayPropertyData(FName name) : base(name)
{
Value = [];
Expand Down
4 changes: 2 additions & 2 deletions UAssetAPI/PropertyTypes/Structs/StructPropertyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ public virtual PropertyData this[string key]
{
get
{
return this[FName.DefineDummy(null, key)];
return this[FName.FromString(Name?.Asset, key)];
}
set
{
this[FName.DefineDummy(null, key)] = value;
this[FName.FromString(Name?.Asset, key)] = value;
}
}

Expand Down
1 change: 1 addition & 0 deletions UAssetAPI/UnrealPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ public virtual Export this[FName key]
}
set
{
value.ObjectName = key;
for (int i = 0; i < Exports.Count; i++)
{
if (Exports[i].ObjectName == key)
Expand Down
7 changes: 5 additions & 2 deletions docs/src/guide/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Once we have entered Visual Studio, we must add a new reference to our UAssetAPI

Once you've referenced the UAssetAPI assembly in your project, you're ready to start parsing assets!

## Using UAssetAPI with UE4 Assets
## Using UAssetAPI with Unreal Assets
### Modifying a specific property

Every Unreal Engine 4 asset parsed with UAssetAPI is represented by the [UAsset](../api/uassetapi.uasset.md#constructors) class. The simplest way to construct a UAsset is to initialize it with the path to the asset on disk (note that if your asset has a paired .uexp file, both files must be located in the same directory, and the path should point to the .uasset file) and an [EngineVersion](../api/uassetapi.unrealtypes.engineversion.html#fields).
Expand All @@ -47,6 +47,9 @@ UAsset myAsset = new UAsset("C:\\plwp_6aam_a0.uasset", EngineVersion.VER_UE4_18)
// We want the 2nd export, so we reference the export at index 1.
// There are many types, but any export that has regular "tagged" data like you see as properties in UAssetGUI can be cast to a NormalExport, like this one.
NormalExport myExport = (NormalExport)myAsset.Exports[1];
// Alternatively, we can reference exports by ObjectName:
// NormalExport myExport = (NormalExport)myAsset.Exports["Default__plwp_6aam_a0_C"];
// we implement the general algorithm used by UAssetAPI here later in the guide
// myExport.Data will give us a List<PropertyData> which you can enumerate if you like, but we can reference a property by name or index with the export directly.
// We know this is a FloatPropertyData because it is serialized as a FloatProperty. BoolPropertyData is a BoolProperty, ObjectPropertyData is an ObjectProperty, etc.
Expand Down Expand Up @@ -107,4 +110,4 @@ At the end of the day, we have made no assumptions about the ordering of the exp

UAssetAPI is only one layer of abstraction above the raw binary format, which means that it essentially gives you full access to every single aspect of a .uasset file. This means that performing very complex operations can be quite a challenge, so keep experimenting!

You may find it useful while learning to export assets into JSON through the `.SerializeJSON()` method or through UAssetGUI, as the JSON format very closely mirrors the way that assets are laid out in UAssetAPI. You can also find lots of examples for UAssetAPI syntax and usage in the [unit tests](https://github.com/atenfyr/UAssetAPI/blob/master/UAssetAPI.Tests/AssetUnitTests.cs).
You may find it useful while learning to export assets into JSON through the `.SerializeJSON()` method or through UAssetGUI, as the JSON format very closely mirrors the way that assets are laid out in UAssetAPI. You can also find some more examples for UAssettAPI syntax and usage under the [More Examples](extras.md) page, and even more examples in the [unit tests](https://github.com/atenfyr/UAssetAPI/blob/master/UAssetAPI.Tests/AssetUnitTests.cs).
195 changes: 195 additions & 0 deletions docs/src/guide/extras.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# More Examples

This page contains several more examples of UAssetAPI usage for completing specific tasks.

### A simple, complete example
```cs
UAsset myAsset = new UAsset("C:\\plwp_6aam_a0.uasset", EngineVersion.VER_UE4_18);

// Find the export with the name "Default__plwp_6aam_a0_C"
NormalExport cdoExport = (NormalExport)myAsset["Default__plwp_6aam_a0_C"];
// Add/replace a property called SpeedMaximum
cdoExport["SpeedMaximum"] = new FloatPropertyData() { Value = 999999 };
// or, modify it directly
FloatPropertyData SpeedMaximum = (FloatPropertyData)cdoExport["SpeedMaximum"];
SpeedMaximum.Value = 999999;

myAsset.Write("C:\\NEW.uasset");
```

### Finding specific exports
```cs
UAsset myAsset = new UAsset("C:\\plwp_6aam_a0.uasset", EngineVersion.VER_UE4_18);

// We can find specific exports by index:
Export cdo = myAsset.Exports[1]; // Export 2; here, indexing from 0
// or like this:
cdo = new FPackageIndex(2).ToExport(myAsset); // also Export 2; FPackageIndex uses negative numbers for imports, 0 for null, and positive numbers for exports
// or, by ObjectName:
cdo = myAsset["Default__plwp_6aam_a0_C"]; // you can find this string value e.g. in UAssetGUI
cdo = myAsset[new FName(myAsset, "Default__plwp_6aam_a0_C")];
// or, to locate the ClassDefaultObject:
foreach (Export exp in myAsset.Exports)
{
if (exp.ObjectFlags.HasFlag(EObjectFlags.RF_ClassDefaultObject))
{
cdo = exp;
break;
}
}
// or, based on any property; maybe by SerialSize (length on disk):
long maxSerialSize = -1;
foreach (Export exp in myAsset.Exports)
{
if (exp.SerialSize > maxSerialSize)
{
maxSerialSize = exp.SerialSize;
cdo = exp;
}
}
```

### Accessing basic export types and property types
```cs
UAsset myAsset = new UAsset("C:\\plwp_6aam_a0.uasset", EngineVersion.VER_UE4_18);
Export exp = myAsset["Default__plwp_6aam_a0_C"];

// Export contains all fields contained within UAssetGUI's "Export Information"
// to manipulate data under "Export Data," you generally need to cast it to a child type
// NormalExport contains most all "normal" data, i.e. standard tagged properties
if (exp is NormalExport normalExport)
{
for (int i = 0; i < normalExport.Data.Count; i++)
{
PropertyData prop = normalExport.Data[i];
Console.WriteLine(prop.Name.ToString() + ": " + prop.PropertyType.ToString());

// you can access prop.Value for many types, but for other types, you can cast to a child type and access other fields
if (prop is FloatPropertyData floatProp) floatProp.Value = 60; // change all floats to = 60
// ArrayPropertyData.Value is a PropertyData[] array, entries referenced by index
// StructPropertyData.Value is a List<PropertyData>, or you can index StructPropertyData directly
if (prop is ArrayPropertyData arrProp)
{
for (int j = 0; j < arrProp.Value.Length; j++)
{
PropertyData prop2 = arrProp.Value[i];
Console.WriteLine(prop2.Name.ToString() + ": " + prop2.PropertyType.ToString());
// etc.
// note that arrays and structs can contain arrays and structs too...
}
}

if (prop is StructPropertyData structProp)
{
for (int j = 0; j < structProp.Value.Count; j++)
{
PropertyData prop2 = structProp.Value[i];
Console.WriteLine(prop2.Name.ToString() + ": " + prop2.PropertyType.ToString());
// etc.
// note that arrays and structs can contain arrays and structs too...
}

// or:
// PropertyData prop2 = structProp["PropertyNameHere"];
}
}
}

// DataTableExport is a NormalExport, but also contains entries in DataTables
if (exp is DataTableExport dtExport)
{
// dtExport.Data exists, but it typically only contains struct type information
// to access other entries, use:
List<StructPropertyData> entries = dtExport.Table.Data;

// etc.
}

// RawExport is an export that failed to parse for some reason, but you can still access and modify its binary data
if (exp is RawExport rawExport)
{
byte[] rawData = rawExport.Data;

// etc.
}

// see other examples for more advanced export types!
```

### Duplicating properties
```cs
UAsset myAsset = new UAsset("C:\\plwp_6aam_a0.uasset", EngineVersion.VER_UE4_18);
NormalExport cdoExport = (NormalExport)myAsset["Default__plwp_6aam_a0_C"];

FloatPropertyData targetProp = (FloatPropertyData)cdoExport["SpeedMaximum"];

// if we try something like:
/*
FloatPropertyData newProp = targetProp;
newProp.Value = 999999;
*/

// we'll accidentally change the value of targetProp too!
// we can duplicate this property using .Clone() instead:
FloatPropertyData newProp = (FloatPropertyData)targetProp.Clone();
newProp.Value = 999999;
cdoExport["SpeedMaximum2"] = newProp;

// .Clone() performs a deep copy, so you can e.g. clone a StructProperty and modify child properties freely
// .Clone() on an Export directly, however, is not implemented properly for child export types (e.g. the .Data list of a NormalExport is not cloned)
```

### Read assets that use unversioned properties
```cs
// to read an asset that uses unversioned properties, you must first source a .usmap mappings file for the game the asset is from, e.g. with UE4SS
// you can read a mappings file with the Usmap class, and pass it into the UAsset constructor
Usmap mappings = new Usmap("C:\\MyGame.usmap");
UAsset myAsset = new UAsset("C:\\my_asset.uasset", EngineVersion.VER_UE5_3, mappings);

// then, read and write data as normal
// myAsset.HasUnversionedProperties will return true
// notes for the curious:
// * using the FName constructor adds new entries to the name map, which is often frivolous with unversioned properties; if you care, use FName.DefineDummy instead, but if UAssetAPI tries to write a dummy FName to disk it will throw an exception
// * UAssetAPI only supports reading .usmap files, not writing
// * UAssetAPI supports .usmap versions 0 through 3, uncompressed and zstandard-compressed files, and PPTH/EATR/ENVP extensions
```

### Interface with JSON
```cs
UAsset myAsset = new UAsset("C:\\plwp_6aam_a0.uasset", EngineVersion.VER_UE4_18);

// write asset to JSON
string jsonSerializedAsset = tester.SerializeJson();
File.WriteAllText("C:\\plwp_6aam_a0.json", jsonSerializedAsset);

// read asset back from JSON
UAsset myAsset2 = UAsset.DeserializeJson("C:\\plwp_6aam_a0.json");
// myAsset2 should contain the same information as myAsset
// write asset to binary format
myAsset2.Write("C:\\plwp_6aam_a0_NEW.uasset");
```

### Read and modify blueprint bytecode
```cs
UAsset myAsset = new UAsset("C:\\my_asset.uasset", EngineVersion.VER_UE4_18);

// all StructExport exports can contain blueprint bytecode, let's pretend Export 1 is a StructExport
StructExport myStructExport = (StructExport)myAsset.Exports[0];

KismetExpression[] bytecode = myStructExport.ScriptBytecode;
if (bytecode != null) // bytecode may fail to parse, in which case it will be null and stored raw in ScriptBytecodeRaw
{
// KismetExpression has many child classes, one child class for each type of instruction
// as with PropertyData, you can access .RawValue for many instruction types, but you'll need to cast for other kinds of instructions to access specific fields
foreach (KismetExpression instruction in bytecode)
{
Console.WriteLine(instruction.Token.ToString() + ": " + instruction.RawValue.ToString());
}
}
```

0 comments on commit e2cfb9d

Please sign in to comment.