web: savegames through the page, since there is no saves/ directory
mrpci_request_save / mrpci_save_out / mrpci_save_in carry a SaveData across the wasm boundary, so the browser build can save and restore even though Command::SaveGame's std::fs call goes nowhere there. The moment being stored is exactly the one the native build writes to saves/<slot>.json — the same GameState::save_data — so a slot means the same thing in both places. F5 now asks the page instead of the filesystem; F7 points at the SAVES menu, because restoring needs to know *which* slot and a function key can't say. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
50ede0febf
commit
caf4d5e558
@ -13,4 +13,6 @@ rustflags = [
|
||||
"-C", "link-arg=--allow-undefined",
|
||||
"-C", "link-arg=--export=mrpci_alloc",
|
||||
"-C", "link-arg=--export=mrpci_world_in",
|
||||
"-C", "link-arg=--export=mrpci_save_in",
|
||||
"-C", "link-arg=--export=mrpci_request_save",
|
||||
]
|
||||
|
||||
@ -126,9 +126,20 @@ pub fn world_from_bundle(json: &str) -> Option<World> {
|
||||
/// on the next frame. No threads, no async.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web_bridge {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub static PENDING_LOAD: Mutex<Option<String>> = Mutex::new(None);
|
||||
pub static PENDING_RESTORE: Mutex<Option<String>> = Mutex::new(None);
|
||||
// Atomic, not Mutex<bool>: with no threads in the picture LLVM is free to
|
||||
// const-fold a plain static whose only writer is JS, which it cannot see.
|
||||
// MRPGI learned this the hard way; don't relearn it.
|
||||
pub static SAVE_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
extern "C" {
|
||||
/// The engine hands a savegame back to the page.
|
||||
pub fn mrpci_save_out(ptr: *const u8, len: usize);
|
||||
}
|
||||
|
||||
/// JS asks for a buffer to write into.
|
||||
#[no_mangle]
|
||||
@ -139,11 +150,26 @@ mod web_bridge {
|
||||
/// JS hands over a bundle JSON it wrote into that buffer.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mrpci_world_in(ptr: *mut u8, len: usize) {
|
||||
let s = unsafe {
|
||||
*PENDING_LOAD.lock().unwrap() = Some(take(ptr, len));
|
||||
}
|
||||
|
||||
/// JS hands over a savegame to restore.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mrpci_save_in(ptr: *mut u8, len: usize) {
|
||||
*PENDING_RESTORE.lock().unwrap() = Some(take(ptr, len));
|
||||
}
|
||||
|
||||
/// JS asks the engine to emit its current progress on the next frame.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mrpci_request_save() {
|
||||
SAVE_REQUESTED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn take(ptr: *mut u8, len: usize) -> String {
|
||||
unsafe {
|
||||
let boxed = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]);
|
||||
String::from_utf8_lossy(&boxed).into_owned()
|
||||
};
|
||||
*PENDING_LOAD.lock().unwrap() = Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,6 +253,34 @@ async fn amain(game_dir: String) {
|
||||
// --- a game handed over by the page ----------------------------------
|
||||
// The browser build is a *player*: the picker (and drag-and-drop)
|
||||
// swap the whole world underneath it, and the session restarts.
|
||||
// --- savegames, via the page --------------------------------------
|
||||
// There is no `saves/` directory in a browser, so F5/F7 and the
|
||||
// arcade's slot menu both go out through the page instead. The
|
||||
// moment being saved is exactly the one the native build writes to
|
||||
// disk — GameState::save_data — so a slot means the same thing in
|
||||
// both places.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
use std::sync::atomic::Ordering;
|
||||
if web_bridge::SAVE_REQUESTED.swap(false, Ordering::SeqCst) {
|
||||
match serde_json::to_string(&gs.save_data()) {
|
||||
Ok(j) => unsafe { web_bridge::mrpci_save_out(j.as_ptr(), j.len()) },
|
||||
Err(e) => ui.log.push(format!("(couldn't pack the save: {})", e)),
|
||||
}
|
||||
}
|
||||
let restore = web_bridge::PENDING_RESTORE.lock().unwrap().take();
|
||||
if let Some(json) = restore {
|
||||
match serde_json::from_str::<mrpci_core::SaveData>(&json) {
|
||||
Ok(d) => {
|
||||
let evs = gs.restore_data(d);
|
||||
handle_events(&evs, &mut ui, &mut audio);
|
||||
ui.log.push("(restored.)".into());
|
||||
}
|
||||
Err(_) => ui.log.push("(that isn't an MRPCI savegame.)".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let incoming = web_bridge::PENDING_LOAD.lock().unwrap().take();
|
||||
@ -321,13 +375,29 @@ async fn amain(game_dir: String) {
|
||||
ui.typing = true;
|
||||
ui.input.clear();
|
||||
}
|
||||
// Native writes saves/quick.json. The browser has no saves/ dir,
|
||||
// so the same two keys ask the page to put the blob wherever the
|
||||
// player's savegames live — their account, or this browser.
|
||||
if is_key_pressed(KeyCode::F5) {
|
||||
let evs = gs.apply(Command::SaveGame { slot: "quick".into() });
|
||||
handle_events(&evs, &mut ui, &mut audio);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let evs = gs.apply(Command::SaveGame { slot: "quick".into() });
|
||||
handle_events(&evs, &mut ui, &mut audio);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
web_bridge::SAVE_REQUESTED.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
ui.log.push("(saving\u{2026})".into());
|
||||
}
|
||||
}
|
||||
if is_key_pressed(KeyCode::F7) {
|
||||
let evs = gs.apply(Command::RestoreGame { slot: "quick".into() });
|
||||
handle_events(&evs, &mut ui, &mut audio);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let evs = gs.apply(Command::RestoreGame { slot: "quick".into() });
|
||||
handle_events(&evs, &mut ui, &mut audio);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
ui.log.push("(use the SAVES menu up top to restore.)".into());
|
||||
}
|
||||
if is_key_pressed(KeyCode::F9) {
|
||||
let evs = gs.apply(Command::NewGame { room: None });
|
||||
|
||||
@ -45,14 +45,48 @@
|
||||
<script>
|
||||
// miniquad resolves the engine's undefined imports at runtime; register
|
||||
// before the wasm loads or the instantiation fails.
|
||||
miniquad_add_plugin({ register_plugin: () => {}, version: '1', name: 'mrpci_bridge' })
|
||||
let onSaveOut = null
|
||||
miniquad_add_plugin({
|
||||
register_plugin: (imports) => {
|
||||
imports.env.mrpci_save_out = (ptr, len) => {
|
||||
const json = new TextDecoder().decode(new Uint8Array(wasm_memory.buffer, ptr, len))
|
||||
if (onSaveOut) onSaveOut(json)
|
||||
}
|
||||
},
|
||||
version: '1',
|
||||
name: 'mrpci_bridge',
|
||||
})
|
||||
|
||||
/** Hand a WorldBundle JSON to the running engine. */
|
||||
function sendBundleToEngine(json) {
|
||||
/** Copy a string into the engine and call an exported entry point. */
|
||||
function handOver(fn, json) {
|
||||
const bytes = new TextEncoder().encode(json)
|
||||
const ptr = wasm_exports.mrpci_alloc(bytes.length)
|
||||
new Uint8Array(wasm_memory.buffer, ptr, bytes.length).set(bytes)
|
||||
wasm_exports.mrpci_world_in(ptr, bytes.length)
|
||||
fn(ptr, bytes.length)
|
||||
}
|
||||
|
||||
/** Hand a WorldBundle JSON to the running engine. */
|
||||
function sendBundleToEngine(json) {
|
||||
handOver(wasm_exports.mrpci_world_in, json)
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine emits its progress a frame after being asked, so a save
|
||||
* is a round trip, not a return value — hence the promise. The timeout
|
||||
* matters: if the engine is wedged, the menu should say so rather than
|
||||
* hang on a button press forever.
|
||||
*/
|
||||
function captureSave() {
|
||||
return new Promise((resolve) => {
|
||||
if (!wasm_exports?.mrpci_request_save) return resolve(null)
|
||||
const timer = setTimeout(() => { onSaveOut = null; resolve(null) }, 2000)
|
||||
onSaveOut = (json) => {
|
||||
clearTimeout(timer)
|
||||
onSaveOut = null
|
||||
resolve(json)
|
||||
}
|
||||
wasm_exports.mrpci_request_save()
|
||||
})
|
||||
}
|
||||
|
||||
load('mrpci.wasm')
|
||||
@ -70,6 +104,10 @@
|
||||
engine: 'mrpci',
|
||||
ready: () => typeof wasm_exports !== 'undefined' && !!wasm_exports,
|
||||
load: (json) => sendBundleToEngine(json),
|
||||
saves: {
|
||||
capture: () => captureSave(),
|
||||
restore: (json) => handOver(wasm_exports.mrpci_save_in, json),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user