mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-12 15:04:47 +00:00
Fix resource lookup; re-export .dts and .dif with custom properties
This commit is contained in:
parent
8147a1c418
commit
1182726bb4
1156 changed files with 126 additions and 114 deletions
|
|
@ -1,6 +1,10 @@
|
|||
import { memo, Suspense, useMemo } from "react";
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { BASE_URL, shapeTextureToUrl, shapeToUrl } from "../loaders";
|
||||
import {
|
||||
FALLBACK_TEXTURE_URL,
|
||||
shapeTextureToUrl,
|
||||
shapeToUrl,
|
||||
} from "../loaders";
|
||||
import { filterGeometryByVertexGroups, getHullBoneIndices } from "../meshUtils";
|
||||
import {
|
||||
createAlphaAsRoughnessMaterial,
|
||||
|
|
@ -12,8 +16,6 @@ import { useDebug } from "./SettingsProvider";
|
|||
import { useShapeInfo } from "./ShapeInfoProvider";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
|
||||
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
||||
|
||||
/**
|
||||
* Load a .glb file that was converted from a .dts, used for static shapes.
|
||||
*/
|
||||
|
|
@ -29,7 +31,22 @@ export function ShapeTexture({
|
|||
material?: MeshStandardMaterial;
|
||||
shapeName?: string;
|
||||
}) {
|
||||
const url = shapeTextureToUrl(material.name, FALLBACK_URL);
|
||||
const url = useMemo(() => {
|
||||
const flagNames = new Set(material.userData.flag_names ?? []);
|
||||
const isIflMaterial = flagNames.has("IflMaterial");
|
||||
const resourcePath = material.userData.resource_path;
|
||||
if (!resourcePath) {
|
||||
console.warn(
|
||||
`Material index out of range on shape "${shapeName}" - rendering fallback.`,
|
||||
);
|
||||
}
|
||||
return resourcePath && !isIflMaterial
|
||||
? // Use custom `resource_path` added by forked io_dts3d Blender add-on
|
||||
shapeTextureToUrl(resourcePath)
|
||||
: // Not supported yet
|
||||
FALLBACK_TEXTURE_URL;
|
||||
}, [material]);
|
||||
|
||||
const isOrganic = shapeName && /borg|xorg|porg|dorg/i.test(shapeName);
|
||||
|
||||
const texture = useTexture(url, (texture) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Fragment, useMemo } from "react";
|
||||
import { getMissionInfo, getMissionList, getSource } from "../manifest";
|
||||
import { getMissionInfo, getMissionList, getSourceAndPath } from "../manifest";
|
||||
import { useControls, useDebug, useSettings } from "./SettingsProvider";
|
||||
import orderBy from "lodash.orderby";
|
||||
|
||||
|
|
@ -27,8 +27,8 @@ const sourceGroupNames = {
|
|||
const groupedMissions = getMissionList().reduce(
|
||||
(groupMap, missionName) => {
|
||||
const missionInfo = getMissionInfo(missionName);
|
||||
const source = getSource(missionInfo.resourcePath);
|
||||
const groupName = sourceGroupNames[source] ?? null;
|
||||
const [sourcePath] = getSourceAndPath(missionInfo.resourcePath);
|
||||
const groupName = sourceGroupNames[sourcePath] ?? null;
|
||||
const groupMissions = groupMap.get(groupName) ?? [];
|
||||
if (!excludeMissions.has(missionName)) {
|
||||
groupMissions.push({
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ import { memo, Suspense, useMemo } from "react";
|
|||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Mesh } from "three";
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { BASE_URL, interiorTextureToUrl, interiorToUrl } from "../loaders";
|
||||
import { interiorTextureToUrl, interiorToUrl } from "../loaders";
|
||||
import type { TorqueObject } from "../torqueScript";
|
||||
import { getPosition, getProperty, getRotation, getScale } from "../mission";
|
||||
import { setupColor } from "../textureUtils";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import { useDebug } from "./SettingsProvider";
|
||||
|
||||
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
||||
|
||||
/**
|
||||
* Load a .gltf file that was converted from a .dif, used for "interior" models.
|
||||
*/
|
||||
|
|
@ -20,7 +18,7 @@ function useInterior(interiorFile: string) {
|
|||
}
|
||||
|
||||
function InteriorTexture({ materialName }: { materialName: string }) {
|
||||
const url = interiorTextureToUrl(materialName, FALLBACK_URL);
|
||||
const url = interiorTextureToUrl(materialName);
|
||||
const texture = useTexture(url, (texture) => setupColor(texture));
|
||||
|
||||
return <meshStandardMaterial map={texture} side={2} />;
|
||||
|
|
@ -39,10 +37,15 @@ function InteriorMesh({ node }: { node: Mesh }) {
|
|||
>
|
||||
{Array.isArray(node.material) ? (
|
||||
node.material.map((mat, index) => (
|
||||
<InteriorTexture key={index} materialName={mat.name} />
|
||||
<InteriorTexture
|
||||
key={index}
|
||||
materialName={mat.userData.resource_path}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<InteriorTexture materialName={node.material.name} />
|
||||
<InteriorTexture
|
||||
materialName={node.material.userData.resource_path}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,70 @@
|
|||
import { parseImageFileList } from "./imageFileList";
|
||||
import { getActualResourcePath, getMissionInfo, getSource } from "./manifest";
|
||||
import {
|
||||
getActualResourceKey,
|
||||
getMissionInfo,
|
||||
getSourceAndPath,
|
||||
} from "./manifest";
|
||||
import { parseMissionScript } from "./mission";
|
||||
import { normalizePath } from "./stringUtils";
|
||||
import { parseTerrainBuffer } from "./terrain";
|
||||
|
||||
export const BASE_URL = "/t2-mapper";
|
||||
export const RESOURCE_ROOT_URL = `${BASE_URL}/base/`;
|
||||
export const FALLBACK_TEXTURE_URL = `${BASE_URL}/magenta.png`;
|
||||
|
||||
export function getUrlForPath(resourcePath: string, fallbackUrl?: string) {
|
||||
resourcePath = getActualResourcePath(resourcePath);
|
||||
let sourcePath: string;
|
||||
let resourceKey;
|
||||
try {
|
||||
sourcePath = getSource(resourcePath);
|
||||
resourceKey = getActualResourceKey(resourcePath);
|
||||
} catch (err) {
|
||||
if (fallbackUrl) {
|
||||
// console.error(err);
|
||||
console.warn(
|
||||
`Resource "${resourcePath}" not found - rendering fallback.`,
|
||||
);
|
||||
return fallbackUrl;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (!sourcePath) {
|
||||
return `${RESOURCE_ROOT_URL}${resourcePath}`;
|
||||
const [sourcePath, actualPath] = getSourceAndPath(resourceKey);
|
||||
if (sourcePath) {
|
||||
return `${RESOURCE_ROOT_URL}@vl2/${sourcePath}/${actualPath}`;
|
||||
} else {
|
||||
return `${RESOURCE_ROOT_URL}@vl2/${sourcePath}/${resourcePath}`;
|
||||
return `${RESOURCE_ROOT_URL}${actualPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function interiorToUrl(name: string) {
|
||||
const difUrl = getUrlForPath(`interiors/${name}`);
|
||||
return difUrl.replace(/\.dif$/i, ".glb");
|
||||
const url = getUrlForPath(`interiors/${name}`);
|
||||
return url.replace(/\.dif$/i, ".glb");
|
||||
}
|
||||
|
||||
export function shapeToUrl(name: string) {
|
||||
const difUrl = getUrlForPath(`shapes/${name}`);
|
||||
return difUrl.replace(/\.dts$/i, ".glb");
|
||||
const url = getUrlForPath(`shapes/${name}`);
|
||||
return url.replace(/\.dts$/i, ".glb");
|
||||
}
|
||||
|
||||
export function terrainTextureToUrl(name: string) {
|
||||
name = name.replace(/^terrain\./, "");
|
||||
return getUrlForPath(`textures/terrain/${name}.png`, `${BASE_URL}/black.png`);
|
||||
return getUrlForPath(`textures/terrain/${name}.png`, FALLBACK_TEXTURE_URL);
|
||||
}
|
||||
|
||||
export function interiorTextureToUrl(name: string, fallbackUrl?: string) {
|
||||
name = name.replace(/\.\d+$/, "");
|
||||
return getUrlForPath(`textures/${name}.png`, fallbackUrl);
|
||||
export function interiorTextureToUrl(name: string) {
|
||||
// name = name.replace(/\.\d+$/, "");
|
||||
return getUrlForPath(`textures/${name}.png`, FALLBACK_TEXTURE_URL);
|
||||
}
|
||||
|
||||
export function textureFrameToUrl(fileName: string) {
|
||||
return getUrlForPath(`textures/skins/${fileName}`);
|
||||
return getUrlForPath(`textures/skins/${fileName}`, FALLBACK_TEXTURE_URL);
|
||||
}
|
||||
|
||||
export function shapeTextureToUrl(name: string, fallbackUrl?: string) {
|
||||
name = name.replace(/^skins\\/, "");
|
||||
name = name.replace(/\.\d+$/, "");
|
||||
return getUrlForPath(`textures/skins/${name}.png`, fallbackUrl);
|
||||
export function shapeTextureToUrl(name: string) {
|
||||
// name = name.replace(/\.\d+$/, "");
|
||||
return getUrlForPath(`textures/${name}.png`, FALLBACK_TEXTURE_URL);
|
||||
}
|
||||
|
||||
export function textureToUrl(name: string) {
|
||||
try {
|
||||
return getUrlForPath(`textures/${name}.png`);
|
||||
} catch (err) {
|
||||
return `${BASE_URL}/black.png`;
|
||||
}
|
||||
return getUrlForPath(`textures/${name}.png`, FALLBACK_TEXTURE_URL);
|
||||
}
|
||||
|
||||
export function audioToUrl(fileName: string) {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import untypedManifest from "@/public/manifest.json";
|
|||
import { normalizePath } from "./stringUtils";
|
||||
|
||||
// Source tuple: [sourcePath] or [sourcePath, actualPath] if casing differs
|
||||
type SourceTuple = [string] | [string, string];
|
||||
type SourceTuple =
|
||||
| [sourcePath: string]
|
||||
| [sourcePath: string, actualPath: string];
|
||||
// Resource entry: [firstSeenPath, ...sourceTuples]
|
||||
type ResourceEntry = [string, ...SourceTuple[]];
|
||||
type ResourceEntry = [firstSeenPath: string, ...SourceTuple[]];
|
||||
|
||||
/**
|
||||
* Manifest format: keys are normalized (lowercased) paths, values are
|
||||
* Manifest format: keys are normalized (lowercased, forward-slash) paths,
|
||||
* values are ResourceEntry arrays (see above):
|
||||
* [firstSeenPath, ...sourceTuples] where each source tuple is either:
|
||||
* - [sourcePath] if the file has the same casing as firstSeenPath
|
||||
* - [sourcePath, actualPath] if the file has different casing in that source
|
||||
|
|
@ -24,79 +27,68 @@ const manifest = untypedManifest as unknown as {
|
|||
>;
|
||||
};
|
||||
|
||||
function normalizeKey(resourcePath: string): string {
|
||||
function getResourceKey(resourcePath: string): string {
|
||||
return normalizePath(resourcePath).toLowerCase();
|
||||
}
|
||||
|
||||
function getEntry(resourcePath: string): ResourceEntry | undefined {
|
||||
return manifest.resources[normalizeKey(resourcePath)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source vl2 archive for a resource (or empty string for loose files).
|
||||
* Returns the last/winning source since later vl2s override earlier ones.
|
||||
*/
|
||||
export function getSource(resourcePath: string): string {
|
||||
const entry = getEntry(resourcePath);
|
||||
if (entry && entry.length > 1) {
|
||||
const lastSourceTuple = entry[entry.length - 1] as SourceTuple;
|
||||
return lastSourceTuple[0];
|
||||
} else {
|
||||
throw new Error(`Resource not found in manifest: ${resourcePath}`);
|
||||
}
|
||||
export function getSourceAndPath(
|
||||
resourceKey: string,
|
||||
): [sourceName: string, pathInSource: string] {
|
||||
const entry = manifest.resources[resourceKey];
|
||||
const [firstSeenPath, ...sources] = entry;
|
||||
const [sourcePath, actualPath] = sources[sources.length - 1];
|
||||
return [sourcePath, actualPath ?? firstSeenPath];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual resource path with its original casing as seen in the filesystem.
|
||||
* This handles case-insensitive lookups by normalizing the input path.
|
||||
* Given a file path, check the manifest for an exact match (but case insensitive),
|
||||
* followed by removing numeric suffixes and certain intermediate directories.
|
||||
* Return the normalized resource path that was found.
|
||||
*
|
||||
* FIXME: Figure out how T2/Torque actually resolves these.
|
||||
*/
|
||||
export function getActualResourcePath(resourcePath: string): string {
|
||||
const entry = getEntry(resourcePath);
|
||||
if (entry) {
|
||||
return entry[0]; // First element is the first-seen casing
|
||||
export function getActualResourceKey(resourcePath: string): string {
|
||||
const resourceKey = getResourceKey(resourcePath);
|
||||
|
||||
if (manifest.resources[resourceKey]) {
|
||||
return resourceKey;
|
||||
}
|
||||
|
||||
// Fallback: try stripping numeric suffixes (e.g., "generator0.png" -> "generator.png")
|
||||
const pathWithoutNumber = resourcePath.replace(/\d+(\.(png))$/i, "$1");
|
||||
if (pathWithoutNumber !== resourcePath) {
|
||||
const entryWithoutNumber = getEntry(pathWithoutNumber);
|
||||
if (entryWithoutNumber) {
|
||||
return entryWithoutNumber[0];
|
||||
}
|
||||
const keyWithoutNumber = resourceKey.replace(/\d+(\.(png))$/i, "$1");
|
||||
if (manifest.resources[keyWithoutNumber]) {
|
||||
return keyWithoutNumber;
|
||||
}
|
||||
|
||||
// Fallback: try nested texture paths
|
||||
const normalized = normalizeKey(resourcePath);
|
||||
if (normalized.startsWith("textures/")) {
|
||||
for (const key of Object.keys(manifest.resources)) {
|
||||
const stripped = key.replace(
|
||||
/^(textures\/)((lush|desert|badlands|lava|ice|jaggedclaw|terraintiles)\/)/,
|
||||
"$1",
|
||||
);
|
||||
if (stripped === normalized) {
|
||||
return manifest.resources[key][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
// // Fallback: try nested texture paths
|
||||
// if (resourcePath.startsWith("textures/")) {
|
||||
// for (const key of getResourceList()) {
|
||||
// const stripped = key.replace(
|
||||
// /^(textures\/)((lush|desert|badlands|lava|ice|jaggedclaw|terraintiles)\/)/,
|
||||
// "$1",
|
||||
// );
|
||||
// if (stripped === normalized) {
|
||||
// return manifest.resources[key][0];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return resourcePath;
|
||||
throw new Error(`Resource not found in manifest: ${resourcePath}`);
|
||||
}
|
||||
|
||||
export function getResourceList(): string[] {
|
||||
return Object.keys(manifest.resources);
|
||||
}
|
||||
|
||||
export function getFilePath(resourcePath: string): string {
|
||||
const entry = getEntry(resourcePath);
|
||||
if (!entry) {
|
||||
return `docs/base/${resourcePath}`;
|
||||
}
|
||||
const [firstSeenPath, ...sourceTuples] = entry;
|
||||
const lastSourceTuple = sourceTuples[sourceTuples.length - 1];
|
||||
const lastSource = lastSourceTuple[0];
|
||||
const actualPath = lastSourceTuple[1] ?? firstSeenPath;
|
||||
if (lastSource) {
|
||||
return `docs/base/@vl2/${lastSource}/${actualPath}`;
|
||||
export function getLocalFilePath(resourcePath: string): string {
|
||||
const resourceKey = getResourceKey(resourcePath);
|
||||
const [sourcePath, actualPath] = getSourceAndPath(resourceKey);
|
||||
if (sourcePath) {
|
||||
return `docs/base/@vl2/${sourcePath}/${actualPath}`;
|
||||
} else {
|
||||
return `docs/base/${actualPath}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue