Skip to content

Commit

Permalink
feat: add support for saving and importing SRAM
Browse files Browse the repository at this point in the history
  • Loading branch information
arianrhodsandlot committed Jan 23, 2025
1 parent 8d12600 commit e9a6bef
Show file tree
Hide file tree
Showing 82 changed files with 426 additions and 251 deletions.
8 changes: 8 additions & 0 deletions docs/src/content/docs/apis/launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ const nostalgist = await Nostalgist.launch({
The initial state to be loaded after launching.
+ #### `sram`
**type:** `Blob`
**since:** `0.12.0`
The initial sram to be loaded after launching.
+ #### `shader`
**type:** `string`
Expand Down
29 changes: 29 additions & 0 deletions docs/src/content/docs/apis/save-sram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: saveSRAM
---

Save the SRAM of the current running game.

## Since
`0.12.0`

## Usage
```js
const nostalgist = await Nostalgist.nes('zelda.nes')

// save the SRAM
const sram = await nostalgist.saveSRAM()

// launch with the SRAM
await nostalgist.nes({
rom: 'zelda.nes',
sram,
})
```

## Returns
+ ### `state`

**type:** `Blob`

The SRAM of the current running game.
2 changes: 1 addition & 1 deletion docs/src/content/docs/apis/save-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Save the state of the current running game.
const nostalgist = await Nostalgist.nes('flappybird.nes')

// save the state
const { state } = await nostalgist.saveState(state)
const { state } = await nostalgist.saveState()

// load the state
await nostalgist.loadState(state)
Expand Down
8 changes: 1 addition & 7 deletions eslint.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
import { createConfig } from '@arianrhodsandlot/eslint-config'

export default createConfig({
rules: {
'sonarjs/no-empty-test-file': 'off',
},
})
export { default } from '@arianrhodsandlot/eslint-config'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"@happy-dom/global-registrator": "16.7.2",
"@playwright/test": "1.49.1",
"@types/is-ci": "3.0.4",
"@types/node": "22.10.7",
"@types/node": "22.10.8",
"@types/wicg-file-system-access": "2023.10.5",
"astro": "5.1.8",
"eslint": "9.18.0",
Expand Down
218 changes: 131 additions & 87 deletions playground/activate.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1,157 @@
import type { Nostalgist as Nostalgist_ } from '../src/index.ts'
import { testSRAMDataUrl, testSRAMRomUrl, testStateDataUrl } from './constants.ts'

let Nostalgist: typeof Nostalgist_
let nostalgist: Nostalgist_
let state: { state: Blob }
let sram: Blob

const handlers = {
async nes() {
nostalgist = await Nostalgist.nes('pong1k.nes')
},
static: {
async nes() {
nostalgist = await Nostalgist.nes('pong1k.nes')
},

async megadrive() {
nostalgist = await Nostalgist.megadrive('asciiwar.bin')
},
async megadrive() {
nostalgist = await Nostalgist.megadrive('asciiwar.bin')
},

async gbc() {
nostalgist = await Nostalgist.gbc('combatsoccer.gbc')
},
async gbc() {
nostalgist = await Nostalgist.gbc('combatsoccer.gbc')
},

async launchSize() {
nostalgist = await Nostalgist.gbc({ rom: 'combatsoccer.gbc', size: { height: 100, width: 100 } })
},
async launchSize() {
nostalgist = await Nostalgist.gbc({ rom: 'combatsoccer.gbc', size: { height: 100, width: 100 } })
},

async launchShader() {
nostalgist = await Nostalgist.launch({ core: 'genesis_plus_gx', rom: 'asciiwar.bin', shader: 'crt/crt-geom' })
},
async launchShader() {
nostalgist = await Nostalgist.launch({ core: 'genesis_plus_gx', rom: 'asciiwar.bin', shader: 'crt/crt-geom' })
},

async launchAndCancel() {
const abortController = new AbortController()
setTimeout(() => {
abortController.abort()
}, 500)
nostalgist = await Nostalgist.nes({ rom: 'pong1k.nes', signal: abortController.signal })
},
async launchAndCancel() {
const abortController = new AbortController()
setTimeout(() => {
abortController.abort()
}, 500)
nostalgist = await Nostalgist.nes({ rom: 'pong1k.nes', signal: abortController.signal })
},

async launchWithHooks() {
nostalgist = await Nostalgist.nes({
async beforeLaunch(nostalgist) {
globalThis.nostalgist = nostalgist
console.warn(typeof nostalgist, 'beforeLaunch')
await new Promise((resolve) => {
setTimeout(resolve, 100)
})
},
onLaunch(nostalgist) {
console.warn(typeof nostalgist, 'onLaunch')
},
rom: 'pong1k.nes',
})
},
async launchWithHooks() {
nostalgist = await Nostalgist.nes({
async beforeLaunch(nostalgist) {
globalThis.nostalgist = nostalgist
console.warn(typeof nostalgist, 'beforeLaunch')
await new Promise((resolve) => {
setTimeout(resolve, 100)
})
},
onLaunch(nostalgist) {
console.warn(typeof nostalgist, 'onLaunch')
},
rom: 'pong1k.nes',
})
},

async launchState() {
const stateDataUrl =
'data:application/octet-stream;base64,I1JaSVB2ASMAAAIAuDUAAAAAAABBBAAAeJztmt9rHEUcwL+7cxe3PZqNtUnR1usd1DaxFfYuOSw2kuztj9wl92Ozu3fXi7VULPEn5MUfsc1DJEEsTUKgYJGIlEBfqmgrItXH/gUB9clnqW8W8uKDMn5ndjc9o01LYwzF+ZC5+cyPnf3uzh07s8TVPV/3LalslVOf5gBsw6MLmNcOAUcqKACOASBjYd+MzqowOSdCgWYkXih/OKEcNvOB/O7qZQA2DkxLwbCTyyrLemAxWYJk3wc/fD1/BctT3bDG44NHe2A3dKAO7mcVZ67z+m4cSOqELUS5wbNbV4D8uwNfC7IB0O5fHpjJZYpD4N/0avy2BN/DMxpI2o8Aq3EtAbdgb9CKaNJ9DplO9R9Nq0f60+qzXWn16eOgdnaA2p3CdAzUwz3r+988f/P88uSv7wPEn/pu9sjlLyQY/5zIpz6TpZe/jJHTX8XlF1Nt0lR3lzRFWbLJSTpEztACeZMWyVt0mEzRETJLS2SONrE8huUXsHwSy8/JU/S4PEv75Tk6Lp+jr8gz9FX5An1Nvkhfl5foG/LHtI71Dax/h5yj75IZOkku0PfIRXqWLFGMhWIsFGOhGAvFWCjGvJVpvp0sqGS+kyx0kQ+/pXT+QJAlyVyapCi9TFd+Of0AU/0XbmySzZ5fIBAI1rOnOynbmA/r5TJ/4BRHS/kYE1xaFI2Jl5izBvS3mV+jlPpe3sPnPfx8Sm1jXcsTEw5fW5BLOwAqvu6ydYVge0lIMonF29S75YqyY2dC2dWOufJPeceju7s2So/t6dy7Udq3/8knNkrJh5zUPdju+DbLvb5ftXVcDdkZ8l98xwV3R/z+txbx+9/49++4ejkFf9+fDuD+dAD3p5iODeD+1MOObInxf9+HOk7NZSusxeQgjFSrI3w5Zlq6ycXxnBKXE1XbZiLV/aGgxtVNk70Iiit+ZFDP13ivpDNkVZgsxg7g53DTzbMOCgxXm14snMmS53t8pPhBAmAXjAprWVkiYBsVn5/M8Yb4Sx9NCdI0rMZvgyRZFaPAOqjFUTtYQFZca4idYvl53y3yKgnF4G2W5jmBZCLJRqKVq0yIlYkkG4lmZvg4ViaSbCSaWWeiYFM9PCqQRMmqaOwy2HMIPdPi2RbvjdxrWJbMb4fh2pno1qBnI/cahs96SJJXdEfZSQZzumGw5iaWcvkiv1fxHM5CdEzOK47d8QKfE8jVzXIo4dXnvLFgLvHYUOyyH8xuw9SDV3m6wcM6lPiJoPOwPvJXZNcwef0jrLNhZqMVO951PsO7mIdxJs9egkron7S3g5E37lxq3si2eG+L97V4bu12VPy1b5DvVe3IG6VwTIV7tsV7o9jQ+6J6G6+LbxfGv+G7CSxnWVn6jXJ8o1pjfVljQ69b07D9dCgpDIa9QcWcCgQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgeOgBsCpmiv1bwJ+v4hVL'
const response = await fetch(stateDataUrl)
const state = await response.blob()
nostalgist = await Nostalgist.nes({ rom: 'flappybird.nes', state })
},
async launchState() {
const response = await fetch(testStateDataUrl)
const state = await response.blob()
nostalgist = await Nostalgist.nes({ rom: 'flappybird.nes', state })
},

async saveState() {
state = await nostalgist.saveState()
console.info(state)
},
async launchConstantSRAM() {
const response = await fetch(testSRAMDataUrl)
nostalgist = await Nostalgist.nes({
rom: testSRAMRomUrl,
sram: await response.blob(),
})
},

async loadState() {
await nostalgist.loadState(state.state)
},
async launchSavedSRAM() {
nostalgist = await Nostalgist.nes({ rom: testSRAMRomUrl, sram })
},

pause() {
nostalgist.pause()
async launchROMSupportsSRAM() {
nostalgist = await Nostalgist.nes({ rom: testSRAMRomUrl })
},
},

resume() {
nostalgist.resume()
},
instance: {
async saveState() {
state = await nostalgist.saveState()
console.info(state)
},

restart() {
nostalgist.restart()
},
async loadState() {
await nostalgist.loadState(state.state)
},

resize() {
nostalgist.resize({ height: 400, width: 400 })
},
async saveSRAM() {
sram = await nostalgist.saveSRAM()
},

async pressA() {
await nostalgist.press('a')
},
pause() {
nostalgist.pause()
},

async pressStart() {
await nostalgist.press('start')
},
resume() {
nostalgist.resume()
},

async screenshot() {
const blob = await nostalgist.screenshot()
const image = new Image()
image.id = 'screenshot'
image.src = URL.createObjectURL(blob)
image.style.display = 'block'
image.style.margin = '1em auto 0 auto'
await new Promise((resolve) => {
image.addEventListener('load', resolve)
})
document.body.append(image)
},
restart() {
nostalgist.restart()
},

exit() {
nostalgist.exit()
},
resize() {
nostalgist.resize({ height: 400, width: 400 })
},

exitWithoutRemovingCanvas() {
nostalgist.exit({ removeCanvas: false })
async pressA() {
await nostalgist.press('a')
},

async pressStart() {
await nostalgist.press('start')
},

async screenshot() {
const blob = await nostalgist.screenshot()
const image = new Image()
image.id = 'screenshot'
image.src = URL.createObjectURL(blob)
image.style.display = 'block'
image.style.margin = '1em auto 0 auto'
await new Promise((resolve) => {
image.addEventListener('load', resolve)
})
document.body.append(image)
},

exit() {
nostalgist.exit()
},

exitWithoutRemovingCanvas() {
nostalgist.exit({ removeCanvas: false })
},
},
}

function renderButtons() {
const [staticTitle, instanceTitle] = document.querySelectorAll('h3')
staticTitle.insertAdjacentHTML(
'afterend',
Object.keys(handlers.static)
.map((name) => `<button type="button">${name}</button>`)
.join(' '),
)
instanceTitle.insertAdjacentHTML(
'afterend',
Object.keys(handlers.instance)
.map((name) => `<button type="button">${name}</button>`)
.join(' '),
)
}

export function activate(mod: typeof Nostalgist_) {
const nostalgistConfig = {
style: {
Expand Down Expand Up @@ -144,14 +184,18 @@ export function activate(mod: typeof Nostalgist_) {
Nostalgist = mod
Nostalgist.configure(nostalgistConfig)

renderButtons()

document.body.addEventListener('click', async function listener({ target }) {
if (!(target instanceof HTMLButtonElement && target.textContent)) {
return
}
target.disabled = true
try {
await handlers[target.textContent]()
} catch {}
await { ...handlers.instance, ...handlers.static }[target.textContent]()
} catch (error) {
console.error(error)
}
target.disabled = false
target.blur()
})
Expand Down
Loading

0 comments on commit e9a6bef

Please sign in to comment.