mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 20:25:01 +00:00
wip
This commit is contained in:
parent
7de0355f3a
commit
ee6ff28af5
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
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 { 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<string, string[]>();
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, "/");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue