From 5365ce618863b3b6434ffb235c78f52d9725fdfc Mon Sep 17 00:00:00 2001 From: Corwin Date: Wed, 10 Apr 2024 20:50:55 +0100 Subject: [PATCH 1/4] remove hard reset --- website/agb/src/app/mgba/mgbaWrapper.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/agb/src/app/mgba/mgbaWrapper.tsx b/website/agb/src/app/mgba/mgbaWrapper.tsx index 15022f9c..ecbba3cc 100644 --- a/website/agb/src/app/mgba/mgbaWrapper.tsx +++ b/website/agb/src/app/mgba/mgbaWrapper.tsx @@ -103,7 +103,6 @@ export const MgbaWrapper = forwardRef( restart: () => mgbaRef.current?.restart(), buttonPress: (key: GbaKey) => mgbaRef.current?.buttonPress(key), buttonRelease: (key: GbaKey) => mgbaRef.current?.buttonRelease(key), - hardReset: () => setMgbaId((id) => id + 1), })); useAvoidItchIoScrolling(); @@ -123,7 +122,6 @@ export const MgbaWrapper = forwardRef( )} {isPlaying ? ( Date: Wed, 10 Apr 2024 23:24:44 +0100 Subject: [PATCH 2/4] get saving working --- website/agb/src/app/mgba/mgba.tsx | 89 ++++++++++++++----- .../agb/src/app/mgba/useController.hook.ts | 8 +- website/agb/src/app/mgba/useFrameSkip.hook.ts | 8 +- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/website/agb/src/app/mgba/mgba.tsx b/website/agb/src/app/mgba/mgba.tsx index 974f5668..708323b0 100644 --- a/website/agb/src/app/mgba/mgba.tsx +++ b/website/agb/src/app/mgba/mgba.tsx @@ -10,12 +10,11 @@ import { GbaKey, KeyBindings } from "./bindings"; import { styled } from "styled-components"; import { useFrameSkip } from "./useFrameSkip.hook"; import { useController } from "./useController.hook"; - -type Module = any; +import { useLocalStorage } from "./useLocalStorage.hook"; interface MgbaProps { gameUrl: string; - volume?: Number; + volume?: number; controls: KeyBindings; paused: boolean; } @@ -55,14 +54,58 @@ async function downloadGame(gameUrl: string): Promise { } } +interface SaveGame { + [gameName: string]: number[]; +} + export const Mgba = forwardRef( ({ gameUrl, volume, controls, paused }, ref) => { const canvas = useRef(null); - const mgbaModule = useRef({} as mGBAEmulator); + const mgbaModule = useRef(); + + const [saveGame, setSaveGame] = useLocalStorage( + {}, + "agbrswebplayer/savegames" + ); const [state, setState] = useState(MgbaState.Uninitialised); const [gameLoaded, setGameLoaded] = useState(false); + useEffect(() => { + function beforeUnload() { + const gameSplit = gameUrl.split("/"); + const gameBaseName = gameSplit[gameSplit.length - 1]; + + const save = mgbaModule.current?.getSave(); + if (!save) return; + + setSaveGame({ + ...saveGame, + [gameBaseName]: [...save], + }); + } + + window.addEventListener("beforeunload", beforeUnload); + + return () => { + window.removeEventListener("beforeunload", beforeUnload); + }; + }, [gameUrl, saveGame, setSaveGame]); + + useEffect(() => { + if (state !== MgbaState.Initialised) return; + + const gameSplit = gameUrl.split("/"); + const gameBaseName = gameSplit[gameSplit.length - 1]; + + const save = saveGame[gameBaseName]; + if (!save) return; + + const savePath = `${MGBA_ROM_DIRECTORY}/${gameBaseName}.sav`; + + mgbaModule.current?.FS.writeFile(savePath, new Uint8Array([0, 1, 2, 3])); + }, [gameUrl, saveGame, state]); + useEffect(() => { if (state !== MgbaState.Initialised) return; (async () => { @@ -71,9 +114,9 @@ export const Mgba = forwardRef( const gameBaseName = gameSplit[gameSplit.length - 1]; const gamePath = `${MGBA_ROM_DIRECTORY}/${gameBaseName}`; - mgbaModule.current.FS.writeFile(gamePath, new Uint8Array(gameData)); - mgbaModule.current.loadGame(gamePath); - mgbaModule.current.setVolume(0.1); // for some reason you have to do this or you get no sound + mgbaModule.current?.FS.writeFile(gamePath, new Uint8Array(gameData)); + mgbaModule.current?.loadGame(gamePath); + mgbaModule.current?.setVolume(0.1); // for some reason you have to do this or you get no sound setGameLoaded(true); })(); }, [state, gameUrl]); @@ -85,22 +128,19 @@ export const Mgba = forwardRef( if (state !== MgbaState.Uninitialised) return; setState(MgbaState.Initialising); - mgbaModule.current = { - canvas: canvas.current, - }; - mGBA(mgbaModule.current).then((module: Module) => { - mgbaModule.current = module; - module.FSInit(); - setState(MgbaState.Initialised); - }); + const mModule = await mGBA({ canvas: canvas.current }); + mgbaModule.current = mModule; + await mModule.FSInit(); + await mModule.FSSync(); + setState(MgbaState.Initialised); })(); if (state === MgbaState.Initialised) return () => { try { - mgbaModule.current.quitGame(); - mgbaModule.current.quitMgba(); + mgbaModule.current?.quitGame(); + mgbaModule.current?.quitMgba(); } catch {} }; }, [state]); @@ -119,30 +159,31 @@ export const Mgba = forwardRef( ? "Return" : value.toLowerCase().replace("arrow", "").replace("key", ""); - mgbaModule.current.bindKey(binding, key); + mgbaModule.current?.bindKey(binding, key); } }, [controls, gameLoaded]); useEffect(() => { if (!gameLoaded) return; - mgbaModule.current.setVolume(volume ?? 1.0); + mgbaModule.current?.setVolume(volume ?? 1.0); }, [gameLoaded, volume]); useEffect(() => { if (!gameLoaded) return; if (paused) { - mgbaModule.current.pauseGame(); + mgbaModule.current?.pauseGame(); } else { - mgbaModule.current.resumeGame(); + mgbaModule.current?.resumeGame(); } }, [gameLoaded, paused]); useImperativeHandle(ref, () => { return { - restart: () => mgbaModule.current.quickReload(), - buttonPress: (key: GbaKey) => mgbaModule.current.buttonPress(key), - buttonRelease: (key: GbaKey) => mgbaModule.current.buttonUnpress(key), + restart: () => mgbaModule.current?.quickReload(), + buttonPress: (key: GbaKey) => mgbaModule.current?.buttonPress(key), + buttonRelease: (key: GbaKey) => mgbaModule.current?.buttonUnpress(key), + saveGame: () => {}, }; }); diff --git a/website/agb/src/app/mgba/useController.hook.ts b/website/agb/src/app/mgba/useController.hook.ts index 4555d526..52bbbc27 100644 --- a/website/agb/src/app/mgba/useController.hook.ts +++ b/website/agb/src/app/mgba/useController.hook.ts @@ -2,7 +2,9 @@ import { MutableRefObject, useEffect } from "react"; import { mGBAEmulator } from "./vendor/mgba"; import { GbaKey } from "./bindings"; -export function useController(mgbaModule: MutableRefObject) { +export function useController( + mgbaModule: MutableRefObject +) { useEffect(() => { let stopped = false; @@ -64,13 +66,13 @@ export function useController(mgbaModule: MutableRefObject) { for (let oldButton of previouslyPressedButtons) { if (!currentlyPressed.has(oldButton)) { - mgbaModule.current.buttonUnpress(oldButton); + mgbaModule.current?.buttonUnpress(oldButton); } } for (let newButton of currentlyPressed) { if (!previouslyPressedButtons.has(newButton)) { - mgbaModule.current.buttonPress(newButton); + mgbaModule.current?.buttonPress(newButton); } } diff --git a/website/agb/src/app/mgba/useFrameSkip.hook.ts b/website/agb/src/app/mgba/useFrameSkip.hook.ts index 8f75309f..c6999ff3 100644 --- a/website/agb/src/app/mgba/useFrameSkip.hook.ts +++ b/website/agb/src/app/mgba/useFrameSkip.hook.ts @@ -1,7 +1,9 @@ import { MutableRefObject, useEffect } from "react"; import { mGBAEmulator } from "./vendor/mgba"; -export function useFrameSkip(mgbaModule: MutableRefObject) { +export function useFrameSkip( + mgbaModule: MutableRefObject +) { useEffect(() => { let previous: number | undefined = undefined; let stopped = false; @@ -23,12 +25,12 @@ export function useFrameSkip(mgbaModule: MutableRefObject) { if (totalTime >= 1 / 60) { totalTime -= 1 / 60; if (paused) { - mgbaModule.current.resumeGame(); + mgbaModule.current?.resumeGame(); paused = false; } } else { if (!paused) { - mgbaModule.current.pauseGame(); + mgbaModule.current?.pauseGame(); paused = true; } } From dc44d20627efca26971a4b0f65d32fa632bd9594 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 20 Apr 2024 16:18:42 +0100 Subject: [PATCH 3/4] save to separate areas --- examples/hyperspace-roll/src/save.rs | 7 ++++--- examples/the-dungeon-puzzlers-lament/src/lib.rs | 6 ++++-- examples/the-dungeon-puzzlers-lament/src/save.rs | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/hyperspace-roll/src/save.rs b/examples/hyperspace-roll/src/save.rs index ada5cf6a..a810a6c7 100644 --- a/examples/hyperspace-roll/src/save.rs +++ b/examples/hyperspace-roll/src/save.rs @@ -3,6 +3,7 @@ use agb::save::{Error, SaveManager}; use agb::Gba; static HIGH_SCORE: AtomicU32 = AtomicU32::new(0); +static SAVE_OFFSET: usize = 1; pub fn init_save(gba: &mut Gba) -> Result<(), Error> { gba.save.init_sram(); @@ -18,7 +19,7 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> { save_high_score(&mut gba.save, 0)?; } else { let mut buffer = [0; 4]; - access.read(1, &mut buffer)?; + access.read(SAVE_OFFSET, &mut buffer)?; let high_score = u32::from_le_bytes(buffer); let score = if high_score > 100 { 0 } else { high_score }; @@ -35,8 +36,8 @@ pub fn load_high_score() -> u32 { pub fn save_high_score(save: &mut SaveManager, score: u32) -> Result<(), Error> { save.access()? - .prepare_write(1..5)? - .write(1, &score.to_le_bytes())?; + .prepare_write(SAVE_OFFSET..SAVE_OFFSET + 4)? + .write(SAVE_OFFSET, &score.to_le_bytes())?; HIGH_SCORE.store(score, Ordering::SeqCst); Ok(()) } diff --git a/examples/the-dungeon-puzzlers-lament/src/lib.rs b/examples/the-dungeon-puzzlers-lament/src/lib.rs index d4ccc8be..1d1770a4 100644 --- a/examples/the-dungeon-puzzlers-lament/src/lib.rs +++ b/examples/the-dungeon-puzzlers-lament/src/lib.rs @@ -121,8 +121,10 @@ pub fn entry(mut gba: agb::Gba) -> ! { oam: unmanaged, }; - let mut current_level = 0; - let mut maximum_level = save::load_max_level() as usize; + let saved_level = save::load_max_level() as usize; + + let mut current_level = saved_level; + let mut maximum_level = saved_level; loop { if current_level >= level::Level::num_levels() { current_level = 0; diff --git a/examples/the-dungeon-puzzlers-lament/src/save.rs b/examples/the-dungeon-puzzlers-lament/src/save.rs index 5923fa5c..398d9f49 100644 --- a/examples/the-dungeon-puzzlers-lament/src/save.rs +++ b/examples/the-dungeon-puzzlers-lament/src/save.rs @@ -5,6 +5,7 @@ use agb::{ }; static MAXIMUM_LEVEL: AtomicU32 = AtomicU32::new(0); +static SAVE_OFFSET: usize = 0xFF; pub fn init_save(gba: &mut Gba) -> Result<(), Error> { gba.save.init_sram(); @@ -20,7 +21,7 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> { save_max_level(&mut gba.save, 0)?; } else { let mut buffer = [0; 4]; - access.read(1, &mut buffer)?; + access.read(SAVE_OFFSET, &mut buffer)?; let max_level = u32::from_le_bytes(buffer); if max_level > 100 { @@ -39,8 +40,8 @@ pub fn load_max_level() -> u32 { pub fn save_max_level(save: &mut SaveManager, level: u32) -> Result<(), Error> { save.access()? - .prepare_write(1..5)? - .write(1, &level.to_le_bytes())?; + .prepare_write(SAVE_OFFSET..SAVE_OFFSET + 4)? + .write(SAVE_OFFSET, &level.to_le_bytes())?; MAXIMUM_LEVEL.store(level, Ordering::SeqCst); Ok(()) } From 4022e8413eb1e9e0698d60f0585bd20a305931b2 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 20 Apr 2024 16:19:07 +0100 Subject: [PATCH 4/4] load gba rom as a url to let next handle cache invalidation --- justfile | 3 +-- website/agb/src/app/mgba/mgba.tsx | 21 ++++++++++++--------- website/agb/src/app/mgba/mgbaWrapper.tsx | 2 +- website/agb/src/app/page.tsx | 4 +++- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/justfile b/justfile index 848cf761..f5f7afc9 100644 --- a/justfile +++ b/justfile @@ -91,10 +91,9 @@ build-mgba-wasm: build-combo-rom-site: just _build-rom "examples/combo" "AGBGAMES" + gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz build-site-app: build-mgba-wasm build-combo-rom-site - mkdir -p website/agb/public - gzip -9 -c examples/target/examples/combo.gba > website/agb/public/combo.gba.gz (cd website/agb && npm install --no-save --prefer-offline --no-audit) (cd website/agb && npm run build) diff --git a/website/agb/src/app/mgba/mgba.tsx b/website/agb/src/app/mgba/mgba.tsx index 708323b0..002acb91 100644 --- a/website/agb/src/app/mgba/mgba.tsx +++ b/website/agb/src/app/mgba/mgba.tsx @@ -13,7 +13,7 @@ import { useController } from "./useController.hook"; import { useLocalStorage } from "./useLocalStorage.hook"; interface MgbaProps { - gameUrl: string; + gameUrl: URL; volume?: number; controls: KeyBindings; paused: boolean; @@ -41,10 +41,12 @@ export interface MgbaHandle { buttonRelease: (key: GbaKey) => void; } -async function downloadGame(gameUrl: string): Promise { +async function downloadGame(gameUrl: URL): Promise { const game = await fetch(gameUrl); - if (gameUrl.endsWith(".gz")) { + const gameUrlString = gameUrl.toString(); + + if (gameUrlString.endsWith(".gz")) { const decompressedStream = (await game.blob()) .stream() .pipeThrough(new DecompressionStream("gzip")); @@ -67,13 +69,14 @@ export const Mgba = forwardRef( {}, "agbrswebplayer/savegames" ); + const gameUrlString = gameUrl.toString(); const [state, setState] = useState(MgbaState.Uninitialised); const [gameLoaded, setGameLoaded] = useState(false); useEffect(() => { function beforeUnload() { - const gameSplit = gameUrl.split("/"); + const gameSplit = gameUrlString.split("/"); const gameBaseName = gameSplit[gameSplit.length - 1]; const save = mgbaModule.current?.getSave(); @@ -90,12 +93,12 @@ export const Mgba = forwardRef( return () => { window.removeEventListener("beforeunload", beforeUnload); }; - }, [gameUrl, saveGame, setSaveGame]); + }, [gameUrlString, saveGame, setSaveGame]); useEffect(() => { if (state !== MgbaState.Initialised) return; - const gameSplit = gameUrl.split("/"); + const gameSplit = gameUrlString.split("/"); const gameBaseName = gameSplit[gameSplit.length - 1]; const save = saveGame[gameBaseName]; @@ -104,13 +107,13 @@ export const Mgba = forwardRef( const savePath = `${MGBA_ROM_DIRECTORY}/${gameBaseName}.sav`; mgbaModule.current?.FS.writeFile(savePath, new Uint8Array([0, 1, 2, 3])); - }, [gameUrl, saveGame, state]); + }, [gameUrlString, saveGame, state]); useEffect(() => { if (state !== MgbaState.Initialised) return; (async () => { const gameData = await downloadGame(gameUrl); - const gameSplit = gameUrl.split("/"); + const gameSplit = gameUrlString.split("/"); const gameBaseName = gameSplit[gameSplit.length - 1]; const gamePath = `${MGBA_ROM_DIRECTORY}/${gameBaseName}`; @@ -119,7 +122,7 @@ export const Mgba = forwardRef( mgbaModule.current?.setVolume(0.1); // for some reason you have to do this or you get no sound setGameLoaded(true); })(); - }, [state, gameUrl]); + }, [state, gameUrl, gameUrlString]); // init mgba useEffect(() => { diff --git a/website/agb/src/app/mgba/mgbaWrapper.tsx b/website/agb/src/app/mgba/mgbaWrapper.tsx index ecbba3cc..504da4d9 100644 --- a/website/agb/src/app/mgba/mgbaWrapper.tsx +++ b/website/agb/src/app/mgba/mgbaWrapper.tsx @@ -60,7 +60,7 @@ const StartButtonWrapper = styled.button` `; interface MgbaWrapperProps { - gameUrl: string; + gameUrl: URL; isPlaying?: boolean; setIsPlaying?: (isPlaying: boolean) => void; } diff --git a/website/agb/src/app/page.tsx b/website/agb/src/app/page.tsx index 41c58c8b..0f8e833a 100644 --- a/website/agb/src/app/page.tsx +++ b/website/agb/src/app/page.tsx @@ -78,6 +78,8 @@ function shouldStartPlaying(isTouchScreen: boolean | undefined) { return !isTouchScreen; } +const COMBO_GAME = new URL("combo.gba.gz", import.meta.url); + function MgbaWithControllerSides() { const mgba = useRef(null); @@ -105,7 +107,7 @@ function MgbaWithControllerSides() {