mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-02-27 02:23:52 +00:00
Initial commit
This commit is contained in:
commit
2211ed7650
10117 changed files with 735995 additions and 0 deletions
48
src/arrayUtils.ts
Normal file
48
src/arrayUtils.ts
Normal 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
5
src/interior.ts
Normal 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
34
src/manifest.ts
Normal 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
200
src/mission.ts
Normal 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
3
src/stringUtils.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function normalize(pathString: string) {
|
||||
return pathString.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||
}
|
||||
58
src/terrain.ts
Normal file
58
src/terrain.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue