From c1cdbbc2d5e54dd7c39c882a809f36ca65577a3b Mon Sep 17 00:00:00 2001 From: Dustin Brett Date: Tue, 21 Jan 2025 20:59:56 -0800 Subject: [PATCH] Emujs in isolated mode --- components/apps/Emulator/index.tsx | 8 +- components/apps/Emulator/types.ts | 26 +-- components/apps/Emulator/useEmulator.ts | 218 +++++++++++++----------- contexts/process/directory.ts | 2 +- 4 files changed, 139 insertions(+), 115 deletions(-) diff --git a/components/apps/Emulator/index.tsx b/components/apps/Emulator/index.tsx index 5ab06bfa..51dc208a 100644 --- a/components/apps/Emulator/index.tsx +++ b/components/apps/Emulator/index.tsx @@ -4,9 +4,11 @@ import AppContainer from "components/system/Apps/AppContainer"; import { type ComponentProcessProps } from "components/system/Apps/RenderComponent"; const Emulator: FC = ({ id }) => ( - -
- + ); export default Emulator; diff --git a/components/apps/Emulator/types.ts b/components/apps/Emulator/types.ts index 6fe466df..5c44926e 100644 --- a/components/apps/Emulator/types.ts +++ b/components/apps/Emulator/types.ts @@ -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; diff --git a/components/apps/Emulator/useEmulator.ts b/components/apps/Emulator/useEmulator.ts index b9caf4eb..76535487 100644 --- a/components/apps/Emulator/useEmulator.ts +++ b/components/apps/Emulator/useEmulator.ts @@ -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 = ( + 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(undefined); - const loadedUrlRef = useRef(""); - const loadRom = useCallback(async () => { - if (!url) return; + const getContentWindow = useIsolatedContentWindow(id, containerRef); + const loadedUrl = useRef(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 => { - if (await exists(savePath)) { - currentEmulator.loadState?.(await readFile(savePath)); - } + contentWindow.EJS_onGameStart = withWindowConstructor( + ({ detail: { emulator: currentEmulator } }) => { + const loadState = async (): Promise => { + 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( + ({ 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; diff --git a/contexts/process/directory.ts b/contexts/process/directory.ts index 278f1010..af8956b1 100644 --- a/contexts/process/directory.ts +++ b/contexts/process/directory.ts @@ -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: {