From ee6ff28af5a0cd2b607d4d122e4481ec9c863390 Mon Sep 17 00:00:00 2001 From: Brian Beck Date: Wed, 12 Nov 2025 19:22:29 -0800 Subject: [PATCH] wip --- .nvmrc | 1 + app/layout.tsx | 4 +-- package.json | 1 + scripts/{ => blender}/dif2gltf.py | 24 ++------------ scripts/blender/dts2gltf.py | 53 +++++++++++++++++++++++++++++++ scripts/convert-dif.ts | 27 ++++++++++++++++ scripts/convert-dts.ts | 33 +++++++++++++++++++ scripts/convert.ts | 17 ---------- scripts/manifest.ts | 37 ++++++++++++++++++--- src/arrayUtils.ts | 48 ---------------------------- src/stringUtils.ts | 7 +++- 11 files changed, 157 insertions(+), 95 deletions(-) create mode 100644 .nvmrc rename scripts/{ => blender}/dif2gltf.py (65%) create mode 100644 scripts/blender/dts2gltf.py create mode 100644 scripts/convert-dif.ts create mode 100644 scripts/convert-dts.ts delete mode 100644 scripts/convert.ts delete mode 100644 src/arrayUtils.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..d9133a54 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.0 diff --git a/app/layout.tsx b/app/layout.tsx index 7d9b004e..710b1a1f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,8 @@ import "./style.css"; export const metadata = { - title: "Tribes 2 Maps", - description: "Be the map genius.", + title: "MapGenius – Explore maps for Tribes 2", + description: "Tribes 2 forever.", }; export default function RootLayout({ diff --git a/package.json b/package.json index 0ed23347..7404ec63 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "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/", "postbuild": "git checkout -- public/base", + "manifest": "tsx scripts/manifest.ts", "start": "next dev" }, "dependencies": { diff --git a/scripts/dif2gltf.py b/scripts/blender/dif2gltf.py similarity index 65% rename from scripts/dif2gltf.py rename to scripts/blender/dif2gltf.py index 07131948..0893e3cf 100644 --- a/scripts/dif2gltf.py +++ b/scripts/blender/dif2gltf.py @@ -32,28 +32,8 @@ if not enabled: mods = [m.__name__ for m in addon_utils.modules()] 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: - op_id, op_call = resolve_operator() + op_id, op_call = "import_scene.dif", bpy.ops.import_scene.dif except Exception as e: die(str(e)) @@ -66,7 +46,7 @@ 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 + export_format=export_format, # GLB | GLTF_SEPARATE use_selection=False, export_apply=True, ) diff --git a/scripts/blender/dts2gltf.py b/scripts/blender/dts2gltf.py new file mode 100644 index 00000000..977f3bf6 --- /dev/null +++ b/scripts/blender/dts2gltf.py @@ -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 -- [--addon io_scene_dts] [--format GLB|GLTF_SEPARATE]") +argv = argv[argv.index("--")+1:] +if len(argv) < 2: die("Need and ") +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}") diff --git a/scripts/convert-dif.ts b/scripts/convert-dif.ts new file mode 100644 index 00000000..f115216c --- /dev/null +++ b/scripts/convert-dif.ts @@ -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(); diff --git a/scripts/convert-dts.ts b/scripts/convert-dts.ts new file mode 100644 index 00000000..b9901784 --- /dev/null +++ b/scripts/convert-dts.ts @@ -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(); diff --git a/scripts/convert.ts b/scripts/convert.ts deleted file mode 100644 index 5249af3d..00000000 --- a/scripts/convert.ts +++ /dev/null @@ -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(); diff --git a/scripts/manifest.ts b/scripts/manifest.ts index c01360d2..b9bf67b5 100644 --- a/scripts/manifest.ts +++ b/scripts/manifest.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { parseArgs } from "node:util"; import unzipper from "unzipper"; -import { normalize } from "@/src/stringUtils"; +import { normalizePath } from "@/src/stringUtils"; const archiveFilePattern = /\.vl2$/i; @@ -12,6 +12,33 @@ function isArchive(name: string) { 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() { const fileSources = new Map(); @@ -21,7 +48,7 @@ async function buildManifest() { withFileTypes: true, })) { if (entry.isFile()) { - const fullPath = normalize(`${entry.parentPath}/${entry.name}`); + const fullPath = normalizePath(`${entry.parentPath}/${entry.name}`); if (isArchive(entry.name)) { archiveFiles.push(fullPath); } else { @@ -31,17 +58,17 @@ async function buildManifest() { } for (const filePath of looseFiles) { - const relativePath = normalize(path.relative(baseDir, filePath)); + const relativePath = normalizePath(path.relative(baseDir, filePath)); fileSources.set(relativePath, [""]); } archiveFiles.sort(); 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); for (const archiveEntry of archive.files) { if (archiveEntry.type === "File") { - const filePath = normalize(archiveEntry.path); + const filePath = normalizePath(archiveEntry.path); const sources = fileSources.get(filePath) ?? []; sources.push(relativePath); fileSources.set(filePath, sources); diff --git a/src/arrayUtils.ts b/src/arrayUtils.ts deleted file mode 100644 index e55d7b42..00000000 --- a/src/arrayUtils.ts +++ /dev/null @@ -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; -} diff --git a/src/stringUtils.ts b/src/stringUtils.ts index 7b975d94..0a086d37 100644 --- a/src/stringUtils.ts +++ b/src/stringUtils.ts @@ -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, "/"); }