Saving, technically done
This commit is contained in:
@ -63,7 +63,6 @@ class HomeplayEmulator {
|
|||||||
try {
|
try {
|
||||||
this.saveUpdates = await this.hasSaveUpdates();
|
this.saveUpdates = await this.hasSaveUpdates();
|
||||||
if(this.saveUpdates) {
|
if(this.saveUpdates) {
|
||||||
console.log('Saving');
|
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@ -111,7 +110,9 @@ class HomeplayEmulator {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.EJS_emulator.gameManager.saveSaveFiles();
|
window.EJS_emulator.gameManager.saveSaveFiles();
|
||||||
this.gameStarted = true;
|
setTimeout(() => {
|
||||||
|
this.gameStarted = true;
|
||||||
|
}, 1000);
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,12 +146,8 @@ class HomeplayEmulator {
|
|||||||
window.EJS_volume = data.volume;
|
window.EJS_volume = data.volume;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'save':
|
|
||||||
window.EJS_emulator.saveSaveFiles();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error('Unknown message', message);
|
console.error('Website sent invalid message', data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,19 +181,21 @@ class HomeplayEmulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSaveFiles = () => {
|
getSaveFiles = () => {
|
||||||
return [ 'rom.srm' ];
|
return [ 'rom.srm', 'rom.rtc' ];
|
||||||
}
|
}
|
||||||
|
|
||||||
hasSaveUpdates = async () => {
|
hasSaveUpdates = async () => {
|
||||||
if(!window.EJS_emulator) return false;
|
if(!window.EJS_emulator) return false;
|
||||||
if(!window.EJS_emulator.gameManager) return false;
|
if(!window.EJS_emulator.gameManager) return false;
|
||||||
if(!this.gameStarted) return false;
|
if(!this.gameStarted) return false;
|
||||||
|
// At least 20 seconds must've passed.
|
||||||
|
if(window.EJS_emulator.gameManager.getFrameNum() < 20 * 60) return false;
|
||||||
|
|
||||||
const saveFiles = this.getSaveFiles();
|
const saveFiles = this.getSaveFiles();
|
||||||
const hashes = await Promise.all(saveFiles.map(f => this.getFileMD5(`/homeplay/saves/${f}`)));
|
const hashes = await Promise.all(saveFiles.map(f => this.getFileMD5(`/homeplay/saves/${f}`)));
|
||||||
|
|
||||||
window.EJS_emulator.gameManager.saveSaveFiles();
|
window.EJS_emulator.gameManager.saveSaveFiles();
|
||||||
|
|
||||||
const newHashes = await Promise.all(saveFiles.map(f => this.getFileMD5(`/homeplay/saves/${f}`)));
|
const newHashes = await Promise.all(saveFiles.map(f => this.getFileMD5(`/homeplay/saves/${f}`)));
|
||||||
const changedFiles = saveFiles.filter((f, i) => hashes[i] !== newHashes[i]);
|
const changedFiles = saveFiles.filter((f, i) => hashes[i] !== newHashes[i]);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import styles from './Emulator.module.scss';
|
import styles from './Emulator.module.scss';
|
||||||
import { GAME_SYSTEM_CORES, GameSystem, GameSystemCore } from '@/lib/game';
|
import { GAME_SYSTEM_CORES, GameSystem, GameSystemCore } from '@/lib/game';
|
||||||
import { useLanguage } from '@/providers/LanguageProvider';
|
import { useLanguage } from '@/providers/LanguageProvider';
|
||||||
|
import { useAPI } from '@/providers/APIProvider';
|
||||||
|
|
||||||
type SendEmulatorMessageInit = {
|
type SendEmulatorMessageInit = {
|
||||||
message:'init';
|
message:'init';
|
||||||
@ -18,8 +19,7 @@ type SendEmulatorMessageVolume = {
|
|||||||
|
|
||||||
type SendEmulatorMessage = (
|
type SendEmulatorMessage = (
|
||||||
SendEmulatorMessageInit |
|
SendEmulatorMessageInit |
|
||||||
SendEmulatorMessageVolume |
|
SendEmulatorMessageVolume
|
||||||
{ message: 'save' }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
type ReceiveEmulatorMessage = (
|
type ReceiveEmulatorMessage = (
|
||||||
@ -41,11 +41,32 @@ export const Emulator:React.FC<EmulatorProps> = props => {
|
|||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const [ hasInit, setHasInit ] = useState(false);
|
const [ hasInit, setHasInit ] = useState(false);
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const [ saving, setSaving ] = useState<boolean>(false);
|
||||||
|
const [ savingError, setSavingError ] = useState<string|null>(null);
|
||||||
|
const { saveUpdate } = useAPI();
|
||||||
|
|
||||||
const send = (msg:SendEmulatorMessage) => {
|
const send = (msg:SendEmulatorMessage) => {
|
||||||
iframeRef.current?.contentWindow?.postMessage(msg, '*');
|
iframeRef.current?.contentWindow?.postMessage(msg, '*');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const save = async (data:string) => {
|
||||||
|
if(saving) return;
|
||||||
|
if(!data) return;
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await saveUpdate({
|
||||||
|
gameId: props.gameId,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
console.log('res', res);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(hasInit) return;
|
if(hasInit) return;
|
||||||
setHasInit(true);
|
setHasInit(true);
|
||||||
@ -70,11 +91,7 @@ export const Emulator:React.FC<EmulatorProps> = props => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'save':
|
case 'save':
|
||||||
// Download save data
|
save(msg.data).catch(console.error);
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = `data:application/zip;base64,${msg.data}`;
|
|
||||||
a.download = `${props.gameId}.zip`;
|
|
||||||
a.click();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'start':
|
case 'start':
|
||||||
@ -82,6 +99,7 @@ export const Emulator:React.FC<EmulatorProps> = props => {
|
|||||||
case 'load_state':
|
case 'load_state':
|
||||||
case 'save_state':
|
case 'save_state':
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error('Invalid message received:', msg);
|
console.error('Invalid message received:', msg);
|
||||||
break
|
break
|
||||||
|
@ -2,12 +2,15 @@ import React from 'react';
|
|||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import './globals.scss';
|
import './globals.scss';
|
||||||
import { LanguageProvider } from '@/providers/LanguageProvider';
|
import { LanguageProvider } from '@/providers/LanguageProvider';
|
||||||
|
import { APIProvider } from '@/providers/APIProvider';
|
||||||
|
|
||||||
const RootLayout:React.FC<AppProps> = ({ Component, pageProps }) => {
|
const RootLayout:React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<Component {...pageProps} />
|
<APIProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</APIProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,13 +4,41 @@ import * as path from 'path';
|
|||||||
|
|
||||||
const PATH_SAVES = path.resolve('.', 'data', 'saves');
|
const PATH_SAVES = path.resolve('.', 'data', 'saves');
|
||||||
|
|
||||||
const handler:NextApiHandler = async (req, res) => {
|
const handlerPut:NextApiHandler = async (req, res) => {
|
||||||
if (req.method !== 'GET') {
|
if(!req.body || typeof req.body !== 'object') {
|
||||||
res.setHeader('Allow', ['GET']);
|
res.status(400).end(`Bad Request`);
|
||||||
res.status(405).end(`Method Not Allowed`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!req.body.gameId || typeof req.body.gameId !== 'string') {
|
||||||
|
res.status(400).end(`Bad Request`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!req.body.data || typeof req.body.data !== 'string') {
|
||||||
|
res.status(400).end(`Bad Request`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the length of the data.
|
||||||
|
if(req.body.data.length > 1024 * 1024) {// 1MB
|
||||||
|
res.status(413).end(`Payload Too Large`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data is a base64 string, convert it to a binary zip file.
|
||||||
|
const pathSave = path.resolve(PATH_SAVES, `${req.body.gameId}.zip`);
|
||||||
|
try {
|
||||||
|
const data = Buffer.from(req.body.data, 'base64');
|
||||||
|
fs.promises.writeFile(pathSave, data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).end(`Internal Server Error`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).end(`true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerGet:NextApiHandler = async (req, res) => {
|
||||||
// ID query param
|
// ID query param
|
||||||
if(!req.query.id || typeof req.query.id !== 'string') {
|
if(!req.query.id || typeof req.query.id !== 'string') {
|
||||||
res.status(404).end(`Not Found`);
|
res.status(404).end(`Not Found`);
|
||||||
@ -39,6 +67,20 @@ const handler:NextApiHandler = async (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).end(`Internal Server Error`);
|
res.status(500).end(`Internal Server Error`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler:NextApiHandler = async (req, res) => {
|
||||||
|
switch(req.method) {
|
||||||
|
case 'POST':
|
||||||
|
case 'PUT':
|
||||||
|
return handlerPut(req, res);
|
||||||
|
|
||||||
|
case 'GET':
|
||||||
|
return handlerGet(req, res);
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.status(405).end(`Method Not Allowed`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
60
src/providers/APIProvider.tsx
Normal file
60
src/providers/APIProvider.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React, { createContext, useContext, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type APIContextType = {
|
||||||
|
saveUpdate:(p:{ gameId:string, data:string }) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const APIContext = createContext<APIContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const APIProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const apiSend = async <T,>(p:{
|
||||||
|
path:string;
|
||||||
|
method:'POST'|'GET'|'PUT'|'DELETE';
|
||||||
|
body?:any;
|
||||||
|
query?:Record<string, string>;
|
||||||
|
}) => {
|
||||||
|
const query = p.query ? '?' + Object.entries(p.query).map(([k, v]) => {
|
||||||
|
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
||||||
|
}).join('&') : '';
|
||||||
|
|
||||||
|
const res = await fetch(`/api/v1/${p.path}${query}`, {
|
||||||
|
method: p.method,
|
||||||
|
body: p.body ? JSON.stringify(p.body) : undefined,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!res.ok) {
|
||||||
|
throw new Error(`API Error: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const saveUpdate:APIContextType['saveUpdate'] = async p => {
|
||||||
|
return await apiSend({
|
||||||
|
path: 'save',
|
||||||
|
method: 'PUT',
|
||||||
|
body: p
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provider
|
||||||
|
return (
|
||||||
|
<APIContext.Provider value={{
|
||||||
|
saveUpdate,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</APIContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAPI = (): APIContextType => {
|
||||||
|
const context = useContext(APIContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAPI must be used within an APIProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
Reference in New Issue
Block a user