mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 12:14:47 +00:00
wip
This commit is contained in:
parent
7de0355f3a
commit
ee6ff28af5
|
|
@ -1,8 +1,8 @@
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Tribes 2 Maps",
|
title: "MapGenius – Explore maps for Tribes 2",
|
||||||
description: "Be the map genius.",
|
description: "Tribes 2 forever.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"deploy": "npm run build && git add -f docs && git commit -m \"Deploy\" && git push",
|
"deploy": "npm run build && git add -f docs && git commit -m \"Deploy\" && git push",
|
||||||
"prebuild": "npm run clean && git checkout -- docs && rimraf public/base && mv docs/base public/",
|
"prebuild": "npm run clean && git checkout -- docs && rimraf public/base && mv docs/base public/",
|
||||||
"postbuild": "git checkout -- public/base",
|
"postbuild": "git checkout -- public/base",
|
||||||
|
"manifest": "tsx scripts/manifest.ts",
|
||||||
"start": "next dev"
|
"start": "next dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -32,28 +32,8 @@ if not enabled:
|
||||||
mods = [m.__name__ for m in addon_utils.modules()]
|
mods = [m.__name__ for m in addon_utils.modules()]
|
||||||
die(f"Could not enable '{addon_mod}'. Installed add-ons: {mods}")
|
die(f"Could not enable '{addon_mod}'. Installed add-ons: {mods}")
|
||||||
|
|
||||||
# ---- resolve importer (import_scene.* or wm.*) ----
|
|
||||||
def list_ops(ns):
|
|
||||||
try: return [n for n in dir(ns) if not n.startswith("_")]
|
|
||||||
except: return []
|
|
||||||
|
|
||||||
def resolve_operator():
|
|
||||||
if forced_op:
|
|
||||||
mod, name = forced_op.split(".", 1)
|
|
||||||
return forced_op, getattr(getattr(bpy.ops, mod), name)
|
|
||||||
# auto-discover
|
|
||||||
for modname in ("import_scene", "wm"):
|
|
||||||
names = list_ops(getattr(bpy.ops, modname))
|
|
||||||
hits = [n for n in names if "dif" in n.lower()] or [n for n in names if "torque" in n.lower()]
|
|
||||||
if len(hits) == 1:
|
|
||||||
name = hits[0]
|
|
||||||
return f"{modname}.{name}", getattr(getattr(bpy.ops, modname), name)
|
|
||||||
if len(hits) > 1:
|
|
||||||
raise RuntimeError(f"Multiple candidates: {[f'{modname}.{h}' for h in hits]}. Use --op.")
|
|
||||||
raise RuntimeError("No DIF-like importer found.")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
op_id, op_call = resolve_operator()
|
op_id, op_call = "import_scene.dif", bpy.ops.import_scene.dif
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
die(str(e))
|
die(str(e))
|
||||||
|
|
||||||
|
|
@ -66,7 +46,7 @@ if "FINISHED" not in res: die(f"Import failed via {op_id}: {in_path}")
|
||||||
# ---- export ----
|
# ---- export ----
|
||||||
res = bpy.ops.export_scene.gltf(
|
res = bpy.ops.export_scene.gltf(
|
||||||
filepath=out_path,
|
filepath=out_path,
|
||||||
export_format=export_format, # GLB | GLTF_SEPARATE
|
export_format=export_format, # GLB | GLTF_SEPARATE
|
||||||
use_selection=False,
|
use_selection=False,
|
||||||
export_apply=True,
|
export_apply=True,
|
||||||
)
|
)
|
||||||
53
scripts/blender/dts2gltf.py
Normal file
53
scripts/blender/dts2gltf.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# dts2gltf.py
|
||||||
|
import bpy, sys, os, addon_utils
|
||||||
|
|
||||||
|
def die(msg, code=2):
|
||||||
|
print(f"[dts2gltf] ERROR: {msg}", file=sys.stderr); sys.exit(code)
|
||||||
|
|
||||||
|
# ---- args ----
|
||||||
|
argv = sys.argv
|
||||||
|
if "--" not in argv: die("Usage: blender -b -P dts2gltf.py -- <in.dts> <out.glb|.gltf> [--addon io_scene_dts] [--format GLB|GLTF_SEPARATE]")
|
||||||
|
argv = argv[argv.index("--")+1:]
|
||||||
|
if len(argv) < 2: die("Need <in.dts> and <out.glb|.gltf>")
|
||||||
|
in_path, out_path = map(os.path.abspath, argv[:2])
|
||||||
|
|
||||||
|
addon_mod = "io_scene_dts"
|
||||||
|
forced_op = None
|
||||||
|
export_format = "GLTF_SEPARATE"
|
||||||
|
i = 2
|
||||||
|
while i < len(argv):
|
||||||
|
if argv[i] == "--addon" and i+1 < len(argv): addon_mod = argv[i+1]; i += 2
|
||||||
|
elif argv[i] == "--format" and i+1 < len(argv): export_format = argv[i+1]; i += 2
|
||||||
|
else: die(f"Unknown arg: {argv[i]}")
|
||||||
|
if not os.path.isfile(in_path): die(f"Input not found: {in_path}")
|
||||||
|
|
||||||
|
# ---- reset FIRST (so we don't lose the add-on afterward) ----
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
|
||||||
|
# ---- enable add-on ----
|
||||||
|
addon_utils.enable(addon_mod, default_set=True, handle_error=None)
|
||||||
|
loaded, enabled = addon_utils.check(addon_mod)
|
||||||
|
if not enabled:
|
||||||
|
mods = [m.__name__ for m in addon_utils.modules()]
|
||||||
|
die(f"Could not enable '{addon_mod}'. Installed add-ons: {mods}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
op_id, op_call = "import_scene.dts", bpy.ops.import_scene.dts
|
||||||
|
except Exception as e:
|
||||||
|
die(str(e))
|
||||||
|
|
||||||
|
print(f"[dts2gltf] Using importer: {op_id}")
|
||||||
|
|
||||||
|
# ---- import ----
|
||||||
|
res = op_call(filepath=in_path)
|
||||||
|
if "FINISHED" not in res: die(f"Import failed via {op_id}: {in_path}")
|
||||||
|
|
||||||
|
# ---- export ----
|
||||||
|
res = bpy.ops.export_scene.gltf(
|
||||||
|
filepath=out_path,
|
||||||
|
export_format=export_format, # GLB | GLTF_SEPARATE
|
||||||
|
use_selection=False,
|
||||||
|
export_apply=True,
|
||||||
|
)
|
||||||
|
if "FINISHED" not in res: die(f"Export failed: {out_path}")
|
||||||
|
print(f"[dts2gltf] OK: {in_path} -> {out_path}")
|
||||||
27
scripts/convert-dif.ts
Normal file
27
scripts/convert-dif.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
const blender = `/Applications/Blender.app/Contents/MacOS/Blender`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all .dif files in `public/base` and convert them to glTF.
|
||||||
|
*/
|
||||||
|
async function run() {
|
||||||
|
for await (const inFile of fs.glob("public/base/**/*.dif")) {
|
||||||
|
const outFile = inFile.replace(/\.dif$/i, ".gltf");
|
||||||
|
execFileSync(
|
||||||
|
blender,
|
||||||
|
[
|
||||||
|
"--background",
|
||||||
|
"--python",
|
||||||
|
"scripts/blender/dif2gltf.py",
|
||||||
|
"--", // args after here go to the script
|
||||||
|
inFile,
|
||||||
|
outFile,
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
33
scripts/convert-dts.ts
Normal file
33
scripts/convert-dts.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
const blender = `/Applications/Blender.app/Contents/MacOS/Blender`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all .dts files in `public/base` and convert them to glTF.
|
||||||
|
*/
|
||||||
|
async function run() {
|
||||||
|
for await (const inFile of fs.glob("public/base/**/*.dts")) {
|
||||||
|
const outFile = inFile.replace(/\.dts$/i, ".gltf");
|
||||||
|
execFileSync(
|
||||||
|
blender,
|
||||||
|
[
|
||||||
|
"--background",
|
||||||
|
"--python",
|
||||||
|
"scripts/blender/dts2gltf.py",
|
||||||
|
"--", // args after here go to the script
|
||||||
|
inFile,
|
||||||
|
outFile,
|
||||||
|
"--format",
|
||||||
|
"gltf",
|
||||||
|
// "--scale",
|
||||||
|
// "1.0",
|
||||||
|
// "--no-anims",
|
||||||
|
// "--only-visible",
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import { execFileSync } from "node:child_process";
|
|
||||||
|
|
||||||
const blender = `/Applications/Blender.app/Contents/MacOS/Blender`;
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
for await (const inFile of fs.glob("public/**/*.dif")) {
|
|
||||||
const outFile = inFile.replace(/\.dif$/i, ".gltf");
|
|
||||||
execFileSync(
|
|
||||||
blender,
|
|
||||||
["-b", "-P", "scripts/dif2gltf.py", "--", inFile, outFile],
|
|
||||||
{ stdio: "inherit" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
|
|
@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
import unzipper from "unzipper";
|
import unzipper from "unzipper";
|
||||||
import { normalize } from "@/src/stringUtils";
|
import { normalizePath } from "@/src/stringUtils";
|
||||||
|
|
||||||
const archiveFilePattern = /\.vl2$/i;
|
const archiveFilePattern = /\.vl2$/i;
|
||||||
|
|
||||||
|
|
@ -12,6 +12,33 @@ function isArchive(name: string) {
|
||||||
return archiveFilePattern.test(name);
|
return archiveFilePattern.test(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log and return the manifest of files for the given game asset directory.
|
||||||
|
* The assets used to build the mapper are a filtered set of relevant files
|
||||||
|
* (map related assets) from the `Tribes2/GameData/base` folder. The manifest
|
||||||
|
* consists of the set of unique paths (case sensitive!) represented by the file
|
||||||
|
* tree AND the vl2 files as if they had been unzipped. Thus, each file in the
|
||||||
|
* manifest can have one or more "sources". If the file appears outside of a vl2,
|
||||||
|
* it will have a blank source (the empty string) first. Each vl2 containing the
|
||||||
|
* file will then be listed in order. To resolve an asset, the engine uses a
|
||||||
|
* layering approach where paths inside lexicographically-higher vl2 files win
|
||||||
|
* over the same path outside of a vl2 or in a lexicographically-lower vl2 file.
|
||||||
|
* So, to choose the same final asset as the engine, choose the last source in
|
||||||
|
* the list for any given path.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* "textures/terrainTiles/green.png": ["textures.vl2"],
|
||||||
|
* "textures/lava/ds_iwal01a.png": [
|
||||||
|
* "lava.vl2",
|
||||||
|
* "yHDTextures2.0.vl2",
|
||||||
|
* "zAddOnsVL2s/zDiscord-Map-Pack-4.7.1.vl2"
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
async function buildManifest() {
|
async function buildManifest() {
|
||||||
const fileSources = new Map<string, string[]>();
|
const fileSources = new Map<string, string[]>();
|
||||||
|
|
||||||
|
|
@ -21,7 +48,7 @@ async function buildManifest() {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
})) {
|
})) {
|
||||||
if (entry.isFile()) {
|
if (entry.isFile()) {
|
||||||
const fullPath = normalize(`${entry.parentPath}/${entry.name}`);
|
const fullPath = normalizePath(`${entry.parentPath}/${entry.name}`);
|
||||||
if (isArchive(entry.name)) {
|
if (isArchive(entry.name)) {
|
||||||
archiveFiles.push(fullPath);
|
archiveFiles.push(fullPath);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -31,17 +58,17 @@ async function buildManifest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const filePath of looseFiles) {
|
for (const filePath of looseFiles) {
|
||||||
const relativePath = normalize(path.relative(baseDir, filePath));
|
const relativePath = normalizePath(path.relative(baseDir, filePath));
|
||||||
fileSources.set(relativePath, [""]);
|
fileSources.set(relativePath, [""]);
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveFiles.sort();
|
archiveFiles.sort();
|
||||||
for (const archivePath of archiveFiles) {
|
for (const archivePath of archiveFiles) {
|
||||||
const relativePath = normalize(path.relative(baseDir, archivePath));
|
const relativePath = normalizePath(path.relative(baseDir, archivePath));
|
||||||
const archive = await unzipper.Open.file(archivePath);
|
const archive = await unzipper.Open.file(archivePath);
|
||||||
for (const archiveEntry of archive.files) {
|
for (const archiveEntry of archive.files) {
|
||||||
if (archiveEntry.type === "File") {
|
if (archiveEntry.type === "File") {
|
||||||
const filePath = normalize(archiveEntry.path);
|
const filePath = normalizePath(archiveEntry.path);
|
||||||
const sources = fileSources.get(filePath) ?? [];
|
const sources = fileSources.get(filePath) ?? [];
|
||||||
sources.push(relativePath);
|
sources.push(relativePath);
|
||||||
fileSources.set(filePath, sources);
|
fileSources.set(filePath, sources);
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
export function normalize(pathString: string) {
|
/**
|
||||||
|
* Normalizes a path string, but not as complicated as Node's `path.normalize`.
|
||||||
|
* This simply changes all backslashes to `/` (regardless of platform) and
|
||||||
|
* collapses any adjacent slashes to a single slash.
|
||||||
|
*/
|
||||||
|
export function normalizePath(pathString: string) {
|
||||||
return pathString.replace(/\\/g, "/").replace(/\/+/g, "/");
|
return pathString.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue