unreal.js
Version:
A pak reader for games like VALORANT & Fortnite written in Node.JS
469 lines (468 loc) • 19.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PakPackage = void 0;
const FName_1 = require("../objects/uobject/FName");
const Package_1 = require("./Package");
const PackageFileSummary_1 = require("../objects/uobject/PackageFileSummary");
const ObjectResource_1 = require("../objects/uobject/ObjectResource");
const FAssetArchive_1 = require("./reader/FAssetArchive");
const UObject_1 = require("./exports/UObject");
const Exceptions_1 = require("../../exceptions/Exceptions");
const EPackageFlags_1 = require("../objects/uobject/EPackageFlags");
const PayloadType_1 = require("./util/PayloadType");
const UScriptStruct_1 = require("./exports/UScriptStruct");
const ObjectTypeRegistry_1 = require("./ObjectTypeRegistry");
const FAssetArchiveWriter_1 = require("./writer/FAssetArchiveWriter");
const lodash_1 = require("lodash");
const Lazy_1 = require("../../util/Lazy");
const Config_1 = require("../../Config");
const VersionContainer_1 = require("../versions/VersionContainer");
const UEnum_1 = require("./exports/UEnum");
const Pair_1 = require("../../util/Pair");
/**
* UE4 Pak Package
* @extends {Package}
*/
class PakPackage extends Package_1.Package {
/**
* Creates an instance
* @param {Buffer} uasset Uasset data
* @param {?Buffer} uexp Uexp data
* @param {?Buffer} ubulk Ubulk data
* @param {string} name Name of package file
* @param {?FileProvider} provider File provider
* @param {?VersionContainer} versions Game that is used
* @constructor
* @public
*/
constructor(uasset, uexp = null, ubulk = null, name, provider = null, versions = VersionContainer_1.VersionContainer.DEFAULT) {
super(name, provider, versions);
/**
* Pak magic
* @type {number}
* @protected
*/
this.packageMagic = 0x9E2A83C1;
/**
* UEXP data
* @type {?Buffer}
* @public
*/
this.uexp = null;
/**
* UBULK data
* @type {?Buffer}
* @public
*/
this.ubulk = null;
/**
* File provider
* @type {?FileProvider}
* @public
*/
this.provider = null;
this.uasset = uasset;
this.uexp = uexp;
this.ubulk = ubulk;
this.fileName = name;
this.provider = provider;
// init
this.name = provider?.compactFilePath(this.fileName)?.substring(0, this.fileName.lastIndexOf(".")) || this.fileName;
const uassetAr = new FAssetArchive_1.FAssetArchive(this.uasset, this.provider, this.fileName);
const uexpAr = this.uexp ? new FAssetArchive_1.FAssetArchive(this.uexp, this.provider, this.fileName) : uassetAr;
const ubulkAr = this.ubulk ? new FAssetArchive_1.FAssetArchive(this.ubulk, this.provider, this.fileName) : null;
uassetAr.versions = versions;
uassetAr.owner = this;
uexpAr.versions = versions;
uexpAr.owner = this;
if (ubulkAr) {
ubulkAr.versions = versions;
ubulkAr.owner = this;
}
this.nameMap = [];
this.importMap = [];
this.exportMap = [];
this.info = new PackageFileSummary_1.FPackageFileSummary(uassetAr);
if (this.info.tag !== this.packageMagic)
throw new Exceptions_1.ParserException(`Invalid uasset magic, ${this.info.tag} !== ${this.packageMagic}`, uassetAr);
const ver = this.info.fileVersionUE4;
if (ver > 0) { // TODO
uassetAr.ver = ver;
uexpAr.ver = ver;
if (ubulkAr)
ubulkAr.ver = ver;
}
this.packageFlags = this.info.packageFlags;
uassetAr.pos = this.info.nameOffset;
let i = 0;
while (i < this.info.nameCount) {
this.nameMap.push(new FName_1.FNameEntry(uassetAr));
++i;
}
uassetAr.pos = this.info.importOffset;
let x = 0;
while (x < this.info.importCount) {
this.importMap.push(new ObjectResource_1.FObjectImport(uassetAr));
++x;
}
uassetAr.pos = this.info.exportOffset;
let y = 0;
while (y < this.info.exportCount) {
this.exportMap.push(new ObjectResource_1.FObjectExport(uassetAr));
++y;
}
// Setup uexp reader
if (this.uexp != null) {
uexpAr.uassetSize = this.info.totalHeaderSize;
}
uexpAr.bulkDataStartOffset = this.info.bulkDataStartOffset;
uexpAr.useUnversionedPropertySerialization = (this.packageFlags & EPackageFlags_1.EPackageFlags.PKG_UnversionedProperties) !== 0;
// If attached also setup the ubulk reader
if (ubulkAr != null) {
ubulkAr.uassetSize = this.info.totalHeaderSize;
ubulkAr.uexpSize = lodash_1.sum(this.exportMap.map(it => it.serialSize));
uexpAr.addPayload(PayloadType_1.PayloadType.UBULK, ubulkAr);
}
this.exportMap.forEach((e) => {
e.exportObject = new Lazy_1.Lazy(() => {
const uexpAr2 = uexpAr.clone();
uexpAr2.seekRelative(e.serialOffset);
const validPos = (uexpAr2.pos + e.serialSize);
const obj = Package_1.Package.constructExport(e.classIndex.load());
obj.export = e;
obj.name = e.objectName.text;
obj.outer = e.outerIndex.load() || this;
// obj.template = this.findObject(export.templateIndex)
obj.deserialize(uexpAr2, validPos);
if (validPos !== uexpAr2.pos) {
console.warn(`Did not read ${obj.exportType} correctly, ${validPos - uexpAr.pos} bytes remaining`);
}
else {
if (Config_1.Config.GDebug)
console.debug(`Successfully read ${obj.exportType} at ${uexpAr2.toNormalPos(e.serialOffset)} with size ${e.serialSize}`);
}
return obj;
});
});
}
/**
* Stores lazy exports
* @type {Array<Lazy<UObject>>}
* @public
*/
get exportsLazy() {
return this.exportMap.map(it => it.exportObject);
}
/**
* Finds an object by index
* @param {?FPackageIndex} index Index to find
* @returns {?Lazy<any>} Object or null
* @public
*/
findObject(index) {
return (index === null || index.isNull()) ? null :
index.isImport() ? this.findImport(this.importMap[index.toImport()]) :
index.isExport() ? this.exportMap[index.toExport()]?.exportObject :
null;
}
/**
* Loads an import
* @param {?FObjectImport} imp Import to load
* @returns {?UObject} Object or null
* @public
*/
loadImport(imp) {
if (!imp)
return null;
return this.findImport(imp)?.value || null;
}
/**
* Finds an import
* @param {?FObjectImport} imp Import to load
* @returns {?Lazy<UObject>} Object or null
* @public
*/
findImport(imp) {
if (!imp)
return null;
if (imp.className.text === "Class") {
return new Lazy_1.Lazy(() => {
const structName = imp.objectName;
let struct = this.provider?.mappingsProvider?.getStruct(structName);
if (!struct) {
if ((this.packageFlags & EPackageFlags_1.EPackageFlags.PKG_UnversionedProperties) !== 0) {
throw new Exceptions_1.MissingSchemaException(`Unknown struct ${structName}`);
}
struct = new UScriptStruct_1.UScriptStruct(ObjectTypeRegistry_1.ObjectTypeRegistry.get(structName.text), structName);
}
return struct;
});
}
// The needed export is located in another asset, try to load it
if (!this.getImportObject(imp.outerIndex))
return null;
if (!this.provider)
return null;
const pkg = this.getPackage(imp.outerIndex);
if (pkg)
return pkg.findObjectByName(imp.objectName.text, imp.className.text);
else
console.warn("Failed to load referenced import");
return null;
}
/**
* Finds an object by name
* @param {string} objectName Name of object
* @param {?string} className Class name of object
* @returns {?Lazy<UObject>} Object or null
* @public
*/
findObjectByName(objectName, className) {
const exp = this.exportMap.find(it => {
return it.objectName.text === objectName
&& (className == null || (this.getImportObject(it.classIndex))?.objectName?.text === className);
});
return exp?.exportObject;
}
findObjectMinimal(index) {
if (index == null || index.isNull() == null)
return null;
if (index.isImport())
return this.resolveImport(index);
return new ResolvedExportObject(index.toExport(), this);
}
resolveImport(importIndex) {
const imp = this.importMap[importIndex.toImport()];
let outerMostIndex = importIndex;
let outerMostImport;
while (true) {
outerMostImport = this.importMap[outerMostIndex.toImport()];
if (outerMostImport.outerIndex.isNull())
break;
outerMostIndex = outerMostImport.outerIndex;
}
outerMostImport = this.importMap[outerMostIndex.toImport()];
// We don't support loading script packages, so just return a fallback
if (outerMostImport.objectName.text.startsWith("/Script/")) {
return new ResolvedImportObject(imp, this);
}
const importPackage = this.provider?.loadGameFile(outerMostImport.objectName.text);
if (importPackage == null) {
console.warn("Missing native package (%s) for import of %s in %s.", outerMostImport.objectName, imp.objectName, this.name);
return new ResolvedImportObject(imp, this);
}
let outer = null;
if (outerMostIndex != imp.outerIndex && imp.outerIndex.isImport()) {
//var outerImport = importMap[import.outerIndex.toImport()]
outer = this.resolveImport(imp.outerIndex).getPathName0();
/*if (outer == null) {
console.warn("Missing outer for import of ({}): {} in {} was not found, but the package exists.", name, outerImport.objectName, importPackage.getFullName())
return ResolvedImportObject(import, this)
}*/
}
const impPkgLen = importPackage.exportMap.length;
for (let i = 0; i < impPkgLen; ++i) {
const exp = importPackage.exportMap[i];
if (exp.objectName != exp.objectName)
continue;
const thisOuter = importPackage.findObjectMinimal(exp.outerIndex);
if (thisOuter?.getPathName0() == outer)
return new ResolvedExportObject(i, importPackage);
}
console.warn("Missing import of (%s): %s in %s was not found, but the package exists.", this.name, imp.objectName, importPackage.getFullName0());
return new ResolvedImportObject(imp, this);
}
/**
* Gets an import object
* @param {FPackageIndex} imp Import to find
* @returns {?FObjectImport} Import or null
* @public
*/
getImportObject(imp) {
return imp.isImport() ? this.importMap[imp.toImport()] : null;
}
/**
* Gets an export object
* @param {FPackageIndex} imp Export to find
* @returns {?FObjectExport} Export or null
* @public
*/
getExportObject(imp) {
return imp.isExport() ? this.exportMap[imp.toExport()] : null;
}
/**
* Gets either export or import object
* @param {FPackageIndex} imp Index to find
* @returns {FObjectImport | FObjectExport | null} Object or null
* @public
*/
getResource(imp) {
return this.getImportObject(imp) || this.getExportObject(imp);
}
/**
* Turns this into json
* @param {?Locres} locres Locres to use
* @returns {Array<IJson>} Json
* @public
*/
toJson(locres) {
const object = [];
for (const it of this.exports) {
if (!(it instanceof UObject_1.UObject))
continue;
const json = it.toJson(locres);
object.push({
type: it.exportType,
name: it.name,
properties: json
});
}
return object;
}
/**
* Gets a package from index
* @param {FPackageIndex} imp Package to get
* @returns {?Package} Package or null
* @private
*/
getPackage(imp) {
const obj = this.getImportObject(imp);
if (obj) {
return this.provider.loadGameFile(obj.objectName.text);
}
return null;
}
/* DON'T USE THIS */
updateHeader() {
const uassetWriter = new FAssetArchiveWriter_1.FByteArchiveWriter();
uassetWriter.versions = this.versions;
uassetWriter.nameMap = this.nameMap;
uassetWriter.importMap = this.importMap;
uassetWriter.exportMap = this.exportMap;
this.info.serialize(uassetWriter);
const nameMapOffset = uassetWriter.pos();
if (this.info.nameCount !== this.nameMap.length) {
throw new Exceptions_1.ParserException(`Invalid name count, summary says ${this.info.nameCount} names but name map is ${this.nameMap.length} entries long`, uassetWriter);
}
this.nameMap.forEach((it) => it.serialize(uassetWriter));
const importMapOffset = uassetWriter.pos();
if (this.info.importCount !== this.importMap.length)
throw new Exceptions_1.ParserException(`Invalid import count, summary says ${this.info.importCount} imports but import map is ${this.importMap.length} entries long`, uassetWriter);
this.importMap.forEach((it) => it.serialize(uassetWriter));
const exportMapOffset = uassetWriter.pos();
if (this.info.exportCount !== this.exportMap.length)
throw new Exceptions_1.ParserException(`Invalid export count, summary says ${this.info.exportCount} exports but export map is ${this.exportMap.length} entries long`, uassetWriter);
this.exportMap.forEach((it) => it.serialize(uassetWriter));
this.info.totalHeaderSize = uassetWriter.pos();
this.info.nameOffset = nameMapOffset;
this.info.importOffset = importMapOffset;
this.info.exportOffset = exportMapOffset;
}
/* DON'T USE THIS */
write(uassetOutputStream, uexpOutputStream, ubulkOutputStream) {
this.updateHeader();
const uexpWriter = this.writer(uexpOutputStream);
uexpWriter.versions = this.versions;
uexpWriter.uassetSize = this.info.totalHeaderSize;
this.exports.forEach((it) => {
const beginPos = uexpWriter.relativePos();
it.serialize(uexpWriter);
const finalPos = uexpWriter.relativePos();
if (it.export) {
it.export.serialOffset = beginPos;
it.export.serialSize = finalPos - beginPos;
}
});
uexpWriter.writeUInt32(this.packageMagic);
const uassetWriter = this.writer(uassetOutputStream);
uassetWriter.versions = this.versions;
this.info.serialize(uassetWriter);
const nameMapPadding = this.info.nameOffset - uassetWriter.pos();
if (nameMapPadding > 0)
uassetWriter.write(nameMapPadding);
if (this.info.nameCount !== this.nameMap.length)
throw new Exceptions_1.ParserException(`Invalid name count, summary says ${this.info.nameCount} names but name map is ${this.nameMap.length} entries long`, uassetWriter);
this.nameMap.forEach((it) => it.serialize(uassetWriter));
const importMapPadding = this.info.importOffset - uassetWriter.pos();
if (importMapPadding > 0)
uassetWriter.write(importMapPadding);
if (this.info.importCount !== this.nameMap.length)
throw new Exceptions_1.ParserException(`Invalid import count, summary says ${this.info.importCount} imports but import map is ${this.importMap.length} entries long`, uassetWriter);
this.importMap.forEach((it) => it.serialize(uassetWriter));
const exportMapPadding = this.info.exportOffset - uassetWriter.pos();
if (exportMapPadding > 0)
uassetWriter.write(exportMapPadding);
if (this.info.exportCount !== this.exportMap.length)
throw new Exceptions_1.ParserException(`Invalid export count, summary says ${this.info.exportCount} exports but export map is ${this.exportMap.length} entries long`, uassetWriter);
this.exportMap.forEach((it) => it.serialize(uassetWriter));
ubulkOutputStream?.destroy();
}
/* DON'T USE THIS */
writer(outputStream) {
const obj = new FAssetArchiveWriter_1.FAssetArchiveWriter(outputStream);
obj.nameMap = this.nameMap;
obj.importMap = this.importMap;
obj.exportMap = this.exportMap;
return obj;
}
}
exports.PakPackage = PakPackage;
class ResolvedExportObject extends Package_1.ResolvedObject {
constructor(exportIndex, pkg) {
super(pkg, exportIndex);
this.export = pkg.exportMap[exportIndex];
}
get name() {
return this.export.objectName;
}
getOuter() {
return this.pkg.findObjectMinimal(this.export.outerIndex) || new Package_1.ResolvedLoadedObject(this.pkg);
}
getClazz() {
return this.pkg.findObjectMinimal(this.export.classIndex);
}
getSuper() {
return this.pkg.findObjectMinimal(this.export.superIndex);
}
getObject() {
return this.export.exportObject;
}
}
/** Fallback if we cannot resolve the export in another package */
class ResolvedImportObject extends Package_1.ResolvedObject {
constructor(_import, pkg) {
super(pkg);
this._import = _import;
}
get name() {
return this._import.objectName;
}
getOuter() {
return this.pkg.findObjectMinimal(this._import.outerIndex);
}
getClazz() {
return new Package_1.ResolvedLoadedObject(new UScriptStruct_1.UScriptStruct(this._import.className));
}
getObject() {
return new Lazy_1.Lazy(() => {
const n = this._import.className.text;
const _n = this._import.objectName;
if (n === "Class" || n === "ScripStruct")
return this.pkg.provider?.mappingsProvider?.getStruct(_n);
if (n === "Enum") {
const enumValues = this.pkg.provider?.mappingsProvider?.getEnum(_n);
if (enumValues != null) {
const enm = new UEnum_1.UEnum();
enm.name = _n.text;
enm.names = new Array(enumValues.length);
for (let i = 0; i < enumValues.length; ++i) {
enm.names[i] = new Pair_1.Pair(FName_1.FName.dummy(`${_n}::${enumValues[i]}`), i);
}
return enm;
}
else
return null;
}
});
}
}