add loading progress support and new indicator

This commit is contained in:
Brian Beck 2025-12-04 14:24:51 -08:00
parent 8e6ae456f0
commit 2a730b8a44
55 changed files with 207 additions and 71609 deletions

View file

@ -7,6 +7,7 @@ import { renderObject } from "./renderObject";
import { memo, useEffect, useState } from "react";
import { RuntimeProvider } from "./RuntimeProvider";
import {
createProgressTracker,
createScriptCache,
FileSystemHandler,
runServer,
@ -50,6 +51,7 @@ function useParsedMission(name: string) {
interface ExecutedMissionState {
missionGroup: TorqueObject | undefined;
runtime: TorqueRuntime | undefined;
progress: number;
}
function useExecutedMission(
@ -59,6 +61,7 @@ function useExecutedMission(
const [state, setState] = useState<ExecutedMissionState>({
missionGroup: undefined,
runtime: undefined,
progress: 0,
});
useEffect(() => {
@ -70,6 +73,13 @@ function useExecutedMission(
// FIXME: Always just runs as the first game type for now...
const missionType = parsedMission.missionTypes[0];
// 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,
@ -78,10 +88,12 @@ function useExecutedMission(
fileSystem,
cache: scriptCache,
signal: controller.signal,
progress: progressTracker,
ignoreScripts: [
"scripts/admin.cs",
"scripts/ai.cs",
"scripts/aiCTF.cs",
"scripts/aiTDM.cs",
"scripts/aiHunters.cs",
"scripts/deathMessages.cs",
"scripts/graphBuild.cs",
@ -92,11 +104,12 @@ function useExecutedMission(
},
onMissionLoadDone: () => {
const missionGroup = runtime.getObjectByName("MissionGroup");
setState({ missionGroup, runtime });
setState({ missionGroup, runtime, progress: 1 });
},
});
return () => {
progressTracker.off("update", handleProgress);
controller.abort();
runtime.destroy();
};
@ -107,7 +120,7 @@ function useExecutedMission(
interface MissionProps {
name: string;
onLoadingChange?: (isLoading: boolean) => void;
onLoadingChange?: (isLoading: boolean, progress?: number) => void;
}
export const Mission = memo(function Mission({
@ -115,12 +128,15 @@ export const Mission = memo(function Mission({
onLoadingChange,
}: MissionProps) {
const { data: parsedMission } = useParsedMission(name);
const { missionGroup, runtime } = useExecutedMission(name, parsedMission);
const { missionGroup, runtime, progress } = useExecutedMission(
name,
parsedMission,
);
const isLoading = !missionGroup || !runtime;
useEffect(() => {
onLoadingChange?.(isLoading);
}, [isLoading, onLoadingChange]);
onLoadingChange?.(isLoading, progress);
}, [isLoading, progress, onLoadingChange]);
if (isLoading) {
return null;

View file

@ -7,6 +7,7 @@ import { TorqueObject, TorqueRuntime, TorqueRuntimeOptions } from "./types";
export { generate, type GeneratorOptions } from "./codegen";
export type { Program } from "./ast";
export { createBuiltins } from "./builtins";
export { createProgressTracker, type ProgressTracker } from "./progress";
export { createRuntime, createScriptCache } from "./runtime";
export { normalizePath } from "./utils";
export type {

View file

@ -0,0 +1,75 @@
export type ProgressEventType = "update";
export type ProgressListener = () => void;
export interface ProgressTracker {
/** Total items discovered so far (increases as dependencies are found) */
readonly total: number;
/** Items completed */
readonly loaded: number;
/** Currently loading item path, if any */
readonly current: string | null;
/** Progress as a ratio from 0 to 1 (or 0 if total is 0) */
readonly progress: number;
/** Subscribe to progress updates */
on(event: ProgressEventType, listener: ProgressListener): void;
/** Unsubscribe from progress updates */
off(event: ProgressEventType, listener: ProgressListener): void;
}
export interface ProgressTrackerInternal extends ProgressTracker {
/** Increment total count when a new item is discovered */
addItem(path: string): void;
/** Mark an item as completed */
completeItem(): void;
/** Set the currently loading item */
setCurrent(path: string | null): void;
}
export function createProgressTracker(): ProgressTrackerInternal {
const listeners = new Set<ProgressListener>();
let total = 0;
let loaded = 0;
let current: string | null = null;
function notify(): void {
for (const listener of listeners) {
listener();
}
}
return {
get total() {
return total;
},
get loaded() {
return loaded;
},
get current() {
return current;
},
get progress() {
return total === 0 ? 0 : loaded / total;
},
on(_event: ProgressEventType, listener: ProgressListener) {
listeners.add(listener);
},
off(_event: ProgressEventType, listener: ProgressListener) {
listeners.delete(listener);
},
addItem(path: string) {
total++;
current = path;
notify();
},
completeItem() {
loaded++;
current = null;
notify();
},
setCurrent(path: string | null) {
current = path;
notify();
},
};
}

View file

@ -1121,12 +1121,16 @@ export function createRuntime(
return;
}
// Track this script in progress
options.progress?.addItem(ref);
const loadPromise = (async () => {
// Pass original path to loader - it handles its own normalization
const source = await loader(ref);
if (source == null) {
console.warn(`Script not found: ${ref}`);
state.failedScripts.add(normalized);
options.progress?.completeItem();
return;
}
@ -1136,6 +1140,7 @@ export function createRuntime(
} catch (err) {
console.warn(`Failed to parse script: ${ref}`, err);
state.failedScripts.add(normalized);
options.progress?.completeItem();
return;
}
@ -1152,6 +1157,7 @@ export function createRuntime(
// Store the parsed AST
state.scripts.set(normalized, depAst);
options.progress?.completeItem();
})();
loadingPromises.set(normalized, loadPromise);
@ -1173,13 +1179,19 @@ export function createRuntime(
return createLoadedScript(state.scripts.get(normalized)!, path);
}
// Track this script in progress
options.progress?.addItem(path);
// Pass original path to loader - it handles its own normalization
const source = await loader(path);
if (source == null) {
options.progress?.completeItem();
throw new Error(`Script not found: ${path}`);
}
return loadFromSource(source, { path });
const result = await loadFromSource(source, { path });
options.progress?.completeItem();
return result;
}
async function loadFromSource(

View file

@ -1,4 +1,5 @@
import type { Program } from "./ast";
import type { ProgressTrackerInternal } from "./progress";
import type { CaseInsensitiveMap } from "./utils";
export type TorqueFunction = (...args: any[]) => any;
@ -108,6 +109,12 @@ export interface TorqueRuntimeOptions {
* Create with `createScriptCache()`.
*/
cache?: ScriptCache;
/**
* Progress tracker for monitoring script loading. If provided, the runtime
* will report loading progress as scripts are discovered and loaded.
* Create with `createProgressTracker()`.
*/
progress?: ProgressTrackerInternal;
}
export interface LoadScriptOptions {