fix title and meta tags, remove more Next.js stuff

This commit is contained in:
Brian Beck 2026-03-13 23:12:28 -07:00
parent 5025065188
commit 842ddb7df7
13 changed files with 17 additions and 774 deletions

11
app/global.d.ts vendored
View file

@ -1,11 +0,0 @@
import type { getMissionList, getMissionInfo } from "@/src/manifest";
import type { DemoRecording } from "@/src/demo/types";
declare global {
interface Window {
setMissionName?: (missionName: string) => void;
getMissionList?: typeof getMissionList;
getMissionInfo?: typeof getMissionInfo;
loadDemoRecording?: (recording: DemoRecording) => void;
}
}

View file

@ -1,27 +0,0 @@
import { ReactNode } from "react";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import "./style.css";
export const metadata = {
title: "MapGenius  Explore maps for Tribes 2",
description: "Tribes 2 forever.",
};
export const viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<NuqsAdapter defaultOptions={{ clearOnDefault: false }}>
{children}
</NuqsAdapter>
</body>
</html>
);
}

View file

@ -1,24 +0,0 @@
"use client";
import { Suspense } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { FeaturesProvider } from "@/src/components/FeaturesProvider";
import { MapInspector } from "@/src/components/MapInspector";
import { SettingsProvider } from "@/src/components/SettingsProvider";
// Three.js has its own loaders for textures and models, but we need to load other
// stuff too, e.g. missions, terrains, and more. This client is used for those.
const queryClient = new QueryClient();
export default function HomePage() {
return (
<Suspense>
<FeaturesProvider>
<QueryClientProvider client={queryClient}>
<SettingsProvider>
<MapInspector />
</SettingsProvider>
</QueryClientProvider>
</FeaturesProvider>
</Suspense>
);
}

View file

@ -1,161 +0,0 @@
.CanvasContainer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
.LoadingIndicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
pointer-events: none;
z-index: 1;
opacity: 0.8;
}
.LoadingIndicator[data-complete="true"] {
animation: loadingComplete 0.3s ease-out forwards;
}
.Spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.2);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes loadingComplete {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.Sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 260px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
color: #fff;
font-size: 13px;
z-index: 2;
display: flex;
flex-direction: column;
overflow: hidden;
}
.SidebarSection {
padding: 10px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.SidebarSection:last-child {
border-bottom: none;
}
.SectionLabel {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 6px;
}
.AnimationList {
flex: 1;
overflow-y: auto;
padding: 0 12px 12px;
}
.AnimationItem {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.AnimationItem:hover {
background: rgba(255, 255, 255, 0.08);
}
.AnimationItem[data-active="true"] {
background: rgba(255, 255, 255, 0.15);
}
.PlayButton {
flex-shrink: 0;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
font-size: 11px;
padding: 0;
}
.PlayButton:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.AnimationItem[data-active="true"] .PlayButton {
background: rgba(100, 180, 255, 0.3);
color: #fff;
}
.AnimationName {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ClipName {
flex-shrink: 0;
font-size: 10px;
color: rgba(255, 255, 255, 0.3);
white-space: nowrap;
}
.CyclicIcon {
flex-shrink: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.3);
title: "Cyclic (looping)";
}
.CheckboxField {
display: flex;
align-items: center;
gap: 6px;
}

View file

@ -1,441 +0,0 @@
"use client";
import {
useState,
useEffect,
useEffectEvent,
useCallback,
Suspense,
useMemo,
} from "react";
import { Canvas, GLProps } from "@react-three/fiber";
import * as THREE from "three";
import { NoToneMapping, SRGBColorSpace, PCFShadowMap } from "three";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { OrbitControls, Center, Bounds, useBounds } from "@react-three/drei";
import { SettingsProvider, useDebug } from "@/src/components/SettingsProvider";
import { ShapeRenderer, useStaticShape } from "@/src/components/GenericShape";
import { ShapeInfoProvider } from "@/src/components/ShapeInfoProvider";
import { DebugElements } from "@/src/components/DebugElements";
import { TickProvider } from "@/src/components/TickProvider";
import { ShapeSelect } from "@/src/components/ShapeSelect";
import { engineStore, useEngineSelector } from "@/src/state/engineStore";
import {
getResourceList,
getResourceMap,
getResourceKey,
getSourceAndPath,
} from "@/src/manifest";
import { createParser, useQueryState } from "nuqs";
import { createScriptLoader } from "@/src/torqueScript/scriptLoader.browser";
import picomatch from "picomatch";
import {
createScriptCache,
type FileSystemHandler,
runServer,
type TorqueObject,
type TorqueRuntime,
} from "@/src/torqueScript";
import styles from "./page.module.css";
import { ignoreScripts } from "@/src/torqueScript/ignoreScripts";
const queryClient = new QueryClient();
const sceneBg = new THREE.Color(0.1, 0.1, 0.1);
const glSettings: GLProps = {
toneMapping: NoToneMapping,
outputColorSpace: SRGBColorSpace,
};
const loadScript = createScriptLoader();
const scriptCache = createScriptCache();
const fileSystem: FileSystemHandler = {
findFiles: (pattern) => {
const isMatch = picomatch(pattern, { nocase: true });
return getResourceList()
.filter((path) => isMatch(path))
.map((resourceKey) => {
const [, actualPath] = getSourceAndPath(resourceKey);
return actualPath;
});
},
isFile: (resourcePath) => {
const resourceKeys = getResourceMap();
const resourceKey = getResourceKey(resourcePath);
return resourceKeys[resourceKey] != null;
},
};
const defaultShape = "deploy_inventory.dts";
const parseAsShape = createParser<string>({
parse: (query: string) => query,
serialize: (value: string) => value,
eq: (a, b) => a === b,
}).withDefault(defaultShape);
/**
* Hook to run the TorqueScript runtime once (hardcoded to SC_Normal/CTF)
* so deploy animations and other script-driven behaviors work.
*/
function useShapeRuntime(): TorqueRuntime | null {
const [runtime, setRuntime] = useState<TorqueRuntime | null>(null);
useEffect(() => {
const controller = new AbortController();
let isDisposed = false;
const { runtime, ready } = runServer({
missionName: "SC_Normal",
missionType: "CTF",
runtimeOptions: {
loadScript,
fileSystem,
cache: scriptCache,
signal: controller.signal,
ignoreScripts,
},
});
void ready
.then(() => {
if (isDisposed || controller.signal.aborted) return;
engineStore.getState().setRuntime(runtime);
setRuntime(runtime);
})
.catch((err) => {
if (err instanceof Error && err.name === "AbortError") return;
console.error("Shape runtime failed:", err);
});
// Seed store immediately
engineStore.getState().setRuntime(runtime);
const unsubscribe = runtime.subscribeRuntimeEvents((event) => {
if (event.type !== "batch.flushed") return;
engineStore.getState().applyRuntimeBatch(event.events, {
tick: event.tick,
});
});
return () => {
isDisposed = true;
controller.abort();
unsubscribe();
engineStore.getState().clearRuntime();
runtime.destroy();
};
}, []);
return runtime;
}
/** Create a minimal TorqueObject for the shape viewer. */
function createFakeObject(
runtime: TorqueRuntime | null,
shapeName: string,
): TorqueObject {
// Try to find a matching datablock for this shape so deploy animations work.
let datablockName: string | undefined;
if (runtime) {
for (const obj of runtime.state.objectsById.values()) {
if (
obj.shapeFile &&
String(obj.shapeFile).toLowerCase() === shapeName.toLowerCase()
) {
datablockName = obj._name;
break;
}
}
}
return {
_id: 99999,
_class: "StaticShapeData",
_className: "StaticShape",
...(datablockName ? { datablock: datablockName } : {}),
} as TorqueObject;
}
function FitOnLoad() {
const bounds = useBounds();
useEffect(() => {
bounds.refresh().fit();
}, [bounds]);
return null;
}
interface AnimationInfo {
name: string;
alias: string | null;
cyclic: boolean | null;
}
/** Reports available animations (with cyclic and alias info when available). */
function AnimationReporter({
shapeName,
onAnimations,
}: {
shapeName: string;
onAnimations: (anims: AnimationInfo[]) => void;
}) {
const gltf = useStaticShape(shapeName);
const shapeAliases = useEngineSelector((state) =>
state.runtime.sequenceAliases.get(shapeName.toLowerCase()),
);
const anims = useMemo(() => {
// Collect cyclic info from vis_sequence nodes on the scene
const visCyclic = new Map<string, boolean>();
gltf.scene.traverse((node: any) => {
const ud = node.userData;
if (ud?.vis_sequence && ud.vis_cyclic != null) {
visCyclic.set(ud.vis_sequence.toLowerCase(), !!ud.vis_cyclic);
}
});
// Build reverse alias map: clip name -> alias
let reverseAliases: Map<string, string> | undefined;
if (shapeAliases) {
reverseAliases = new Map();
for (const [alias, clipName] of shapeAliases) {
reverseAliases.set(clipName, alias);
}
}
return gltf.animations.map((clip) => ({
name: clip.name,
alias: reverseAliases?.get(clip.name.toLowerCase()) ?? null,
cyclic: visCyclic.get(clip.name.toLowerCase()) ?? null,
}));
}, [gltf, shapeAliases]);
const reportAnimations = useEffectEvent(onAnimations);
useEffect(() => {
reportAnimations(anims);
}, [anims]);
return null;
}
/** Plays the selected animation via the TorqueScript runtime. */
function AnimationPlayer({
object,
runtime,
animation,
}: {
object: TorqueObject;
runtime: TorqueRuntime | null;
animation: string;
}) {
useEffect(() => {
if (!runtime || !animation) return;
// Use nsCall to dispatch directly on the ShapeBase namespace, bypassing
// class chain resolution (the fake object's _className won't resolve
// to ShapeBase through the namespace parent chain).
for (let slot = 0; slot < 4; slot++) {
runtime.$.nsCall("ShapeBase", "stopThread", object, slot);
}
runtime.$.nsCall("ShapeBase", "playThread", object, 0, animation);
return () => {
for (let slot = 0; slot < 4; slot++) {
runtime.$.nsCall("ShapeBase", "stopThread", object, slot);
}
};
}, [runtime, object, animation]);
return null;
}
function ShapeViewer({
shapeName,
runtime,
onAnimations,
selectedAnimation,
}: {
shapeName: string;
runtime: TorqueRuntime | null;
onAnimations: (anims: AnimationInfo[]) => void;
selectedAnimation: string;
}) {
const object = useMemo(
() => createFakeObject(runtime, shapeName),
[runtime, shapeName],
);
return (
<ShapeInfoProvider type="StaticShape" object={object} shapeName={shapeName}>
<Center>
<ShapeRenderer />
<AnimationReporter shapeName={shapeName} onAnimations={onAnimations} />
<AnimationPlayer
object={object}
runtime={runtime}
animation={selectedAnimation}
/>
<FitOnLoad />
</Center>
</ShapeInfoProvider>
);
}
function SceneLighting() {
return (
<>
<ambientLight intensity={0.6} />
<directionalLight position={[50, 80, 30]} intensity={1.2} />
</>
);
}
function ShapeInspector() {
const [currentShape, setCurrentShape] = useQueryState("shape", parseAsShape);
const runtime = useShapeRuntime();
const [availableAnimations, setAvailableAnimations] = useState<
AnimationInfo[]
>([]);
const [selectedAnimation, setSelectedAnimation] = useState<string>("");
const handleAnimations = useCallback((anims: AnimationInfo[]) => {
setAvailableAnimations(anims);
setSelectedAnimation("");
}, []);
const [showLoading, setShowLoading] = useState(true);
useEffect(() => {
if (runtime) {
const timer = setTimeout(() => setShowLoading(false), 300);
return () => clearTimeout(timer);
}
}, [runtime]);
return (
<QueryClientProvider client={queryClient}>
<main>
<SettingsProvider>
<div className={styles.CanvasContainer}>
{showLoading && (
<div
className={styles.LoadingIndicator}
data-complete={!!runtime}
>
<div className={styles.Spinner} />
</div>
)}
<Canvas
frameloop="always"
gl={glSettings}
shadows={{ type: PCFShadowMap }}
scene={{ background: sceneBg }}
camera={{ position: [5, 3, 5], fov: 90 }}
>
<TickProvider>
<SceneLighting />
<Bounds fit clip observe margin={1.5}>
<Suspense>
<ShapeViewer
key={currentShape}
shapeName={currentShape}
runtime={runtime}
onAnimations={handleAnimations}
selectedAnimation={selectedAnimation}
/>
</Suspense>
</Bounds>
<DebugElements />
<OrbitControls makeDefault />
</TickProvider>
</Canvas>
</div>
<ShapeControls
currentShape={currentShape}
onChangeShape={setCurrentShape}
animations={availableAnimations}
selectedAnimation={selectedAnimation}
onChangeAnimation={setSelectedAnimation}
/>
</SettingsProvider>
</main>
</QueryClientProvider>
);
}
function ShapeControls({
currentShape,
onChangeShape,
animations,
selectedAnimation,
onChangeAnimation,
}: {
currentShape: string;
onChangeShape: (shape: string) => void;
animations: AnimationInfo[];
selectedAnimation: string;
onChangeAnimation: (name: string) => void;
}) {
const { debugMode, setDebugMode } = useDebug();
return (
<div className={styles.Sidebar}>
<div className={styles.SidebarSection}>
<ShapeSelect value={currentShape} onChange={onChangeShape} />
</div>
<div className={styles.SidebarSection}>
<div className={styles.CheckboxField}>
<input
id="debugInput"
type="checkbox"
checked={debugMode}
onChange={(e) => setDebugMode(e.target.checked)}
/>
<label htmlFor="debugInput">Debug</label>
</div>
</div>
{animations.length > 0 && (
<>
<div className={styles.SidebarSection}>
<div className={styles.SectionLabel}>Animations</div>
</div>
<div className={styles.AnimationList}>
{animations.map((anim) => (
<div
key={anim.name}
className={styles.AnimationItem}
data-active={selectedAnimation === anim.name}
onClick={() =>
onChangeAnimation(
selectedAnimation === anim.name ? "" : anim.name,
)
}
>
<button
className={styles.PlayButton}
title={`Play ${anim.alias ?? anim.name}`}
>
{selectedAnimation === anim.name ? "\u25A0" : "\u25B6"}
</button>
<span className={styles.AnimationName}>
{anim.alias ?? anim.name}
</span>
{anim.alias && (
<span
className={styles.ClipName}
title={`GLB clip: ${anim.name}`}
>
{anim.name}
</span>
)}
{anim.cyclic === true && (
<span className={styles.CyclicIcon} title="Cyclic (looping)">
{"\u221E"}
</span>
)}
</div>
))}
</div>
</>
)}
</div>
);
}
export default function ShapesPage() {
return (
<Suspense>
<ShapeInspector />
</Suspense>
);
}

View file

@ -1,44 +0,0 @@
html {
box-sizing: border-box;
margin: 0;
padding: 0;
background: black;
overflow: hidden;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
user-select: none;
-webkit-touch-callout: none;
}
html {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: 100%;
}
body {
margin: 0;
padding: 0;
overflow: hidden;
}
input[type="range"] {
max-width: 80px;
}

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1,10 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>t2-mapper</title>
<meta charset="utf-8" />
<title>MapGenius  Explore maps for Tribes 2</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="description" content="Tribes 2 forever." />
<link rel="icon" type="image/png" href="/t2-mapper/icon.png" />
<script type="module" crossorigin src="/t2-mapper/assets/index-ClGJzuqQ.js"></script>
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/chunk-DECur_0Z.js">
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/logger-DePRU8Hm.js">

View file

@ -1,10 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>t2-mapper</title>
<meta charset="utf-8" />
<title>MapGenius  Explore maps for Tribes 2</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="description" content="Tribes 2 forever." />
<link rel="icon" type="image/png" href="/icon.png" />
</head>
<body>
<div id="root"></div>

6
next-env.d.ts vendored
View file

@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -1,52 +0,0 @@
import { NextConfig } from "next";
import { PHASE_DEVELOPMENT_SERVER } from "next/constants";
const nextConfig = (phase, { defaultConfig }): NextConfig => {
return {
// Suppress static export config warnings in dev mode as they are not relevant.
output: phase === PHASE_DEVELOPMENT_SERVER ? undefined : "export",
distDir: "./docs",
basePath: "/t2-mapper",
assetPrefix: "/t2-mapper/",
trailingSlash: true,
reactCompiler: true,
experimental: { viewTransition: true },
headers:
// TorqueScript files should be served as text. This won't affect what
// GitHub Pages does with the static export, but it'll at least improve
// the dev server. Otherwise, the responses can't be easily inspected in
// the Network tab.
phase === PHASE_DEVELOPMENT_SERVER
? async () => {
return [
{
source: "/:path*.cs",
headers: [
{
key: "Content-Type",
value: "text/plain; charset=utf-8",
},
],
},
];
}
: undefined,
// For the dev server, redirect / to the `basePath` for convenience, so you
// can just open localhost:3000.
redirects:
phase === PHASE_DEVELOPMENT_SERVER
? async () => {
return [
{
source: "/",
destination: "/t2-mapper/",
basePath: false,
permanent: false,
},
];
}
: undefined,
};
};
export default nextConfig;

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -5,6 +5,7 @@ import babel from "@rolldown/plugin-babel";
// https://vite.dev/config/
export default defineConfig({
base: "/t2-mapper/",
server: { port: 3000 },
build: {
outDir: "docs",
emptyOutDir: false,