first commit
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Parser for SAGE `.manifest` files, ported from OpenSAGE
|
||||
* (src/OpenSage.Game/Data/StreamFS/ManifestFile.cs, commit d45d361).
|
||||
*
|
||||
* The manifest is a binary index produced by BinaryAssetBuilder: every asset
|
||||
* compiled into a stream is listed with hashed type/instance ids, an offset
|
||||
* into the asset-name string buffer, and an optional source file name.
|
||||
* Parsing it lets the extension treat `reference` includes (static.xml,
|
||||
* global.xml, audio.xml) as real, searchable asset pools.
|
||||
*/
|
||||
|
||||
import { assetTypeNameFromHash } from "../model/schemaModel";
|
||||
|
||||
export interface ManifestAsset {
|
||||
typeId: number;
|
||||
typeName: string | null;
|
||||
name: string;
|
||||
sourceFileName: string;
|
||||
}
|
||||
|
||||
export interface ManifestReferenceEntry {
|
||||
referenceType: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ManifestInfo {
|
||||
version: number;
|
||||
isBigEndian: boolean;
|
||||
isLinked: boolean;
|
||||
assetCount: number;
|
||||
assets: ManifestAsset[];
|
||||
manifestReferences: ManifestReferenceEntry[];
|
||||
/** Present when the file could not be parsed. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an asset type from its manifest name. BAB stores manifest assets
|
||||
* as "TypeName:Id" (e.g. "PlayerTemplate:Allies"), so the part before the
|
||||
* first colon is the type even when the TypeId hash is unknown to the
|
||||
* bundled AssetType table. Asset ids cannot contain ":" (InstanceId pattern),
|
||||
* so the split is unambiguous.
|
||||
*/
|
||||
export function deriveAssetType(typeName: string | null, assetName: string): string | null {
|
||||
if (typeName) return typeName;
|
||||
const idx = assetName.indexOf(":");
|
||||
if (idx <= 0) return null;
|
||||
const prefix = assetName.slice(0, idx);
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix) ? prefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the referenceable asset id from a manifest name. BAB stores
|
||||
* manifest names as "TypeName:InstanceId" where the instance id may itself
|
||||
* carry a subtype prefix for art assets, e.g.
|
||||
* W3dContainer:W3DContainer:AUANTIVEHICLEVEHICLETECH1_SKN
|
||||
* The referenceable id is the last colon-separated segment (asset ids cannot
|
||||
* contain ":" per the InstanceId pattern).
|
||||
*/
|
||||
export function deriveAssetId(assetName: string): string {
|
||||
const idx = assetName.lastIndexOf(":");
|
||||
return idx >= 0 ? assetName.slice(idx + 1) : assetName;
|
||||
}
|
||||
|
||||
class Cursor {
|
||||
private pos = 0;
|
||||
constructor(private buf: Uint8Array) {}
|
||||
|
||||
get position(): number {
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
byte(): number {
|
||||
if (this.pos >= this.buf.length) throw new Error("Unexpected end of file");
|
||||
return this.buf[this.pos++];
|
||||
}
|
||||
|
||||
uint16(): number {
|
||||
const a = this.byte();
|
||||
const b = this.byte();
|
||||
return a | (b << 8);
|
||||
}
|
||||
|
||||
uint32(): number {
|
||||
const a = this.byte();
|
||||
const b = this.byte();
|
||||
const c = this.byte();
|
||||
const d = this.byte();
|
||||
return ((a | (b << 8) | (c << 16) | (d << 24)) >>> 0);
|
||||
}
|
||||
|
||||
skip(n: number): void {
|
||||
this.pos += n;
|
||||
if (this.pos > this.buf.length) throw new Error("Unexpected end of file");
|
||||
}
|
||||
|
||||
nullTerminatedString(): string {
|
||||
const start = this.pos;
|
||||
while (this.pos < this.buf.length && this.buf[this.pos] !== 0) {
|
||||
this.pos++;
|
||||
}
|
||||
const end = this.pos;
|
||||
if (this.pos < this.buf.length) this.pos++; // consume NUL
|
||||
return decodeAscii(this.buf, start, end);
|
||||
}
|
||||
|
||||
readNameBuffer(endPosition: number): Map<number, string> {
|
||||
const names = new Map<number, string>();
|
||||
let nameOffset = 0;
|
||||
while (this.pos < endPosition) {
|
||||
const start = this.pos;
|
||||
names.set(nameOffset, this.nullTerminatedString());
|
||||
nameOffset += this.pos - start;
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeAscii(buf: Uint8Array, start: number, end: number): string {
|
||||
// Asset ids are ASCII; anything else decodes with replacement chars.
|
||||
let out = "";
|
||||
for (let i = start; i < end; i++) {
|
||||
out += String.fromCharCode(buf[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseManifest(buffer: Uint8Array): ManifestInfo {
|
||||
try {
|
||||
return parseManifestInner(buffer);
|
||||
} catch (err) {
|
||||
return {
|
||||
version: 0,
|
||||
isBigEndian: false,
|
||||
isLinked: false,
|
||||
assetCount: 0,
|
||||
assets: [],
|
||||
manifestReferences: [],
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseManifestInner(buffer: Uint8Array): ManifestInfo {
|
||||
const cursor = new Cursor(buffer);
|
||||
|
||||
const testValue = cursor.uint32();
|
||||
let version: number;
|
||||
let isBigEndian: boolean;
|
||||
let isLinked: boolean;
|
||||
|
||||
if (testValue === 0) {
|
||||
version = cursor.uint16();
|
||||
if (version !== 7) throw new Error(`Unsupported manifest version ${version}`);
|
||||
isBigEndian = readBooleanChecked(cursor);
|
||||
isLinked = readBooleanChecked(cursor);
|
||||
} else {
|
||||
cursor.skip(-4);
|
||||
isBigEndian = readBooleanChecked(cursor);
|
||||
isLinked = readBooleanChecked(cursor);
|
||||
version = cursor.uint16();
|
||||
if (version !== 5 && version !== 6) {
|
||||
throw new Error(`Unsupported manifest version ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
const streamChecksum = cursor.uint32();
|
||||
const allTypesHash = cursor.uint32();
|
||||
const assetCount = cursor.uint32();
|
||||
const totalInstanceDataSize = cursor.uint32();
|
||||
const maxInstanceChunkSize = cursor.uint32();
|
||||
const maxRelocationChunkSize = cursor.uint32();
|
||||
const maxImportsChunkSize = cursor.uint32();
|
||||
const assetReferenceBufferSize = cursor.uint32();
|
||||
const referencedManifestNameBufferSize = cursor.uint32();
|
||||
const assetNameBufferSize = cursor.uint32();
|
||||
const sourceFileNameBufferSize = cursor.uint32();
|
||||
|
||||
void streamChecksum;
|
||||
void allTypesHash;
|
||||
void totalInstanceDataSize;
|
||||
void maxInstanceChunkSize;
|
||||
void maxRelocationChunkSize;
|
||||
void maxImportsChunkSize;
|
||||
|
||||
interface RawEntry {
|
||||
typeId: number;
|
||||
nameOffset: number;
|
||||
sourceFileNameOffset: number;
|
||||
}
|
||||
const rawEntries: RawEntry[] = [];
|
||||
for (let i = 0; i < assetCount; i++) {
|
||||
const typeId = cursor.uint32();
|
||||
cursor.uint32(); // instanceId
|
||||
cursor.uint32(); // typeHash
|
||||
cursor.uint32(); // instanceHash
|
||||
cursor.uint32(); // assetReferenceOffset
|
||||
cursor.uint32(); // assetReferenceCount
|
||||
const nameOffset = cursor.uint32();
|
||||
const sourceFileNameOffset = cursor.uint32();
|
||||
cursor.uint32(); // instanceDataSize
|
||||
cursor.uint32(); // relocationDataSize
|
||||
cursor.uint32(); // importsDataSize
|
||||
if (version >= 6) {
|
||||
cursor.byte(); // isTokenized
|
||||
cursor.skip(3);
|
||||
}
|
||||
rawEntries.push({
|
||||
typeId,
|
||||
nameOffset,
|
||||
sourceFileNameOffset,
|
||||
});
|
||||
}
|
||||
|
||||
// Asset references buffer (not needed for completions).
|
||||
cursor.skip(assetReferenceBufferSize);
|
||||
|
||||
// Referenced manifest names.
|
||||
const manifestRefsEnd = cursor.position + referencedManifestNameBufferSize;
|
||||
const manifestReferences: ManifestReferenceEntry[] = [];
|
||||
while (cursor.position < manifestRefsEnd) {
|
||||
const referenceType = cursor.byte();
|
||||
const path = cursor.nullTerminatedString();
|
||||
manifestReferences.push({ referenceType, path });
|
||||
}
|
||||
|
||||
// Asset names.
|
||||
const assetNamesEnd = cursor.position + assetNameBufferSize;
|
||||
const assetNames = cursor.readNameBuffer(assetNamesEnd);
|
||||
|
||||
// Source file names.
|
||||
const sourceNamesEnd = cursor.position + sourceFileNameBufferSize;
|
||||
const sourceNames = cursor.readNameBuffer(sourceNamesEnd);
|
||||
|
||||
const assets: ManifestAsset[] = rawEntries.map((entry) => ({
|
||||
typeId: entry.typeId,
|
||||
typeName: assetTypeNameFromHash(entry.typeId) ?? null,
|
||||
name: assetNames.get(entry.nameOffset) ?? "",
|
||||
sourceFileName: sourceNames.get(entry.sourceFileNameOffset) ?? "",
|
||||
}));
|
||||
|
||||
return {
|
||||
version,
|
||||
isBigEndian,
|
||||
isLinked,
|
||||
assetCount,
|
||||
assets,
|
||||
manifestReferences,
|
||||
};
|
||||
}
|
||||
|
||||
function readBooleanChecked(cursor: Cursor): boolean {
|
||||
const value = cursor.byte();
|
||||
if (value === 0) return false;
|
||||
if (value === 1) return true;
|
||||
throw new Error(`Invalid boolean byte ${value}`);
|
||||
}
|
||||
Reference in New Issue
Block a user