Initial commit

This commit is contained in:
Brian Beck 2025-09-11 16:48:23 -07:00
commit 2211ed7650
10117 changed files with 735995 additions and 0 deletions

48
src/arrayUtils.ts Normal file
View file

@ -0,0 +1,48 @@
export function rotateHeightMap(
src: Uint16Array,
width: number,
height: number,
degrees: 90 | 180 | 270
) {
let outW: number;
let outH: number;
switch (degrees) {
case 90:
case 270:
outW = height;
outH = width;
break;
case 180:
outW = width;
outH = height;
break;
}
const out = new Uint16Array(outW * outH);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const val = src[y * width + x];
let nx, ny;
switch (degrees) {
case 90:
nx = height - 1 - y;
ny = x;
break;
case 180:
nx = width - 1 - x;
ny = height - 1 - y;
break;
case 270:
nx = y;
ny = width - 1 - x;
}
out[ny * outW + nx] = val;
}
}
return out;
}

5
src/interior.ts Normal file
View file

@ -0,0 +1,5 @@
import hxDif from "@/generated/hxDif.cjs";
export function parseInteriorBuffer(arrayBuffer: ArrayBufferLike) {
return hxDif.Dif.LoadFromArrayBuffer(arrayBuffer);
}

34
src/manifest.ts Normal file
View file

@ -0,0 +1,34 @@
import manifest from "../public/manifest.json";
export function getSource(resourcePath: string) {
const sources = manifest[resourcePath];
if (sources && sources.length > 0) {
return sources[sources.length - 1];
} else {
throw new Error(`Resource not found in manifest: ${resourcePath}`);
}
}
export function getActualResourcePath(resourcePath: string) {
if (manifest[resourcePath]) {
return resourcePath;
}
const resourcePaths = getResourceList();
const lowerCased = resourcePath.toLowerCase();
return (
resourcePaths.find((s) => s.toLowerCase() === lowerCased) ?? resourcePath
);
}
export function getResourceList() {
return Object.keys(manifest).sort();
}
export function getFilePath(resourcePath: string) {
const source = getSource(resourcePath);
if (source) {
return `public/base/@vl2/${source}/${resourcePath}`;
} else {
return `public/base/${resourcePath}`;
}
}

200
src/mission.ts Normal file
View file

@ -0,0 +1,200 @@
import parser from "@/generated/mission.cjs";
const definitionComment = /^ (DisplayName|MissionTypes) = (.+)$/;
const sectionBeginComment = /^--- ([A-Z ]+) BEGIN ---$/;
const sectionEndComment = /^--- ([A-Z ]+) END ---$/;
function parseComment(text) {
let match;
match = text.match(sectionBeginComment);
if (match) {
return {
type: "sectionBegin",
name: match[1],
};
}
match = text.match(sectionEndComment);
if (match) {
return {
type: "sectionEnd",
name: match[1],
};
}
match = text.match(definitionComment);
if (match) {
return {
type: "definition",
identifier: match[1],
value: match[2],
};
}
return null;
}
function parseInstance(instance) {
return {
className: instance.className,
instanceName: instance.instanceName,
properties: instance.body
.filter((def) => def.type === "definition")
.map((def) => {
switch (def.value.type) {
case "string":
case "number":
case "boolean":
return {
target: def.target,
value: def.value.value,
};
case "reference":
return {
target: def.target,
value: def.value,
};
default:
console.error(instance);
throw new Error(
`Unhandled value type: ${def.target.name} = ${def.value.type}`
);
}
}),
children: instance.body
.filter((def) => def.type === "instance")
.map((def) => parseInstance(def)),
};
}
export function parseMissionScript(script) {
// Clean up the script:
// - Remove code-like parts of the script so it's easier to parse.
script = script.replace(
/(\/\/--- OBJECT WRITE END ---\s+)(?:.|[\r\n])*$/,
"$1"
);
let objectWriteBegin = /(\/\/--- OBJECT WRITE BEGIN ---\s+)/.exec(script);
const firstSimGroup = /[\r\n]new SimGroup/.exec(script);
script =
script.slice(0, objectWriteBegin.index + objectWriteBegin[1].length) +
script.slice(firstSimGroup.index);
objectWriteBegin = /(\/\/--- OBJECT WRITE BEGIN ---\s+)/.exec(script);
const missionStringEnd = /(\/\/--- MISSION STRING END ---\s+)/.exec(script);
if (missionStringEnd) {
script =
script.slice(0, missionStringEnd.index + missionStringEnd[1].length) +
script.slice(objectWriteBegin.index);
}
// console.log(script);
const doc = parser.parse(script);
let section = { name: null, definitions: [] };
const mission = {
pragma: {},
sections: [],
};
for (const statement of doc) {
switch (statement.type) {
case "comment": {
const parsed = parseComment(statement.text);
if (parsed) {
switch (parsed.type) {
case "definition": {
if (section.name) {
section.definitions.push(statement);
} else {
mission.pragma[parsed.identifier] = parsed.value;
}
break;
}
case "sectionEnd": {
if (parsed.name !== section.name) {
throw new Error("Ending unmatched section!");
}
if (section.name || section.definitions.length) {
mission.sections.push(section);
}
section = { name: null, definitions: [] };
break;
}
case "sectionBegin": {
if (section.name) {
throw new Error("Already in a section!");
}
if (section.name || section.definitions.length) {
mission.sections.push(section);
}
section = { name: parsed.name, definitions: [] };
break;
}
}
} else {
section.definitions.push(statement);
}
break;
}
default: {
section.definitions.push(statement);
}
}
}
if (section.name || section.definitions.length) {
mission.sections.push(section);
}
return {
displayName: mission.pragma.DisplayName ?? null,
missionTypes: mission.pragma.MissionTypes?.split(" ") ?? [],
missionQuote:
mission.sections
.find((section) => section.name === "MISSION QUOTE")
?.definitions.filter((def) => def.type === "comment")
.map((def) => def.text)
.join("\n") ?? null,
missionString:
mission.sections
.find((section) => section.name === "MISSION STRING")
?.definitions.filter((def) => def.type === "comment")
.map((def) => def.text)
.join("\n") ?? null,
objects: mission.sections
.find((section) => section.name === "OBJECT WRITE")
?.definitions.filter((def) => def.type === "instance")
.map((def) => parseInstance(def)),
globals: mission.sections
.filter((section) => !section.name)
.flatMap((section) =>
section.definitions.filter((def) => def.type === "definition")
),
};
}
type Mission = ReturnType<typeof parseMissionScript>;
export function* iterObjects(objectList) {
for (const obj of objectList) {
yield obj;
for (const child of iterObjects(obj.children)) {
yield child;
}
}
}
export function getTerrainFile(mission: Mission) {
let terrainBlock;
for (const obj of iterObjects(mission.objects)) {
if (obj.className === "TerrainBlock") {
terrainBlock = obj;
break;
}
}
if (!terrainBlock) {
throw new Error("Error!");
}
return terrainBlock.properties.find(
(prop) => prop.target.name === "terrainFile"
).value;
}

3
src/stringUtils.ts Normal file
View file

@ -0,0 +1,3 @@
export function normalize(pathString: string) {
return pathString.replace(/\\/g, "/").replace(/\/+/g, "/");
}

58
src/terrain.ts Normal file
View file

@ -0,0 +1,58 @@
const SIZE = 256;
const SCALE = 8;
export function parseTerrainBuffer(arrayBuffer: ArrayBufferLike) {
const dataView = new DataView(arrayBuffer);
let offset = 0;
const version = dataView.getUint8(offset++);
const heightMap1d = new Uint16Array(SIZE * SIZE);
const textureNames: string[] = [];
const readString = (length: number) => {
let result = "";
for (let i = 0; i < length; i++) {
const byte = dataView.getUint8(offset + i);
if (byte === 0) break; // Stop at null terminator if present
result += String.fromCharCode(byte);
}
offset += length;
return result;
};
for (let i = 0; i < SIZE * SIZE; i++) {
let height = dataView.getUint16(offset, true);
offset += 2;
heightMap1d[i] = height;
}
offset += 256 * 256;
const heightMap = heightMap1d;
for (let i = 0; i < 8; i++) {
const strSize = dataView.getUint8(offset++);
const textureName = readString(strSize);
if (i < 6 && strSize > 0) {
textureNames.push(textureName);
}
}
const alphaMaps = [];
for (const textureName of textureNames) {
const alphaMap = new Uint8Array(SIZE * SIZE);
for (let j = 0; j < SIZE * SIZE; j++) {
var alphaMats = dataView.getUint8(offset++);
alphaMap[j] = alphaMats;
}
alphaMaps.push(alphaMap);
}
return {
version,
textureNames,
heightMap,
alphaMaps,
};
}