Skip to content

Commit df340ca

Browse files
feat: n64 states saving
1 parent bfb83dc commit df340ca

5 files changed

Lines changed: 64 additions & 9 deletions

File tree

src/constants/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const cores = {
1919
mednafen_vb: { displayName: 'Beetle VB' },
2020
mednafen_wswan: { displayName: 'Beetle Wonderswan' },
2121
mgba: { displayName: 'mGBA' },
22-
'mupen64plus-libretro-nx': { displayName: 'Mupen64Plus-Next' },
22+
mupen64plus_next: { displayName: 'Mupen64Plus-Next' },
2323
nestopia: { displayName: 'Nestopia' },
2424
o2em: { displayName: 'O2EM' },
2525
pcsx_rearmed: { displayName: 'PCSX ReARMed' },

src/constants/platform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ const basePlatformMap = {
264264
libretroName: 'Sega - Mega Drive - Genesis',
265265
},
266266
n64: {
267-
cores: ['mupen64plus-libretro-nx'],
267+
cores: ['mupen64plus_next'],
268268
displayNameI18nKey: 'platform.n64',
269269
fileExtensions: ['.n64', '.z64', '.v64', '.zip'],
270270
info: {

src/pages/library/components/emulator-portal/hooks/use-emulator.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { attemptAsync, noop } from 'es-toolkit'
1+
import { attemptAsync, delay, noop } from 'es-toolkit'
22
import NoSleep from 'nosleep.js'
33
import { Nostalgist } from 'nostalgist'
44
import { useEffect, useMemo } from 'react'
@@ -32,13 +32,13 @@ const defaultRetroarchConfig: RetroarchConfig = {
3232
input_player2_analog_dpad_mode: 1,
3333
input_player3_analog_dpad_mode: 1,
3434
input_player4_analog_dpad_mode: 1,
35-
rewind_enable: true,
3635
rewind_granularity: 4,
3736
rgui_menu_color_theme: 1,
38-
run_ahead_enabled: true,
3937
run_ahead_frames: 1,
38+
video_gpu_screenshot: true,
4039
}
4140

41+
const nativeConsoleError = console.error
4242
let noSleep: NoSleep
4343
const originalGetUserMedia = globalThis.navigator?.mediaDevices?.getUserMedia?.bind(globalThis.navigator.mediaDevices)
4444
export function useEmulator() {
@@ -79,6 +79,8 @@ export function useEmulator() {
7979
...defaultRetroarchConfig,
8080
...preference.input.keyboardMapping,
8181
...gamepadMapping,
82+
rewind_enable: !['mupen64plus_next'].includes(core),
83+
run_ahead_enabled: !['mupen64plus_next', 'pcsx_rearmed'].includes(core),
8284
video_smooth: preference.emulator.videoSmooth,
8385
},
8486
retroarchCoreConfig: preference.emulator.core[core],
@@ -95,6 +97,7 @@ export function useEmulator() {
9597
isValidating,
9698
mutate: prepare,
9799
} = useSWRImmutable(options, () => Nostalgist.prepare(options))
100+
globalThis.emulator = emulator
98101

99102
const isPreparing = !rom || isValidating
100103

@@ -103,6 +106,7 @@ export function useEmulator() {
103106
return
104107
}
105108

109+
console.error = () => {}
106110
if (!withState) {
107111
emulator.getEmulator().on('beforeLaunch', () => {
108112
try {
@@ -153,9 +157,18 @@ export function useEmulator() {
153157
onCancel(noop)
154158
noSleep ||= new NoSleep()
155159
await noSleep.enable()
160+
161+
// ad-hoc patch for mupen64plus_next
162+
if (core === 'mupen64plus_next') {
163+
await delay(0)
164+
emulator.sendCommand('PAUSE_TOGGLE')
165+
await delay(0)
166+
emulator.sendCommand('PAUSE_TOGGLE')
167+
}
156168
}
157169

158170
async function exit({ reloadAfterExit = false } = {}) {
171+
console.error = nativeConsoleError
159172
const status = emulator?.getStatus() || ''
160173
if (['paused', 'running'].includes(status)) {
161174
emulator?.exit()

src/pages/library/components/emulator-portal/hooks/use-game-states.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,42 @@ async function getStateAndThumbnail(emulator: Nostalgist, object: { state?: File
2525
return { state, thumbnail }
2626
}
2727

28+
async function forceGetEmulatorThumbnail(emulator: Nostalgist, resolution: { width: number; height: number }) {
29+
const source = emulator.getCanvas()
30+
const { width, height } = source
31+
let sw: number
32+
let sh: number
33+
let sx: number
34+
let sy: number
35+
if (width / height > resolution.width / resolution.height) {
36+
sh = height
37+
sw = Math.round(height * (resolution.width / resolution.height))
38+
sx = Math.round((width - sw) / 2)
39+
sy = 0
40+
} else {
41+
sw = width
42+
sh = Math.round(width / (resolution.width / resolution.height))
43+
sx = 0
44+
sy = Math.round((height - sh) / 2)
45+
}
46+
const target = document.createElement('canvas')
47+
target.width = sw
48+
target.height = sh
49+
const context = target.getContext('2d')
50+
return await new Promise<File>((resolve) => {
51+
requestAnimationFrame(() => {
52+
// @ts-expect-error let's assume the context is always available
53+
context.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh)
54+
target.toBlob((blob) => {
55+
if (blob) {
56+
// @ts-expect-error the returned value will be used as hono client form value
57+
resolve(blob)
58+
}
59+
})
60+
})
61+
})
62+
}
63+
2864
export function useGameStates() {
2965
const rom = useRom()
3066
const { core, emulator } = useEmulator()
@@ -53,7 +89,10 @@ export function useGameStates() {
5389
if (!emulator || !core || !rom) {
5490
throw new Error('invalid emulator or core or rom')
5591
}
56-
const { state, thumbnail } = await getStateAndThumbnail(emulator, arg)
92+
let { state, thumbnail } = await getStateAndThumbnail(emulator, arg)
93+
if (core === 'mupen64plus_next') {
94+
thumbnail = await forceGetEmulatorThumbnail(emulator, { height: 3, width: 4 })
95+
}
5796
await $post({ form: { core, rom: rom.id, state, thumbnail, type: 'manual' } })
5897
await Promise.all([reloadStates(), reloadAutoStates()])
5998
},
@@ -63,7 +102,10 @@ export function useGameStates() {
63102
if (!emulator || !core || !rom) {
64103
throw new Error('invalid emulator or core or rom')
65104
}
66-
const { state, thumbnail } = await emulator.saveState()
105+
let { state, thumbnail } = await emulator.saveState()
106+
if (core === 'mupen64plus_next') {
107+
thumbnail = await forceGetEmulatorThumbnail(emulator, { height: 3, width: 4 })
108+
}
67109
await $post({
68110
// @ts-expect-error actually we can use Blob here thought it says only File is accepted
69111
form: { core, rom: rom.id, state, thumbnail, type: 'auto' },

src/pages/library/utils/nostalgist.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { cdnHost } from '#@/utils/isomorphic/cdn.ts'
66
const extractCache = new Map<string, ReturnType<typeof extractCore>>()
77

88
function getCoreCDNUrl(core: string) {
9-
const externalCores = ['a5200', 'prosystem', 'stella2014', 'mupen64plus-libretro-nx']
9+
const externalCores = ['a5200', 'prosystem', 'stella2014', 'mupen64plus_next']
1010
const segments = externalCores.includes(core)
1111
? [
1212
'npm',
13-
['retroassembly-custom-cores', '1.22.2-20260610142137'].join('@'),
13+
['retroassembly-custom-cores', '1.22.2-20260614000946'].join('@'),
1414
'dist',
1515
'cores',
1616
`${core}_libretro.zip`,

0 commit comments

Comments
 (0)