mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 12:14:47 +00:00
185 lines
5 KiB
TypeScript
185 lines
5 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import picomatch from "picomatch";
|
|
import { loadMission } from "../loaders";
|
|
import { type ParsedMission } from "../mission";
|
|
import { createScriptLoader } from "../torqueScript/scriptLoader.browser";
|
|
import { SimObject } from "./SimObject";
|
|
import { memo, useEffect, useMemo, useState } from "react";
|
|
import { RuntimeProvider } from "./RuntimeProvider";
|
|
import {
|
|
createProgressTracker,
|
|
createScriptCache,
|
|
FileSystemHandler,
|
|
runServer,
|
|
TorqueObject,
|
|
TorqueRuntime,
|
|
} from "../torqueScript";
|
|
import {
|
|
getResourceKey,
|
|
getResourceList,
|
|
getResourceMap,
|
|
getSourceAndPath,
|
|
} from "../manifest";
|
|
import { MissionProvider } from "./MissionContext";
|
|
|
|
const loadScript = createScriptLoader();
|
|
// Shared cache for parsed scripts - survives runtime restarts
|
|
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;
|
|
},
|
|
};
|
|
|
|
function useParsedMission(name: string) {
|
|
return useQuery({
|
|
queryKey: ["parsedMission", name],
|
|
queryFn: () => loadMission(name),
|
|
});
|
|
}
|
|
|
|
interface ExecutedMissionState {
|
|
missionGroup: TorqueObject | undefined;
|
|
runtime: TorqueRuntime | undefined;
|
|
progress: number;
|
|
}
|
|
|
|
function useExecutedMission(
|
|
missionName: string,
|
|
missionType: string,
|
|
parsedMission: ParsedMission | undefined,
|
|
): ExecutedMissionState {
|
|
const [state, setState] = useState<ExecutedMissionState>({
|
|
missionGroup: undefined,
|
|
runtime: undefined,
|
|
progress: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!parsedMission) {
|
|
return;
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
|
|
// Create progress tracker and update state on changes
|
|
const progressTracker = createProgressTracker();
|
|
const handleProgress = () => {
|
|
setState((prev) => ({ ...prev, progress: progressTracker.progress }));
|
|
};
|
|
progressTracker.on("update", handleProgress);
|
|
|
|
const { runtime } = runServer({
|
|
missionName,
|
|
missionType,
|
|
runtimeOptions: {
|
|
loadScript,
|
|
fileSystem,
|
|
cache: scriptCache,
|
|
signal: controller.signal,
|
|
progress: progressTracker,
|
|
ignoreScripts: [
|
|
"scripts/admin.cs",
|
|
// `ignoreScripts` supports globs, but out of an abundance of caution
|
|
// we don't want to do `ai*.cs` in case there's some non-AI related
|
|
// word like "air" in a script name.
|
|
"scripts/ai.cs",
|
|
"scripts/aiBotProfiles.cs",
|
|
"scripts/aiBountyGame.cs",
|
|
"scripts/aiChat.cs",
|
|
"scripts/aiCnH.cs",
|
|
"scripts/aiCTF.cs",
|
|
"scripts/aiDeathMatch.cs",
|
|
"scripts/aiDebug.cs",
|
|
"scripts/aiDefaultTasks.cs",
|
|
"scripts/aiDnD.cs",
|
|
"scripts/aiHumanTasks.cs",
|
|
"scripts/aiHunters.cs",
|
|
"scripts/aiInventory.cs",
|
|
"scripts/aiObjectiveBuilder.cs",
|
|
"scripts/aiObjectives.cs",
|
|
"scripts/aiRabbit.cs",
|
|
"scripts/aiSiege.cs",
|
|
"scripts/aiTDM.cs",
|
|
"scripts/aiTeamHunters.cs",
|
|
"scripts/deathMessages.cs",
|
|
"scripts/graphBuild.cs",
|
|
"scripts/navGraph.cs",
|
|
"scripts/serverTasks.cs",
|
|
"scripts/spdialog.cs",
|
|
],
|
|
},
|
|
onMissionLoadDone: () => {
|
|
const missionGroup = runtime.getObjectByName("MissionGroup");
|
|
setState({ missionGroup, runtime, progress: 1 });
|
|
},
|
|
});
|
|
|
|
return () => {
|
|
progressTracker.off("update", handleProgress);
|
|
controller.abort();
|
|
runtime.destroy();
|
|
};
|
|
}, [missionName, parsedMission]);
|
|
|
|
return state;
|
|
}
|
|
|
|
interface MissionProps {
|
|
name: string;
|
|
missionType: string;
|
|
setMissionType: (type: string) => void;
|
|
onLoadingChange?: (isLoading: boolean, progress?: number) => void;
|
|
}
|
|
|
|
export const Mission = memo(function Mission({
|
|
name,
|
|
missionType,
|
|
onLoadingChange,
|
|
}: MissionProps) {
|
|
const { data: parsedMission } = useParsedMission(name);
|
|
|
|
const { missionGroup, runtime, progress } = useExecutedMission(
|
|
name,
|
|
missionType,
|
|
parsedMission,
|
|
);
|
|
const isLoading = !parsedMission || !missionGroup || !runtime;
|
|
|
|
const missionContext = useMemo(
|
|
() => ({
|
|
metadata: parsedMission,
|
|
missionType,
|
|
missionGroup,
|
|
}),
|
|
[parsedMission, missionType, missionGroup],
|
|
);
|
|
|
|
useEffect(() => {
|
|
onLoadingChange?.(isLoading, progress);
|
|
}, [isLoading, progress, onLoadingChange]);
|
|
|
|
if (isLoading) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<MissionProvider value={missionContext}>
|
|
<RuntimeProvider runtime={runtime}>
|
|
<SimObject object={missionGroup} />
|
|
</RuntimeProvider>
|
|
</MissionProvider>
|
|
);
|
|
});
|