Skip to content

Emscripten: Add save manager and settings button to the web shell - #3643

Open
johannschopplich wants to merge 6 commits into
EasyRPG:masterfrom
johannschopplich:emscripten-save-manager
Open

Emscripten: Add save manager and settings button to the web shell#3643
johannschopplich wants to merge 6 commits into
EasyRPG:masterfrom
johannschopplich:emscripten-save-manager

Conversation

@johannschopplich

Copy link
Copy Markdown
Contributor

Follow-up to #3556 and based on that branch. Only the last three commits are new. 🐰

  • Save manager – download any slot as a backup, import a .lsd save into the first free slot (native <dialog>, touch-friendly, etc.)
  • Settings button – opens the engine's settings scene by simulating F1.
  • Screenshot scale defaultscreenshot_scale defaults 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.

@github-actions github-actions Bot added Emscripten WebAssembly/JavaScript port for web browsers Settings All about customizable features, storing and applying them labels Jul 30, 2026
@Ghabry Ghabry added the Has PR Dependencies This PR depends on another PR label Jul 30, 2026
@johannschopplich
johannschopplich force-pushed the emscripten-save-manager branch from 940c0ad to c8adf4c Compare July 30, 2026 10:05
@Ghabry

Ghabry commented Sep 2, 2026

Copy link
Copy Markdown
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:

player.api.uploadSavegame(slot);
watchForSaveChange();
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();

@Ghabry Ghabry added this to the 0.8.2 milestone Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Emscripten WebAssembly/JavaScript port for web browsers Has PR Dependencies This PR depends on another PR Settings All about customizable features, storing and applying them

Development

Successfully merging this pull request may close these issues.

2 participants