Emujs in isolated mode

This commit is contained in:
Dustin Brett
2025-01-21 20:59:56 -08:00
parent dd5c06560e
commit c1cdbbc2d5
4 changed files with 139 additions and 115 deletions

View File

@@ -4,9 +4,11 @@ import AppContainer from "components/system/Apps/AppContainer";
import { type ComponentProcessProps } from "components/system/Apps/RenderComponent";
const Emulator: FC<ComponentProcessProps> = ({ id }) => (
<AppContainer StyledComponent={StyledEmulator} id={id} useHook={useEmulator}>
<div id="emulator" />
</AppContainer>
<AppContainer
StyledComponent={StyledEmulator}
id={id}
useHook={useEmulator}
/>
);
export default Emulator;

View File

@@ -7,6 +7,19 @@ export type Emulator = {
loadState?: (state: Buffer) => void;
};
export type OnSaveState = (event: {
screenshot: Uint8Array;
state: Uint8Array;
}) => void;
export type OnGameStart = (
event: Event & {
detail: {
emulator: Emulator;
};
}
) => void;
declare global {
interface Window {
Browser?: {
@@ -34,17 +47,8 @@ declare global {
};
EJS_gameName?: string;
EJS_gameUrl?: string;
EJS_onGameStart?: (
event: Event & {
detail: {
emulator: Emulator;
};
}
) => void;
EJS_onSaveState?: (event: {
screenshot: Uint8Array;
state: Uint8Array;
}) => void;
EJS_onGameStart?: OnGameStart;
EJS_onSaveState?: OnSaveState;
EJS_pathtodata?: string;
EJS_player?: string;
EJS_startOnLoaded?: boolean;

View File

@@ -1,7 +1,11 @@
import { basename, extname, join } from "path";
import { useCallback, useEffect, useRef } from "react";
import { type Core, emulatorCores } from "components/apps/Emulator/config";
import { type Emulator } from "components/apps/Emulator/types";
import {
type OnGameStart,
type OnSaveState,
type Emulator,
} from "components/apps/Emulator/types";
import { type ContainerHookProps } from "components/system/Apps/AppContainer";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
import useTitle from "components/system/Window/useTitle";
@@ -12,12 +16,26 @@ import { SAVE_PATH } from "utils/constants";
import { bufferToUrl, getExtension, loadFiles } from "utils/functions";
import { zipAsync } from "utils/zipFunctions";
import { useSnapshots } from "hooks/useSnapshots";
import useIsolatedContentWindow from "hooks/useIsolatedContentWindow";
const getCore = (extension: string): [string, Core] =>
(Object.entries(emulatorCores).find(([, { ext }]) =>
ext.includes(extension)
) || []) as [string, Core];
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const withWindowConstructor = <F extends Function>(
fn: F,
context: Window
): F => {
if ("Function" in context) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type, no-param-reassign
fn.constructor = context.Function as Function;
}
return fn;
};
const useEmulator = ({
containerRef,
id,
@@ -28,132 +46,132 @@ const useEmulator = ({
const { exists, readFile } = useFileSystem();
const { createSnapshot } = useSnapshots();
const mountEmFs = useEmscriptenMount();
const {
linkElement,
processes: { [id]: { closing = false, libs = [] } = {} } = {},
} = useProcesses();
const { processes: { [id]: { closing, libs = [] } = {} } = {} } =
useProcesses();
const { prependFileToTitle } = useTitle(id);
const emulatorRef = useRef<Emulator>(undefined);
const loadedUrlRef = useRef<string>("");
const loadRom = useCallback(async () => {
if (!url) return;
const getContentWindow = useIsolatedContentWindow(id, containerRef);
const loadedUrl = useRef<string>(undefined);
const loadRom = useCallback(
async (fileUrl: string) => {
const contentWindow = getContentWindow?.();
containerRef.current?.classList.remove("drop");
if (!contentWindow) return;
if (loadedUrlRef.current) {
if (loadedUrlRef.current !== url) {
loadedUrlRef.current = "";
loadedUrl.current = fileUrl;
try {
window.EJS_terminate?.();
} catch {
// Ignore errors during termination
}
setLoading(true);
if (containerRef.current) {
const div = document.createElement("div");
containerRef.current?.classList.remove("drop");
div.id = "emulator";
[...containerRef.current.children].forEach((child) => child.remove());
containerRef.current.append(div);
loadRom();
}
try {
contentWindow.EJS_terminate?.();
} catch {
// Ignore errors during termination
}
return;
}
[...contentWindow.document.body.children].forEach((child) =>
child.remove()
);
const div = contentWindow.document.createElement("div");
div.id = "emulator";
div.style.placeContent = "center";
contentWindow.document.body.append(div);
loadedUrlRef.current = url;
window.EJS_gameName = basename(url, extname(url));
contentWindow.EJS_gameName = basename(fileUrl, extname(fileUrl));
const [consoleName, { core = "", zip = false } = {}] = getCore(
getExtension(url)
);
const rom = await readFile(url);
const [consoleName, { core = "", zip = false } = {}] = getCore(
getExtension(fileUrl)
);
const rom = await readFile(fileUrl);
window.EJS_gameUrl = bufferToUrl(
zip ? Buffer.from(await zipAsync({ [basename(url)]: rom })) : rom
);
window.EJS_core = core;
contentWindow.EJS_gameUrl = bufferToUrl(
zip ? Buffer.from(await zipAsync({ [basename(fileUrl)]: rom })) : rom
);
contentWindow.EJS_core = core;
const saveName = `${basename(url)}.sav`;
const savePath = join(SAVE_PATH, saveName);
const saveName = `${basename(fileUrl)}.sav`;
const savePath = join(SAVE_PATH, saveName);
window.EJS_onGameStart = ({ detail: { emulator: currentEmulator } }) => {
const loadState = async (): Promise<void> => {
if (await exists(savePath)) {
currentEmulator.loadState?.(await readFile(savePath));
}
contentWindow.EJS_onGameStart = withWindowConstructor<OnGameStart>(
({ detail: { emulator: currentEmulator } }) => {
const loadState = async (): Promise<void> => {
if (await exists(savePath)) {
currentEmulator.loadState?.(await readFile(savePath));
}
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "EmulatorJs");
emulatorRef.current = currentEmulator;
setLoading(false);
mountEmFs(contentWindow.FS as EmscriptenFS, "EmulatorJs");
emulatorRef.current = currentEmulator;
};
loadState();
},
contentWindow
);
contentWindow.EJS_onSaveState = withWindowConstructor<OnSaveState>(
({ screenshot, state }) => {
contentWindow.EJS_terminate?.();
if (state) {
createSnapshot(
saveName,
Buffer.from(state),
Buffer.from(screenshot)
);
}
},
contentWindow
);
contentWindow.EJS_player = "#emulator";
contentWindow.EJS_biosUrl = "";
contentWindow.EJS_pathtodata = "Program Files/EmulatorJs/";
contentWindow.EJS_startOnLoaded = true;
contentWindow.EJS_RESET_VARS = true;
contentWindow.EJS_Buttons = {
cacheManage: false,
loadState: false,
quickLoad: false,
quickSave: false,
saveState: false,
screenRecord: false,
screenshot: false,
};
loadState();
};
window.EJS_onSaveState = ({ screenshot, state }) => {
window.EJS_terminate?.();
await loadFiles(libs, undefined, undefined, undefined, contentWindow);
if (state) {
createSnapshot(saveName, Buffer.from(state), Buffer.from(screenshot));
}
};
window.EJS_player = "#emulator";
window.EJS_biosUrl = "";
window.EJS_pathtodata = "Program Files/EmulatorJs/";
window.EJS_startOnLoaded = true;
window.EJS_RESET_VARS = true;
window.EJS_Buttons = {
cacheManage: false,
loadState: false,
quickLoad: false,
quickSave: false,
saveState: false,
screenRecord: false,
screenshot: false,
};
await loadFiles(libs, undefined, true);
prependFileToTitle(`${window.EJS_gameName} (${consoleName})`);
}, [
containerRef,
createSnapshot,
exists,
libs,
mountEmFs,
prependFileToTitle,
readFile,
setLoading,
url,
]);
prependFileToTitle(`${contentWindow.EJS_gameName} (${consoleName})`);
},
[
containerRef,
createSnapshot,
exists,
getContentWindow,
libs,
mountEmFs,
prependFileToTitle,
readFile,
setLoading,
]
);
useEffect(() => {
if (url) loadRom();
else {
if (url) {
if (url !== loadedUrl.current) loadRom(url);
} else if (!closing) {
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "EmulatorJs");
containerRef.current?.classList.add("drop");
}
}, [containerRef, loadRom, mountEmFs, setLoading, url]);
}, [closing, containerRef, loadRom, setLoading, url]);
useEffect(() => {
if (!loading) {
const canvas = containerRef.current?.querySelector("canvas");
if (canvas instanceof HTMLCanvasElement) {
linkElement(id, "peekElement", canvas);
}
}
return () => {
useEffect(
() => () => {
if (!loading && closing) {
emulatorRef.current?.elements.buttons.saveState?.click();
}
};
}, [closing, containerRef, id, linkElement, loading]);
},
[closing, loading]
);
};
export default useEmulator;

View File

@@ -75,10 +75,10 @@ const directory: Processes = {
"/Program Files/EmulatorJs/emu-css.min.css",
"/Program Files/EmulatorJs/emulator.min.js",
],
hidePeek: true,
icon: "/System/Icons/emulator.webp",
libs: ["/Program Files/EmulatorJs/loader.js"],
lockAspectRatio: true,
singleton: true,
title: "Emulator",
},
FileExplorer: {