diff --git a/README.md b/README.md
index b324fdf..139f931 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
A web app for making simple texture mods for UE5 games. It is inspired by matyalatte's great
[UE4-DDS-Tools](https://github.com/matyalatte/UE4-DDS-Tools), which unfortunately is no longer
-maintained. The main addition provided by this app is support for Virtual Textures and UE 5.5–5.7.
+maintained. The main addition provided by this app is support for Virtual Textures and UE 5.5–5.8.
The project is open-source (MIT license) and contributions are welcome. Some advanced texture types
(like texture arrays and cubemaps) are not supported.
diff --git a/index.html b/index.html
index f731f69..2e4e26d 100644
--- a/index.html
+++ b/index.html
@@ -11,7 +11,7 @@
@@ -113,7 +113,7 @@
About
It is inspired by matyalatte's great
UE4-DDS-Tools,
which unfortunately is no longer maintained. The main addition provided by this app is
- support for Virtual Textures and UE 5.5–5.7. The project is
+ support for Virtual Textures and UE 5.5–5.8. The project is
open-source on
GitHub; contributions are welcome. Some advanced texture types (like texture arrays
and cubemaps) are not supported.
@@ -135,6 +135,7 @@ About
Changelog
+ - 2026-09-13 - Added support for UE 5.8.
- 2026-09-05 - Initial release.
diff --git a/src/browser.ts b/src/browser.ts
index de9bb13..81df387 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -62,14 +62,13 @@ let activeProgressId: number | undefined;
versionSelect.add(new Option("Select UE version", "", true, true));
for (const [value, label] of [
- // [UEVersion.UE5_0, "5.0"],
- // [UEVersion.UE5_1, "5.1"],
[UEVersion.UE5_2, "5.2"],
[UEVersion.UE5_3, "5.3"],
[UEVersion.UE5_4, "5.4"],
[UEVersion.UE5_5, "5.5"],
[UEVersion.UE5_6, "5.6"],
[UEVersion.UE5_7, "5.7"],
+ [UEVersion.UE5_8, "5.8"],
] as const) {
versionSelect.add(new Option(label, String(value)));
}
diff --git a/src/ue/cooked-asset.ts b/src/ue/cooked-asset.ts
index fbace13..5986d75 100644
--- a/src/ue/cooked-asset.ts
+++ b/src/ue/cooked-asset.ts
@@ -65,9 +65,7 @@ export class CookedAsset {
uasset: uassetWriter.toUint8Array(),
uexp: uexpWriter.toUint8Array(),
...(this.files.ubulk !== undefined || ubulk.bytes.byteLength > 0 ? {ubulk: ubulk.bytes} : {}),
- ...(this.files.uptnl !== undefined || uptnl.bytes.byteLength > 0
- ? {uptnl: uptnl.bytes}
- : {}),
+ ...(this.files.uptnl !== undefined || uptnl.bytes.byteLength > 0 ? {uptnl: uptnl.bytes} : {}),
};
}
diff --git a/src/ue/uasset.ts b/src/ue/uasset.ts
index 6f9cddb..1830fb6 100644
--- a/src/ue/uasset.ts
+++ b/src/ue/uasset.ts
@@ -32,7 +32,7 @@ export class UAsset {
// ImportMap
reader.position = asset.summary.importOffset;
- asset.importMap = reader.readArray(asset.summary.importCount, ObjectImport.read);
+ asset.importMap = reader.readArray(asset.summary.importCount, r => ObjectImport.read(r, ver));
// ExportMap
reader.position = asset.summary.exportOffset;
@@ -120,7 +120,7 @@ export class UAsset {
// ImportMap
this.summary.importOffset = writer.position;
for (const imp of this.importMap) {
- imp.write(writer);
+ imp.write(writer, ver);
}
// ExportMap
@@ -306,25 +306,36 @@ export class ObjectImport {
className = new MinimalName(0, 0);
outerIndex = 0; // i32
objectName = new MinimalName(0, 0);
+ packageName = new MinimalName(0, 0);
importOptional = false; // bool32
- static read(reader: AssetReader): ObjectImport {
+ static read(reader: AssetReader, ver: UEVersion): ObjectImport {
const imp = new ObjectImport();
imp.classPackage = MinimalName.read(reader);
imp.className = MinimalName.read(reader);
imp.outerIndex = reader.readInt32();
imp.objectName = MinimalName.read(reader);
+
+ if (ver >= UEVersion.UE5_8) {
+ imp.packageName = MinimalName.read(reader);
+ }
+
imp.importOptional = reader.readBoolean32();
return imp;
}
- write(writer: AssetWriter): void {
+ write(writer: AssetWriter, ver: UEVersion): void {
this.classPackage.write(writer);
this.className.write(writer);
writer.writeInt32(this.outerIndex);
this.objectName.write(writer);
+
+ if (ver >= UEVersion.UE5_8) {
+ this.packageName.write(writer);
+ }
+
writer.writeBoolean32(this.importOptional);
}
}
diff --git a/src/ue/versioning.ts b/src/ue/versioning.ts
index ada53db..025ff08 100644
--- a/src/ue/versioning.ts
+++ b/src/ue/versioning.ts
@@ -11,6 +11,7 @@ export const UEVersion = {
UE5_5: 505_000,
UE5_6: 506_000,
UE5_7: 507_000,
+ UE5_8: 508_000,
} as const;
export type UEVersion = (typeof UEVersion)[keyof typeof UEVersion];
diff --git a/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.uasset b/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.uasset
new file mode 100644
index 0000000..50393a7
Binary files /dev/null and b/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.uasset differ
diff --git a/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.ubulk b/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.ubulk
new file mode 100644
index 0000000..5f48ece
Binary files /dev/null and b/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.ubulk differ
diff --git a/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.uexp b/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.uexp
new file mode 100644
index 0000000..79a4752
Binary files /dev/null and b/test/assets/ue5_8/T_Blocks2_1K_BC1_BC.uexp differ
diff --git a/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.uasset b/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.uasset
new file mode 100644
index 0000000..8ba77ca
Binary files /dev/null and b/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.uasset differ
diff --git a/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.ubulk b/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.ubulk
new file mode 100644
index 0000000..5f48ece
Binary files /dev/null and b/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.ubulk differ
diff --git a/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.uexp b/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.uexp
new file mode 100644
index 0000000..cfafe6d
Binary files /dev/null and b/test/assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC.uexp differ
diff --git a/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.uasset b/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.uasset
new file mode 100644
index 0000000..0d10dab
Binary files /dev/null and b/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.uasset differ
diff --git a/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.ubulk b/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.ubulk
new file mode 100644
index 0000000..abade0c
Binary files /dev/null and b/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.ubulk differ
diff --git a/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.uexp b/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.uexp
new file mode 100644
index 0000000..5a7e8bd
Binary files /dev/null and b/test/assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC.uexp differ
diff --git a/test/parsing.test.ts b/test/parsing.test.ts
index df75caa..17c5266 100644
--- a/test/parsing.test.ts
+++ b/test/parsing.test.ts
@@ -2339,6 +2339,233 @@ test("UE5_7__Texture2D_BC1", async () => {
assert.deepStrictEqual(tex.noneName, new MinimalName(0, 0));
});
+test("UE5_8__Texture2D_BC1", async () => {
+ const version = UEVersion.UE5_8;
+
+ // ======== .uasset ========
+ const uassetPath = "./assets/ue5_8/T_Blocks2_1K_BC1_BC.uasset";
+ const uassetBytes = await readFile(new URL(uassetPath, import.meta.url));
+ const reader = new AssetReader(uassetBytes);
+ const uasset = UAsset.read(reader, version);
+
+ assert.strictEqual(uasset.fileSize, uassetBytes.length);
+ assert.strictEqual(uasset.fileSize, 1244);
+
+ // PackageFileSummary
+ const summary = uasset.summary;
+
+ assert.strictEqual(summary.tag, 0x9e2a83c1);
+ assert.strictEqual(summary.legacyFileVersion, -9);
+
+ assert.strictEqual(summary.fileVersionUE3, 0);
+ assert.strictEqual(summary.fileVersionUE4, 0);
+ assert.strictEqual(summary.fileVersionUE5, 0);
+ assert.strictEqual(summary.fileVersionLicenseeUE, 0);
+ assert.deepStrictEqual(summary.customVersions, []);
+
+ assert.deepStrictEqual(
+ Array.from(summary.savedHash),
+ [
+ 0x5d, 0x59, 0x06, 0xfe, 0x88, 0x65, 0x6a, 0x44, 0xe0, 0x73,
+ 0x14, 0xb6, 0x87, 0x25, 0x3a, 0xf7, 0x71, 0x9a, 0x76, 0x62,
+ ],
+ );
+ assert.strictEqual(summary.totalHeaderSize, 1244);
+
+ assert.strictEqual(summary.packageName, "/Game/Textures/T_Blocks2_1K_BC1_BC");
+
+ const expectedFlags = (
+ EPackageFlags.PKG_Cooked |
+ EPackageFlags.PKG_UnversionedProperties |
+ EPackageFlags.PKG_FilterEditorOnly
+ ) >>> 0;
+ assert.strictEqual(summary.packageFlags, expectedFlags);
+
+ assert.strictEqual(summary.nameCount, 10);
+ assert.strictEqual(summary.softObjectPathsCount, 0);
+ assert.strictEqual(summary.gatherableTextDataCount, 0);
+ assert.strictEqual(summary.exportCount, 1);
+ assert.strictEqual(summary.importCount, 3);
+ assert.strictEqual(summary.cellExportCount, 0);
+ assert.strictEqual(summary.cellImportCount, 0);
+ assert.strictEqual(summary.softPackageReferencesCount, 0);
+ assert.strictEqual(summary.importTypeHierarchiesCount, 0);
+
+ assert.deepStrictEqual(summary.guid, {
+ a: 0,
+ b: 0,
+ c: 0,
+ d: 0,
+ });
+
+ assert.strictEqual(summary.generations.length, 1);
+ const generation = summary.generations[0];
+ assert.strictEqual(generation.exportCount, 1);
+ assert.strictEqual(generation.nameCount, 10);
+
+ const emptyEngine = new EngineVersion(0, 0, 0, 0, "");
+ assert.deepStrictEqual(summary.savedByEngineVersion, emptyEngine);
+ assert.deepStrictEqual(summary.compatibleWithEngineVersion, emptyEngine);
+
+ assert.strictEqual(summary.compressionFlags, 0);
+ assert.strictEqual(summary.compressedChunksCount, 0);
+ assert.strictEqual(summary.packageSource, -980866167);
+ assert.deepStrictEqual(summary.additionalPackagesToCook, []);
+ assert.strictEqual(summary.preloadDependencyCount, 2);
+ assert.strictEqual(summary.namesReferencedFromExportDataCount, 2);
+
+ // NameMap
+ const expectedNames = [
+ "None",
+ "PF_DXT1",
+ "/Game/Textures/T_Blocks2_1K_BC1_BC",
+ "/Script/CoreUObject",
+ "/Script/Engine",
+ "Class",
+ "Default__Texture2D",
+ "Package",
+ "T_Blocks2_1K_BC1_BC",
+ "Texture2D",
+ ];
+ assert.deepStrictEqual(uasset.nameMap.map(x => x.name), expectedNames);
+
+ // ImportMap
+ assert.strictEqual(uasset.importMap.length, 3);
+
+ assert.deepStrictEqual(uasset.importMap[0].classPackage, new MinimalName(3, 0));
+ assert.deepStrictEqual(uasset.importMap[0].className, new MinimalName(5, 0));
+ assert.strictEqual(uasset.importMap[0].outerIndex, -2);
+ assert.deepStrictEqual(uasset.importMap[0].objectName, new MinimalName(9, 0));
+ assert.deepStrictEqual(uasset.importMap[0].packageName, new MinimalName(9, 0));
+ assert.strictEqual(uasset.importMap[0].importOptional, false);
+
+ assert.deepStrictEqual(uasset.importMap[1].classPackage, new MinimalName(3, 0));
+ assert.deepStrictEqual(uasset.importMap[1].className, new MinimalName(7, 0));
+ assert.strictEqual(uasset.importMap[1].outerIndex, 0);
+ assert.deepStrictEqual(uasset.importMap[1].objectName, new MinimalName(4, 0));
+ assert.deepStrictEqual(uasset.importMap[1].packageName, new MinimalName(4, 0));
+ assert.strictEqual(uasset.importMap[1].importOptional, false);
+
+ assert.deepStrictEqual(uasset.importMap[2].classPackage, new MinimalName(4, 0));
+ assert.deepStrictEqual(uasset.importMap[2].className, new MinimalName(9, 0));
+ assert.strictEqual(uasset.importMap[2].outerIndex, -2);
+ assert.deepStrictEqual(uasset.importMap[2].objectName, new MinimalName(6, 0));
+ assert.deepStrictEqual(uasset.importMap[2].packageName, new MinimalName(6, 0));
+ assert.strictEqual(uasset.importMap[2].importOptional, false);
+
+ // ExportMap
+ assert.strictEqual(uasset.exportMap.length, 1);
+
+ const exportObject = uasset.exportMap[0];
+ assert.strictEqual(exportObject.classIndex, -1);
+ assert.strictEqual(exportObject.superIndex, 0);
+ assert.strictEqual(exportObject.templateIndex, -3);
+ assert.strictEqual(exportObject.outerIndex, 0);
+ assert.deepStrictEqual(exportObject.objectName, new MinimalName(8, 0));
+ assert.strictEqual(exportObject.objectFlags, 3);
+ assert.strictEqual(exportObject.serialSize, 3038n);
+ assert.strictEqual(exportObject.forcedExport, false);
+ assert.strictEqual(exportObject.notForClient, false);
+ assert.strictEqual(exportObject.notForServer, false);
+ assert.strictEqual(exportObject.isInheritedInstance, false);
+ assert.strictEqual(exportObject.packageFlags, 0);
+ assert.strictEqual(exportObject.notAlwaysLoadedForEditorGame, true);
+ assert.strictEqual(exportObject.isAsset, true);
+ assert.strictEqual(exportObject.generatePublicHash, false);
+ assert.strictEqual(exportObject.firstExportDependency, 0);
+ assert.strictEqual(exportObject.serializationBeforeSerializationDependencies, 0);
+ assert.strictEqual(exportObject.createBeforeSerializationDependencies, 0);
+ assert.strictEqual(exportObject.serializationBeforeCreateDependencies, 2);
+ assert.strictEqual(exportObject.createBeforeCreateDependencies, 0);
+
+ // DependsMap
+ assert.deepStrictEqual(uasset.dependsMap, [[]]);
+
+ // PreloadDependencies
+ assert.deepStrictEqual(uasset.preloadDependencies, [-1, -3]);
+
+ // DataResourceMap
+ const expectedDataResources = [
+ {serialSize: 524288n, rawSize: 524288n, bulkDataFlags: expectedUbulkDataFlags, bulkType: BulkType.Ubulk},
+ {serialSize: 131072n, rawSize: 131072n, bulkDataFlags: expectedUbulkDataFlags, bulkType: BulkType.Ubulk},
+ {serialSize: 32768n, rawSize: 32768n, bulkDataFlags: expectedUbulkDataFlags, bulkType: BulkType.Ubulk},
+ {serialSize: 8192n, rawSize: 8192n, bulkDataFlags: expectedUbulkDataFlags, bulkType: BulkType.Ubulk},
+ {serialSize: 2048n, rawSize: 2048n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ {serialSize: 512n, rawSize: 512n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ {serialSize: 128n, rawSize: 128n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ {serialSize: 32n, rawSize: 32n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ {serialSize: 8n, rawSize: 8n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ {serialSize: 8n, rawSize: 8n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ {serialSize: 8n, rawSize: 8n, bulkDataFlags: expectedUexpDataFlags, bulkType: BulkType.Uexp},
+ ];
+
+ assert.strictEqual(uasset.dataResourceVersion, ObjectDataResourceVersion.AddedCookedIndex);
+ assert.strictEqual(uasset.dataResourceMap.length, expectedDataResources.length);
+ for (let i = 0; i < expectedDataResources.length; i++) {
+ const actual = uasset.dataResourceMap[i];
+ const expected = expectedDataResources[i];
+
+ assert.strictEqual(actual.flags, 0);
+ assert.strictEqual(actual.cookedIndex, 0);
+ assert.strictEqual(actual.duplicateSerialOffset, -1n);
+ assert.strictEqual(actual.serialSize, expected.serialSize);
+ assert.strictEqual(actual.rawSize, expected.rawSize);
+ assert.strictEqual(actual.outerIndex, 1);
+ assert.strictEqual(actual.bulkDataFlags, expected.bulkDataFlags);
+ assert.strictEqual(actual.bulkType, expected.bulkType);
+ }
+
+ // ======== .uexp ========
+ const uexpPath = uassetPath.replace(".uasset", ".uexp");
+ const uexpBytes = await readFile(new URL(uexpPath, import.meta.url));
+ const uexpReader = new AssetReader(uexpBytes);
+ const uexp = UExp.read(uexpReader, version, uasset);
+
+ assert.strictEqual(uexp.exports.length, 1);
+ const tex = uexp.exports[0];
+ assert.strictEqual(tex.kind, "texture");
+ assert.strictEqual(tex.className, "Texture2D");
+
+ assert.strictEqual(tex.serializeMipData, true);
+ assert.deepStrictEqual(tex.pixelFormatName, new MinimalName(1, 0));
+ assert.strictEqual(tex.importedWidth, 1024);
+ assert.strictEqual(tex.importedHeight, 1024);
+ assert.strictEqual(tex.packedData, 1);
+ assert.strictEqual(tex.pixelFormat, "PF_DXT1");
+ assert.strictEqual(tex.firstMipToSerialize, 0);
+ assert.strictEqual(tex.mipCount, 11);
+
+ const expectedMips = [
+ {inlineDataLength: 0, width: 1024, height: 1024},
+ {inlineDataLength: 0, width: 512, height: 512},
+ {inlineDataLength: 0, width: 256, height: 256},
+ {inlineDataLength: 0, width: 128, height: 128},
+ {inlineDataLength: 2048, width: 64, height: 64},
+ {inlineDataLength: 512, width: 32, height: 32},
+ {inlineDataLength: 128, width: 16, height: 16},
+ {inlineDataLength: 32, width: 8, height: 8},
+ {inlineDataLength: 8, width: 4, height: 4},
+ {inlineDataLength: 8, width: 2, height: 2},
+ {inlineDataLength: 8, width: 1, height: 1},
+ ];
+
+ assert.strictEqual(tex.mips.length, expectedMips.length);
+ for (let i = 0; i < expectedMips.length; i++) {
+ const mip: Mip = tex.mips[i]!;
+ const expected = expectedMips[i];
+
+ assert.strictEqual(mip.dataResourceIndex, i);
+ assert.strictEqual(mip.inlineData.length, expected.inlineDataLength);
+ assert.strictEqual(mip.width, expected.width);
+ assert.strictEqual(mip.height, expected.height);
+ assert.strictEqual(mip.depth, 1);
+ }
+
+ assert.strictEqual(tex.isVirtual, false);
+
+ assert.deepStrictEqual(tex.noneName, new MinimalName(0, 0));
+});
+
test("Ad-hoc parsing test", async () => {
const version = UEVersion.UE5_2;
diff --git a/test/roundtrip.test.ts b/test/roundtrip.test.ts
new file mode 100644
index 0000000..87a3be5
--- /dev/null
+++ b/test/roundtrip.test.ts
@@ -0,0 +1,44 @@
+import assert from "node:assert/strict";
+import {test} from "node:test";
+
+import {AssetWriter} from "../src/ue/asset-writer.ts";
+import {type CookedAsset, type CookedAssetBundle} from "../src/ue/cooked-asset.ts";
+import {UEVersion} from "../src/ue/versioning.ts";
+import {readAsset} from "./util.ts";
+
+function writeAsset(asset: CookedAsset): CookedAssetBundle {
+ const uassetWriter = new AssetWriter();
+ asset.uasset.write(uassetWriter, asset.version);
+
+ const uexpWriter = new AssetWriter();
+ asset.uexp.write(uexpWriter, asset.version, asset.uasset.dataResourceMap);
+
+ return {
+ uasset: uassetWriter.toUint8Array(),
+ uexp: uexpWriter.toUint8Array(),
+ ...(asset.files.ubulk !== undefined ? {ubulk: asset.files.ubulk} : {}),
+ ...(asset.files.uptnl !== undefined ? {uptnl: asset.files.uptnl} : {}),
+ };
+}
+
+test("UE5_7__Texture2D_BC1_Roundtrip", async () => {
+ const original = await readAsset("./assets/ue5_7/T_Blocks2_1K_BC1_BC", UEVersion.UE5_7);
+ const roundtripped = writeAsset(original);
+
+ assert.equal(Buffer.compare(roundtripped.uasset, original.files.uasset), 0);
+ assert.equal(Buffer.compare(roundtripped.uexp, original.files.uexp), 0);
+ assert.ok(roundtripped.ubulk);
+ assert.ok(original.files.ubulk);
+ assert.equal(Buffer.compare(roundtripped.ubulk, original.files.ubulk), 0);
+});
+
+test("UE5_8__Texture2D_BC1_Roundtrip", async () => {
+ const original = await readAsset("./assets/ue5_8/T_Blocks2_1K_BC1_BC", UEVersion.UE5_8);
+ const roundtripped = writeAsset(original);
+
+ assert.equal(Buffer.compare(roundtripped.uasset, original.files.uasset), 0);
+ assert.equal(Buffer.compare(roundtripped.uexp, original.files.uexp), 0);
+ assert.ok(roundtripped.ubulk);
+ assert.ok(original.files.ubulk);
+ assert.equal(Buffer.compare(roundtripped.ubulk, original.files.ubulk), 0);
+});
\ No newline at end of file
diff --git a/test/texture-replacement.test.ts b/test/texture-replacement.test.ts
index fc57687..3e9ec6c 100644
--- a/test/texture-replacement.test.ts
+++ b/test/texture-replacement.test.ts
@@ -1,5 +1,4 @@
import assert from "node:assert/strict";
-import {readFile} from "node:fs/promises";
import {test} from "node:test";
import {decodeTextureMip} from "../src/texture/decoding.ts";
@@ -10,18 +9,9 @@ import {UEVersion} from "../src/ue/versioning.ts";
import {PACKAGE_FILE_TAG} from "../src/ue/summary.ts";
import {BulkDataFlags, BulkType} from "../src/ue/uasset.ts";
import {UINT32_MAX} from "../src/util.ts";
-import {loadWasm} from "./util.ts";
+import {loadWasm, readAsset} from "./util.ts";
import {generateTextureMips} from "../src/texture/mips.ts";
-async function readAsset(basePath: string, version: UEVersion = UEVersion.UE5_4) {
- const [uasset, uexp, ubulk] = await Promise.all(
- [".uasset", ".uexp", ".ubulk"].map(extension => (
- readFile(new URL(basePath + extension, import.meta.url))
- )),
- );
- return parseAsset({uasset, uexp, ubulk}, version);
-}
-
// -------- Swap tests --------
test("Swap_UE5_2__Texture2D_BC1", async () => {
@@ -492,6 +482,84 @@ test("Swap_UE5_7__Texture2D_BC1", async () => {
assert.equal(lastMip.rgba[3], 255);
});
+test("Swap_UE5_8__Texture2D_BC1", async () => {
+ const original = await readAsset("./assets/ue5_8/swap_bc1/original/T_Blocks2_BC1_BC", UEVersion.UE5_8);
+ const editorSwap = await readAsset("./assets/ue5_8/swap_bc1/swapped/T_Blocks2_BC1_BC", UEVersion.UE5_8);
+ const originalSnapshot = structuredClone(original);
+
+ const wasm = await loadWasm();
+ const rgba = new Uint8Array(2048 * 2048 * 4);
+ for (let offset = 0; offset < rgba.length; offset += 4) {
+ rgba.set([40, 100, 200, 255], offset);
+ }
+
+ const progress: Array = [];
+ const originalRgba = rgba.slice();
+ const output = await replaceTexture(original, rgba, 2048, 2048, wasm, {
+ quality: 0,
+ onProgress: (completed, total) => progress.push([completed, total]),
+ });
+ assert.deepStrictEqual(structuredClone(original), originalSnapshot);
+ assert.deepStrictEqual(rgba, originalRgba);
+ assert.equal(output.uasset.byteLength, editorSwap.files.uasset.byteLength);
+ assert.equal(output.uexp.byteLength, editorSwap.files.uexp.byteLength);
+ assert.equal(output.ubulk?.byteLength, editorSwap.files.ubulk?.byteLength);
+ assert.equal(
+ new DataView(output.uexp.buffer, output.uexp.byteOffset).getUint32(
+ output.uexp.byteLength - 4,
+ true,
+ ),
+ PACKAGE_FILE_TAG,
+ );
+ assert.deepStrictEqual(progress[0], [0, 2_796_216]);
+ assert.deepStrictEqual(progress.at(-1), [2_796_216, 2_796_216]);
+
+ const replaced = parseAsset(output, UEVersion.UE5_8);
+ const texture = replaced.getTextureExport();
+ const expectedTexture = editorSwap.getTextureExport();
+ assert.equal(texture.importedWidth, 2048);
+ assert.equal(texture.importedHeight, 2048);
+ assert.equal(texture.mipCount, 12);
+ assert.equal(texture.skipOffset, expectedTexture.skipOffset);
+ assert.deepStrictEqual(
+ texture.mips.map(mip => [mip.width, mip.height, mip.depth]),
+ expectedTexture.mips.map(mip => [mip.width, mip.height, mip.depth]),
+ );
+ assert.deepStrictEqual(
+ replaced.uasset.dataResourceMap.map(resource => [
+ resource.serialOffset,
+ resource.serialSize,
+ resource.rawSize,
+ resource.bulkType,
+ ]),
+ editorSwap.uasset.dataResourceMap.map(resource => [
+ resource.serialOffset,
+ resource.serialSize,
+ resource.rawSize,
+ resource.bulkType,
+ ]),
+ );
+ assert.equal(replaced.uasset.summary.totalHeaderSize, editorSwap.uasset.summary.totalHeaderSize);
+ assert.equal(
+ replaced.uasset.summary.bulkDataStartOffset,
+ editorSwap.uasset.summary.bulkDataStartOffset,
+ );
+ assert.equal(
+ replaced.uasset.exportMap[0]?.serialSize,
+ editorSwap.uasset.exportMap[0]?.serialSize
+ );
+ assert.equal(
+ replaced.uasset.exportMap[0]?.serialOffset,
+ editorSwap.uasset.exportMap[0]?.serialOffset,
+ );
+
+ const lastMip = await decodeTextureMip(replaced, 11, wasm);
+ assert.ok(Math.abs(lastMip.rgba[0]! - 40) <= 10);
+ assert.ok(Math.abs(lastMip.rgba[1]! - 100) <= 10);
+ assert.ok(Math.abs(lastMip.rgba[2]! - 200) <= 10);
+ assert.equal(lastMip.rgba[3], 255);
+});
+
// -------- Misc tests --------
test("generates a complete box-filtered RGBA mip chain", () => {
diff --git a/test/util.ts b/test/util.ts
index 85bb127..29b0d88 100644
--- a/test/util.ts
+++ b/test/util.ts
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
import {readFile} from "node:fs/promises";
import {BcWasm} from "../src/wasm/wasm.ts";
+import {UEVersion} from "../src/ue/versioning.ts";
+import {parseAsset} from "../src/ue/cooked-asset.ts";
export async function loadWasm(): Promise {
const binary = await readFile(new URL("../wasm/bcencdec.wasm", import.meta.url));
@@ -35,3 +37,13 @@ export function readPpmPixels(bytes: Uint8Array, width: number, height: number):
assert.equal(pixels.length, width * height * 3);
return pixels;
}
+
+export async function readAsset(basePath: string, version: UEVersion = UEVersion.UE5_4) {
+ const [uasset, uexp, ubulk] = await Promise.all(
+ [".uasset", ".uexp", ".ubulk"].map(extension => (
+ readFile(new URL(basePath + extension, import.meta.url))
+ )),
+ );
+
+ return parseAsset({uasset, uexp, ubulk}, version);
+}
\ No newline at end of file