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>
This commit is contained in:
1 parent
c5b7896609
commit
7f4324abca
17 files changed
+1390
-30
No files matched your search
+55
-1
@@ -1,9 +1,12 @@
|
||||
// DungeonCraft browser plugin for miniquad: storage, file download/upload, save-on-leave, audio output.
|
||||
// DungeonCraft browser plugin for miniquad: storage, file download/upload, save-on-leave,
|
||||
// HTTP requests (savegame service), audio output.
|
||||
(function () {
|
||||
"use strict";
|
||||
var pending = null; // value handed over by dc_storage_get
|
||||
var file = null; // bytes of a file chosen via the file picker
|
||||
var cache = {}; // key -> string, persisted to localStorage when the page is hidden/closed
|
||||
var http = {}; // request id -> { done, status, body }
|
||||
var httpNext = 1;
|
||||
|
||||
function bytes(ptr, len) {
|
||||
return new Uint8Array(wasm_memory.buffer, ptr, len);
|
||||
@@ -34,8 +37,59 @@
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", function () { document.body.appendChild(input); });
|
||||
|
||||
// Asynchronous HTTP request; the result is polled by the game.
|
||||
// status 0 = network error (e.g. no service, or the page was opened as a local file)
|
||||
function httpStart(method, url, body) {
|
||||
var id = httpNext++;
|
||||
var req = { done: false, status: 0, body: new Uint8Array(0) };
|
||||
http[id] = req;
|
||||
var ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
|
||||
var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, 15000);
|
||||
try {
|
||||
var opts = { method: method, cache: "no-store", headers: {} };
|
||||
if (ctrl) opts.signal = ctrl.signal;
|
||||
if (body !== null) { opts.body = body; opts.headers["Content-Type"] = "application/json"; }
|
||||
fetch(url, opts).then(function (r) {
|
||||
req.status = r.status;
|
||||
return r.arrayBuffer();
|
||||
}).then(function (buf) {
|
||||
req.body = new Uint8Array(buf);
|
||||
req.done = true;
|
||||
clearTimeout(timer);
|
||||
}).catch(function () {
|
||||
req.status = 0;
|
||||
req.done = true;
|
||||
clearTimeout(timer);
|
||||
});
|
||||
} catch (e) {
|
||||
req.done = true;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function register(importObject) {
|
||||
var env = importObject.env;
|
||||
env.dc_http = function (mp, ml, up, ul, bp, bl) {
|
||||
var body = bl > 0 ? bytes(bp, bl).slice() : null;
|
||||
return httpStart(str(mp, ml), str(up, ul), body);
|
||||
};
|
||||
// -2 = still running, otherwise the HTTP status (0 = network error)
|
||||
env.dc_http_poll = function (id) {
|
||||
var r = http[id];
|
||||
if (!r) return 0;
|
||||
return r.done ? r.status : -2;
|
||||
};
|
||||
env.dc_http_len = function (id) {
|
||||
var r = http[id];
|
||||
return r ? r.body.length : 0;
|
||||
};
|
||||
// copies the response body and forgets the request
|
||||
env.dc_http_take = function (id, ptr) {
|
||||
var r = http[id];
|
||||
if (!r) return;
|
||||
bytes(ptr, r.body.length).set(r.body);
|
||||
delete http[id];
|
||||
};
|
||||
env.dc_storage_get = function (kp, kl) {
|
||||
var v = null;
|
||||
try { v = window.localStorage.getItem(str(kp, kl)); } catch (e) { }
|
||||
|
||||
Reference in new issue
Block a user