Emscripten: Add save manager and settings button to the web shell - #3643
Open
johannschopplich wants to merge 6 commits into
Open
Emscripten: Add save manager and settings button to the web shell#3643johannschopplich wants to merge 6 commits into
johannschopplich wants to merge 6 commits into
Conversation
johannschopplich
force-pushed
the
emscripten-save-manager
branch
from
July 30, 2026 10:05
940c0ad to
c8adf4c
Compare
Member
|
Oh this already looks pretty clean already. The stuff I was unhappy with (this API hooking) is completely gone. Would it be more user friendly when the save manager always shows 15 slots? Asking because there is currently no way to upload e.g. in Slot 5 when you have saves e.g. in Slot 1 and Slot 7. Not having a strong opinion about this I just noticed this while testing the uploading. And I'm contributing a patch to get rid of this polling by exposing a Promise: diff --git a/resources/emscripten/emscripten-post.js b/resources/emscripten/emscripten-post.js
index 9d3e8f8c1..5589fa68e 100644
--- a/resources/emscripten/emscripten-post.js
+++ b/resources/emscripten/emscripten-post.js
@@ -9,60 +9,76 @@ Module.initApi = function() {
}
Module.api_private.createInputElement_js = function (id, event) {
- let file = document.getElementById(id);
- if (file == null) {
- file = document.createElement('input');
+ return new Promise((resolve) => {
+ const file = document.createElement('input');
file.type = 'file';
file.id = id;
file.style.display = 'none';
+
file.addEventListener('change', function (evt) {
const selected_file = evt.target.files[0];
- const reader = new FileReader();
- reader.onload = function(file) {
- event(file, selected_file.name);
+ if (!selected_file) {
+ resolve(false);
+ return;
}
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ const res = event(e, selected_file.name);
+ resolve(res !== undefined ? res : true);
+ };
+ reader.onerror = function() {
+ resolve(false);
+ };
reader.readAsArrayBuffer(selected_file);
- });
- }
- file.click();
+ }, { once: true });
+
+ file.addEventListener('cancel', function () {
+ resolve(false);
+ }, { once: true });
+
+ file.click();
+ });
}
Module.api_private.uploadSavegame_js = function (slot) {
- Module.api_private.createInputElement_js('easyrpg_saveFile', function (file) {
+ return Module.api_private.createInputElement_js('easyrpg_saveFile', function (file) {
const result = new Uint8Array(file.currentTarget.result);
var buf = Module._malloc(result.length);
Module.HEAPU8.set(result, buf);
- Module.api_private.uploadSavegameStep2(slot, buf, result.length);
+ const success = Module.api_private.uploadSavegameStep2(slot, buf, result.length);
Module._free(buf);
- Module.api.refreshScene();
+ if (success) {
+ Module.api.refreshScene();
+ }
+ return success;
});
}
Module.api_private.uploadSoundfont_js = function () {
- Module.api_private.createInputElement_js('easyrpg_sfFile', function (file, name) {
+ return Module.api_private.createInputElement_js('easyrpg_sfFile', function (file, name) {
const result = new Uint8Array(file.currentTarget.result);
- //const name_buf = Module._malloc(name.length + 1);
- //stringToUTF8(name, name_buf, name.length + 1);
const content_buf = Module._malloc(result.length);
Module.HEAPU8.set(result, content_buf);
- Module.api_private.uploadSoundfontStep2(name, content_buf, result.length);
- //Module._free(name_buf);
+ const success = Module.api_private.uploadSoundfontStep2(name, content_buf, result.length);
Module._free(content_buf);
- Module.api.refreshScene();
+ if (success) {
+ Module.api.refreshScene();
+ }
+ return success;
});
}
Module.api_private.uploadFont_js = function () {
- Module.api_private.createInputElement_js('easyrpg_sfFile', function (file, name) {
+ return Module.api_private.createInputElement_js('easyrpg_fontFile', function (file, name) {
const result = new Uint8Array(file.currentTarget.result);
- //const name_buf = Module._malloc(name.length + 1);
- //stringToUTF8(name, name_buf, name.length + 1);
const content_buf = Module._malloc(result.length);
Module.HEAPU8.set(result, content_buf);
- Module.api_private.uploadFontStep2(name, content_buf, result.length);
- //Module._free(name_buf);
+ const success = Module.api_private.uploadFontStep2(name, content_buf, result.length);
Module._free(content_buf);
- Module.api.refreshScene();
+ if (success) {
+ Module.api.refreshScene();
+ }
+ return success;
});
}
}
diff --git a/resources/emscripten/emscripten-shell.html b/resources/emscripten/emscripten-shell.html
index 6e5567898..cde376a0f 100644
--- a/resources/emscripten/emscripten-shell.html
+++ b/resources/emscripten/emscripten-shell.html
@@ -921,7 +921,6 @@
*/
let saveSlotBaseline;
let saveHintPollId = 0;
- let saveWatchId = 0;
const relativeTimeFormatter = new Intl.RelativeTimeFormat("en", {
numeric: "auto",
});
@@ -1109,47 +1108,16 @@
}
/**
- * Hand a slot to the engine's upload picker, then refresh the list.
- * The picker writes straight into the in-memory FS, but its hidden
- * <input> is detached from the document, so no change event reaches us –
- * poll the directory until the slot actually lands instead.
+ * Hand a slot to the engine's upload picker, then refresh the list once
+ * the file has been written.
*
* @param {number} slot 1-based slot to write the chosen file into
*/
- function uploadToSlot(slot) {
- player.api.uploadSavegame(slot);
- watchForSaveChange();
- }
-
- /**
- * Poll until the picked file lands, then refresh the list. Gives up after
- * a minute or once the panel closes – the user dismissed the picker
- * without choosing a file.
- */
- function watchForSaveChange() {
- clearInterval(saveWatchId);
- const slotsBeforeUpload = readSaveSlots();
- let elapsedMs = 0;
- saveWatchId = setInterval(() => {
- elapsedMs += 500;
- if (slotsChanged(slotsBeforeUpload, readSaveSlots())) {
- renderSaveSlots();
- clearInterval(saveWatchId);
- } else if (elapsedMs >= 60000 || !saveDialog.open) {
- clearInterval(saveWatchId);
- }
- }, 500);
- }
-
- /**
- * Whether a later slot read differs – a slot appeared or was overwritten
- */
- function slotsChanged(previousSlots, currentSlots) {
- if (previousSlots.size !== currentSlots.size) return true;
- for (const [slot, mtime] of currentSlots) {
- if (previousSlots.get(slot) !== mtime) return true;
+ async function uploadToSlot(slot) {
+ const success = await player.api.uploadSavegame(slot);
+ if (success) {
+ renderSaveSlots();
}
- return false;
}
/**
diff --git a/src/platform/emscripten/interface.cpp b/src/platform/emscripten/interface.cpp
index e122f6263..7326483e8 100644
--- a/src/platform/emscripten/interface.cpp
+++ b/src/platform/emscripten/interface.cpp
@@ -50,22 +50,16 @@ bool Emscripten_Interface::DownloadSavegame(int slot) {
return true;
}
-void Emscripten_Interface::UploadSavegame(int slot) {
- EM_ASM_INT({
- Module.api_private.uploadSavegame_js($0);
- }, slot);
+emscripten::val Emscripten_Interface::UploadSavegame(int slot) {
+ return emscripten::val::module_property("api_private").call<emscripten::val>("uploadSavegame_js", slot);
}
-void Emscripten_Interface::UploadSoundfont() {
- EM_ASM_INT({
- Module.api_private.uploadSoundfont_js($0);
- });
+emscripten::val Emscripten_Interface::UploadSoundfont() {
+ return emscripten::val::module_property("api_private").call<emscripten::val>("uploadSoundfont_js");
}
-void Emscripten_Interface::UploadFont() {
- EM_ASM_INT({
- Module.api_private.uploadFont_js($0);
- });
+emscripten::val Emscripten_Interface::UploadFont() {
+ return emscripten::val::module_property("api_private").call<emscripten::val>("uploadFont_js");
}
void Emscripten_Interface::RefreshScene() {
diff --git a/src/platform/emscripten/interface.h b/src/platform/emscripten/interface.h
index cdff3b192..2f589468f 100644
--- a/src/platform/emscripten/interface.h
+++ b/src/platform/emscripten/interface.h
@@ -19,14 +19,15 @@
#define EP_EMSCRIPTEN_INTERFACE_H
#include <string>
+#include <emscripten/val.h>
class Emscripten_Interface {
public:
static bool DownloadSavegame(int slot);
- static void UploadSavegame(int slot);
- static void UploadSoundfont();
- static void UploadFont();
- static void RefreshScene();
+ static emscripten::val UploadSavegame(int slot);
+ static emscripten::val UploadSoundfont();
+ static emscripten::val UploadFont();
+ static void RefreshScene();
static void TakeScreenshot(bool is_auto_screenshot = false);
static void Reset();
static bool ResetCanvas(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #3556 and based on that branch. Only the last three commits are new. 🐰
.lsdsave into the first free slot (native<dialog>, touch-friendly, etc.)screenshot_scaledefaults to 4 on Emscripten: downloaded screenshots were native 320×240 PNGs that viewers upscale into a blur… For a "web-first" audience, I think a scale factor is valuable. Happy to adjust the factor.