Get saving working (#631)

- [ ] Changelog updated / no changelog update needed
This commit is contained in:
Corwin 2024-04-20 16:43:35 +01:00 committed by GitHub
commit b317602c6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 100 additions and 49 deletions

View file

@ -3,6 +3,7 @@ use agb::save::{Error, SaveManager};
use agb::Gba; use agb::Gba;
static HIGH_SCORE: AtomicU32 = AtomicU32::new(0); static HIGH_SCORE: AtomicU32 = AtomicU32::new(0);
static SAVE_OFFSET: usize = 1;
pub fn init_save(gba: &mut Gba) -> Result<(), Error> { pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
gba.save.init_sram(); gba.save.init_sram();
@ -18,7 +19,7 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
save_high_score(&mut gba.save, 0)?; save_high_score(&mut gba.save, 0)?;
} else { } else {
let mut buffer = [0; 4]; 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 high_score = u32::from_le_bytes(buffer);
let score = if high_score > 100 { 0 } else { high_score }; 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> { pub fn save_high_score(save: &mut SaveManager, score: u32) -> Result<(), Error> {
save.access()? save.access()?
.prepare_write(1..5)? .prepare_write(SAVE_OFFSET..SAVE_OFFSET + 4)?
.write(1, &score.to_le_bytes())?; .write(SAVE_OFFSET, &score.to_le_bytes())?;
HIGH_SCORE.store(score, Ordering::SeqCst); HIGH_SCORE.store(score, Ordering::SeqCst);
Ok(()) Ok(())
} }

View file

@ -121,8 +121,10 @@ pub fn entry(mut gba: agb::Gba) -> ! {
oam: unmanaged, oam: unmanaged,
}; };
let mut current_level = 0; let saved_level = save::load_max_level() as usize;
let mut maximum_level = save::load_max_level() as usize;
let mut current_level = saved_level;
let mut maximum_level = saved_level;
loop { loop {
if current_level >= level::Level::num_levels() { if current_level >= level::Level::num_levels() {
current_level = 0; current_level = 0;

View file

@ -5,6 +5,7 @@ use agb::{
}; };
static MAXIMUM_LEVEL: AtomicU32 = AtomicU32::new(0); static MAXIMUM_LEVEL: AtomicU32 = AtomicU32::new(0);
static SAVE_OFFSET: usize = 0xFF;
pub fn init_save(gba: &mut Gba) -> Result<(), Error> { pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
gba.save.init_sram(); gba.save.init_sram();
@ -20,7 +21,7 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
save_max_level(&mut gba.save, 0)?; save_max_level(&mut gba.save, 0)?;
} else { } else {
let mut buffer = [0; 4]; let mut buffer = [0; 4];
access.read(1, &mut buffer)?; access.read(SAVE_OFFSET, &mut buffer)?;
let max_level = u32::from_le_bytes(buffer); let max_level = u32::from_le_bytes(buffer);
if max_level > 100 { 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> { pub fn save_max_level(save: &mut SaveManager, level: u32) -> Result<(), Error> {
save.access()? save.access()?
.prepare_write(1..5)? .prepare_write(SAVE_OFFSET..SAVE_OFFSET + 4)?
.write(1, &level.to_le_bytes())?; .write(SAVE_OFFSET, &level.to_le_bytes())?;
MAXIMUM_LEVEL.store(level, Ordering::SeqCst); MAXIMUM_LEVEL.store(level, Ordering::SeqCst);
Ok(()) Ok(())
} }

View file

@ -94,10 +94,9 @@ build-mgba-wasm:
build-combo-rom-site: build-combo-rom-site:
just _build-rom "examples/combo" "AGBGAMES" 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 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 install --no-save --prefer-offline --no-audit)
(cd website/agb && npm run build) (cd website/agb && npm run build)

View file

@ -10,12 +10,11 @@ import { GbaKey, KeyBindings } from "./bindings";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { useFrameSkip } from "./useFrameSkip.hook"; import { useFrameSkip } from "./useFrameSkip.hook";
import { useController } from "./useController.hook"; import { useController } from "./useController.hook";
import { useLocalStorage } from "./useLocalStorage.hook";
type Module = any;
interface MgbaProps { interface MgbaProps {
gameUrl: string; gameUrl: URL;
volume?: Number; volume?: number;
controls: KeyBindings; controls: KeyBindings;
paused: boolean; paused: boolean;
} }
@ -42,10 +41,12 @@ export interface MgbaHandle {
buttonRelease: (key: GbaKey) => void; buttonRelease: (key: GbaKey) => void;
} }
async function downloadGame(gameUrl: string): Promise<ArrayBuffer> { async function downloadGame(gameUrl: URL): Promise<ArrayBuffer> {
const game = await fetch(gameUrl); const game = await fetch(gameUrl);
if (gameUrl.endsWith(".gz")) { const gameUrlString = gameUrl.toString();
if (gameUrlString.endsWith(".gz")) {
const decompressedStream = (await game.blob()) const decompressedStream = (await game.blob())
.stream() .stream()
.pipeThrough(new DecompressionStream("gzip")); .pipeThrough(new DecompressionStream("gzip"));
@ -55,28 +56,73 @@ async function downloadGame(gameUrl: string): Promise<ArrayBuffer> {
} }
} }
interface SaveGame {
[gameName: string]: number[];
}
export const Mgba = forwardRef<MgbaHandle, MgbaProps>( export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
({ gameUrl, volume, controls, paused }, ref) => { ({ gameUrl, volume, controls, paused }, ref) => {
const canvas = useRef(null); const canvas = useRef(null);
const mgbaModule = useRef<Module>({} as mGBAEmulator); const mgbaModule = useRef<mGBAEmulator>();
const [saveGame, setSaveGame] = useLocalStorage<SaveGame>(
{},
"agbrswebplayer/savegames"
);
const gameUrlString = gameUrl.toString();
const [state, setState] = useState(MgbaState.Uninitialised); const [state, setState] = useState(MgbaState.Uninitialised);
const [gameLoaded, setGameLoaded] = useState(false); const [gameLoaded, setGameLoaded] = useState(false);
useEffect(() => {
function beforeUnload() {
const gameSplit = gameUrlString.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);
};
}, [gameUrlString, saveGame, setSaveGame]);
useEffect(() => {
if (state !== MgbaState.Initialised) return;
const gameSplit = gameUrlString.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]));
}, [gameUrlString, saveGame, state]);
useEffect(() => { useEffect(() => {
if (state !== MgbaState.Initialised) return; if (state !== MgbaState.Initialised) return;
(async () => { (async () => {
const gameData = await downloadGame(gameUrl); const gameData = await downloadGame(gameUrl);
const gameSplit = gameUrl.split("/"); const gameSplit = gameUrlString.split("/");
const gameBaseName = gameSplit[gameSplit.length - 1]; const gameBaseName = gameSplit[gameSplit.length - 1];
const gamePath = `${MGBA_ROM_DIRECTORY}/${gameBaseName}`; const gamePath = `${MGBA_ROM_DIRECTORY}/${gameBaseName}`;
mgbaModule.current.FS.writeFile(gamePath, new Uint8Array(gameData)); mgbaModule.current?.FS.writeFile(gamePath, new Uint8Array(gameData));
mgbaModule.current.loadGame(gamePath); mgbaModule.current?.loadGame(gamePath);
mgbaModule.current.setVolume(0.1); // for some reason you have to do this or you get no sound mgbaModule.current?.setVolume(0.1); // for some reason you have to do this or you get no sound
setGameLoaded(true); setGameLoaded(true);
})(); })();
}, [state, gameUrl]); }, [state, gameUrl, gameUrlString]);
// init mgba // init mgba
useEffect(() => { useEffect(() => {
@ -85,22 +131,19 @@ export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
if (state !== MgbaState.Uninitialised) return; if (state !== MgbaState.Uninitialised) return;
setState(MgbaState.Initialising); setState(MgbaState.Initialising);
mgbaModule.current = {
canvas: canvas.current,
};
mGBA(mgbaModule.current).then((module: Module) => { const mModule = await mGBA({ canvas: canvas.current });
mgbaModule.current = module; mgbaModule.current = mModule;
module.FSInit(); await mModule.FSInit();
setState(MgbaState.Initialised); await mModule.FSSync();
}); setState(MgbaState.Initialised);
})(); })();
if (state === MgbaState.Initialised) if (state === MgbaState.Initialised)
return () => { return () => {
try { try {
mgbaModule.current.quitGame(); mgbaModule.current?.quitGame();
mgbaModule.current.quitMgba(); mgbaModule.current?.quitMgba();
} catch {} } catch {}
}; };
}, [state]); }, [state]);
@ -119,30 +162,31 @@ export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
? "Return" ? "Return"
: value.toLowerCase().replace("arrow", "").replace("key", ""); : value.toLowerCase().replace("arrow", "").replace("key", "");
mgbaModule.current.bindKey(binding, key); mgbaModule.current?.bindKey(binding, key);
} }
}, [controls, gameLoaded]); }, [controls, gameLoaded]);
useEffect(() => { useEffect(() => {
if (!gameLoaded) return; if (!gameLoaded) return;
mgbaModule.current.setVolume(volume ?? 1.0); mgbaModule.current?.setVolume(volume ?? 1.0);
}, [gameLoaded, volume]); }, [gameLoaded, volume]);
useEffect(() => { useEffect(() => {
if (!gameLoaded) return; if (!gameLoaded) return;
if (paused) { if (paused) {
mgbaModule.current.pauseGame(); mgbaModule.current?.pauseGame();
} else { } else {
mgbaModule.current.resumeGame(); mgbaModule.current?.resumeGame();
} }
}, [gameLoaded, paused]); }, [gameLoaded, paused]);
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return { return {
restart: () => mgbaModule.current.quickReload(), restart: () => mgbaModule.current?.quickReload(),
buttonPress: (key: GbaKey) => mgbaModule.current.buttonPress(key), buttonPress: (key: GbaKey) => mgbaModule.current?.buttonPress(key),
buttonRelease: (key: GbaKey) => mgbaModule.current.buttonUnpress(key), buttonRelease: (key: GbaKey) => mgbaModule.current?.buttonUnpress(key),
saveGame: () => {},
}; };
}); });

View file

@ -60,7 +60,7 @@ const StartButtonWrapper = styled.button`
`; `;
interface MgbaWrapperProps { interface MgbaWrapperProps {
gameUrl: string; gameUrl: URL;
isPlaying?: boolean; isPlaying?: boolean;
setIsPlaying?: (isPlaying: boolean) => void; setIsPlaying?: (isPlaying: boolean) => void;
} }
@ -103,7 +103,6 @@ export const MgbaWrapper = forwardRef<MgbaHandle, MgbaWrapperProps>(
restart: () => mgbaRef.current?.restart(), restart: () => mgbaRef.current?.restart(),
buttonPress: (key: GbaKey) => mgbaRef.current?.buttonPress(key), buttonPress: (key: GbaKey) => mgbaRef.current?.buttonPress(key),
buttonRelease: (key: GbaKey) => mgbaRef.current?.buttonRelease(key), buttonRelease: (key: GbaKey) => mgbaRef.current?.buttonRelease(key),
hardReset: () => setMgbaId((id) => id + 1),
})); }));
useAvoidItchIoScrolling(); useAvoidItchIoScrolling();
@ -123,7 +122,6 @@ export const MgbaWrapper = forwardRef<MgbaHandle, MgbaWrapperProps>(
)} )}
{isPlaying ? ( {isPlaying ? (
<Mgba <Mgba
key={mgbaId}
ref={mgbaRef} ref={mgbaRef}
gameUrl={gameUrl} gameUrl={gameUrl}
volume={volume} volume={volume}

View file

@ -2,7 +2,9 @@ import { MutableRefObject, useEffect } from "react";
import { mGBAEmulator } from "./vendor/mgba"; import { mGBAEmulator } from "./vendor/mgba";
import { GbaKey } from "./bindings"; import { GbaKey } from "./bindings";
export function useController(mgbaModule: MutableRefObject<mGBAEmulator>) { export function useController(
mgbaModule: MutableRefObject<mGBAEmulator | undefined>
) {
useEffect(() => { useEffect(() => {
let stopped = false; let stopped = false;
@ -64,13 +66,13 @@ export function useController(mgbaModule: MutableRefObject<mGBAEmulator>) {
for (let oldButton of previouslyPressedButtons) { for (let oldButton of previouslyPressedButtons) {
if (!currentlyPressed.has(oldButton)) { if (!currentlyPressed.has(oldButton)) {
mgbaModule.current.buttonUnpress(oldButton); mgbaModule.current?.buttonUnpress(oldButton);
} }
} }
for (let newButton of currentlyPressed) { for (let newButton of currentlyPressed) {
if (!previouslyPressedButtons.has(newButton)) { if (!previouslyPressedButtons.has(newButton)) {
mgbaModule.current.buttonPress(newButton); mgbaModule.current?.buttonPress(newButton);
} }
} }

View file

@ -1,7 +1,9 @@
import { MutableRefObject, useEffect } from "react"; import { MutableRefObject, useEffect } from "react";
import { mGBAEmulator } from "./vendor/mgba"; import { mGBAEmulator } from "./vendor/mgba";
export function useFrameSkip(mgbaModule: MutableRefObject<mGBAEmulator>) { export function useFrameSkip(
mgbaModule: MutableRefObject<mGBAEmulator | undefined>
) {
useEffect(() => { useEffect(() => {
let previous: number | undefined = undefined; let previous: number | undefined = undefined;
let stopped = false; let stopped = false;
@ -23,12 +25,12 @@ export function useFrameSkip(mgbaModule: MutableRefObject<mGBAEmulator>) {
if (totalTime >= 1 / 60) { if (totalTime >= 1 / 60) {
totalTime -= 1 / 60; totalTime -= 1 / 60;
if (paused) { if (paused) {
mgbaModule.current.resumeGame(); mgbaModule.current?.resumeGame();
paused = false; paused = false;
} }
} else { } else {
if (!paused) { if (!paused) {
mgbaModule.current.pauseGame(); mgbaModule.current?.pauseGame();
paused = true; paused = true;
} }
} }

View file

@ -78,6 +78,8 @@ function shouldStartPlaying(isTouchScreen: boolean | undefined) {
return !isTouchScreen; return !isTouchScreen;
} }
const COMBO_GAME = new URL("combo.gba.gz", import.meta.url);
function MgbaWithControllerSides() { function MgbaWithControllerSides() {
const mgba = useRef<MgbaHandle>(null); const mgba = useRef<MgbaHandle>(null);
@ -105,7 +107,7 @@ function MgbaWithControllerSides() {
</GameSide> </GameSide>
<GameDisplayWindow> <GameDisplayWindow>
<MgbaWrapper <MgbaWrapper
gameUrl="combo.gba.gz" gameUrl={COMBO_GAME}
ref={mgba} ref={mgba}
isPlaying={playEmulator} isPlaying={playEmulator}
setIsPlaying={setIsPlaying} setIsPlaying={setIsPlaying}