Files
dungeoncraft/src/platform.rs
T
Richard ZinkandClaude Opus 5.5 7f4324abca Server-side savegames: Rust service with a shared savegame list in the menu, fallback to files
- server/: small axum service storing savegames as files (1 MB each, 500 MB total, rate limit)
- docker compose: second container with a volume; nginx forwards /api/
- JS plugin: asynchronous HTTP requests polled by the game
- Save/Load open a list page (name field, new, overwrite, load, delete with confirmation)
- Without a reachable service the game keeps using savegame files and the browser autosave

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:31:36 +01:00

128 lines
4.3 KiB
Rust

// Browser integration via our own miniquad JS plugin (www/dc_plugin.js):
// persistent key/value storage (localStorage), a cache that is written to storage when the
// page is hidden or closed, file download, file upload and HTTP requests.
// Native builds use simple fallbacks.
// Result of an HTTP request: status 0 means the service could not be reached
pub struct HttpResult {
pub status: i32,
pub body: Vec<u8>,
}
#[cfg(target_arch = "wasm32")]
mod imp {
extern "C" {
fn dc_storage_get(key: *const u8, key_len: u32) -> i32;
fn dc_storage_take(dst: *mut u8);
fn dc_storage_set(key: *const u8, key_len: u32, val: *const u8, val_len: u32);
fn dc_cache_set(key: *const u8, key_len: u32, val: *const u8, val_len: u32);
fn dc_download(name: *const u8, name_len: u32, data: *const u8, data_len: u32);
fn dc_open_file();
fn dc_file_poll() -> i32;
fn dc_file_take(dst: *mut u8);
fn dc_http(m: *const u8, ml: u32, u: *const u8, ul: u32, b: *const u8, bl: u32) -> u32;
fn dc_http_poll(id: u32) -> i32;
fn dc_http_len(id: u32) -> u32;
fn dc_http_take(id: u32, dst: *mut u8);
}
// starts a request; poll the returned id with http_poll()
pub fn http(method: &str, url: &str, body: &[u8]) -> u32 {
unsafe { dc_http(method.as_ptr(), method.len() as u32, url.as_ptr(), url.len() as u32, body.as_ptr(), body.len() as u32) }
}
pub fn http_poll(id: u32) -> Option<super::HttpResult> {
unsafe {
let status = dc_http_poll(id);
if status == -2 {
return None;
}
let mut body = vec![0u8; dc_http_len(id) as usize];
dc_http_take(id, body.as_mut_ptr());
Some(super::HttpResult { status, body })
}
}
pub fn storage_get(key: &str) -> Option<String> {
unsafe {
let n = dc_storage_get(key.as_ptr(), key.len() as u32);
if n < 0 {
return None;
}
let mut buf = vec![0u8; n as usize];
dc_storage_take(buf.as_mut_ptr());
String::from_utf8(buf).ok()
}
}
pub fn storage_set(key: &str, val: &str) {
unsafe { dc_storage_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) }
}
// value is written to storage by JS when the page gets hidden or closed
pub fn cache_on_leave(key: &str, val: &str) {
unsafe { dc_cache_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) }
}
pub fn download(name: &str, data: &[u8]) {
unsafe { dc_download(name.as_ptr(), name.len() as u32, data.as_ptr(), data.len() as u32) }
}
// opens the browser file picker; poll with take_file()
pub fn open_file() {
unsafe { dc_open_file() }
}
pub fn take_file() -> Option<Vec<u8>> {
unsafe {
let n = dc_file_poll();
if n < 0 {
return None;
}
let mut buf = vec![0u8; n as usize];
dc_file_take(buf.as_mut_ptr());
Some(buf)
}
}
}
#[cfg(not(target_arch = "wasm32"))]
mod imp {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static STORE: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
}
pub fn storage_get(key: &str) -> Option<String> {
STORE.with(|s| s.borrow().get(key).cloned())
}
pub fn storage_set(key: &str, val: &str) {
STORE.with(|s| s.borrow_mut().insert(key.to_string(), val.to_string()));
}
pub fn cache_on_leave(key: &str, val: &str) {
storage_set(key, val);
}
pub fn download(name: &str, data: &[u8]) {
let _ = std::fs::write(name, data);
}
pub fn open_file() {}
pub fn take_file() -> Option<Vec<u8>> {
None
}
// no savegame service in native builds: every request fails like an unreachable server
pub fn http(_method: &str, _url: &str, _body: &[u8]) -> u32 {
0
}
pub fn http_poll(_id: u32) -> Option<super::HttpResult> {
Some(super::HttpResult { status: 0, body: Vec::new() })
}
}
pub use imp::*;
// Lets the JS plugin loader know this crate uses the "dungeoncraft" plugin
#[no_mangle]
pub extern "C" fn dungeoncraft_crate_version() -> u32 {
1
}