From b0dd56659d79417cc522874dd3c5dcc2db28bb75 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:57:26 -0800 Subject: [PATCH 01/16] a working baseline for Emscripten builds --- 3rdParty/Lua/CMakeLists.txt | 4 ++++ CMake/emscripten_pre.js | 45 +++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 18 +++++++++++---- Source/diablo.cpp | 4 ++++ Source/engine/dx.cpp | 9 ++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 CMake/emscripten_pre.js diff --git a/3rdParty/Lua/CMakeLists.txt b/3rdParty/Lua/CMakeLists.txt index 04147a54d..a031ffca4 100644 --- a/3rdParty/Lua/CMakeLists.txt +++ b/3rdParty/Lua/CMakeLists.txt @@ -26,6 +26,10 @@ if(CMAKE_SYSTEM_NAME MATCHES "Darwin" AND DARWIN_MAJOR_VERSION VERSION_EQUAL 8) # localtime_r gmtime_r find_package(MacportsLegacySupport REQUIRED) target_link_libraries(lua_static PRIVATE MacportsLegacySupport::MacportsLegacySupport) +elseif(EMSCRIPTEN) + # Enable pthread support for Emscripten to match SDL2's USE_PTHREADS=1 + target_compile_options(lua_static PUBLIC -pthread) + target_link_options(lua_static PUBLIC -pthread) elseif(TARGET_PLATFORM STREQUAL "dos") target_compile_definitions(lua_static PUBLIC -DLUA_USE_C89) elseif(ANDROID AND ("${ANDROID_ABI}" STREQUAL "armeabi-v7a" OR "${ANDROID_ABI}" STREQUAL "x86")) diff --git a/CMake/emscripten_pre.js b/CMake/emscripten_pre.js new file mode 100644 index 000000000..ba9cf6177 --- /dev/null +++ b/CMake/emscripten_pre.js @@ -0,0 +1,45 @@ +// Pre-load MPQ files from the server directory into Emscripten virtual filesystem +Module['preRun'] = Module['preRun'] || []; +Module['preRun'].push(function() { + // List of MPQ files to try loading (in priority order) + var mpqFiles = [ + 'diabdat.mpq', + 'DIABDAT.MPQ', + 'spawn.mpq', + 'hellfire.mpq', + 'hfmonk.mpq', + 'hfmusic.mpq', + 'hfvoice.mpq', + 'hfbard.mpq', + 'hfbarb.mpq' + ]; + + // Create a promise-based loading system + var loadPromises = mpqFiles.map(function(filename) { + return new Promise(function(resolve) { + fetch(filename) + .then(function(response) { + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error('File not found'); + }) + .then(function(data) { + console.log('Loading ' + filename + ' into virtual filesystem...'); + FS.writeFile('/' + filename, new Uint8Array(data)); + console.log('Successfully loaded ' + filename); + resolve(); + }) + .catch(function() { + // File doesn't exist, skip silently + resolve(); + }); + }); + }); + + // Wait for all MPQ files to load before continuing + Module.addRunDependency('loadMPQs'); + Promise.all(loadPromises).then(function() { + Module.removeRunDependency('loadMPQs'); + }); +}); diff --git a/CMakeLists.txt b/CMakeLists.txt index 08767a37e..9d6bc4652 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,7 +271,10 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT PS4) if(APPLE) add_link_options("$<$>:LINKER:-dead_strip>") else() - add_link_options("$<$>:LINKER:--gc-sections,--as-needed>") + add_link_options("$<$>:LINKER:--gc-sections>") + if(NOT EMSCRIPTEN) + add_link_options("$<$>:LINKER:--as-needed>") + endif() endif() endif() @@ -301,7 +304,7 @@ endif() # Not a genexp because CMake doesn't support it # https://gitlab.kitware.com/cmake/cmake/-/issues/20546 -if(NOT DISABLE_LTO) +if(NOT DISABLE_LTO AND NOT EMSCRIPTEN) # LTO if supported: include(CheckIPOSupported) check_ipo_supported(RESULT is_ipo_supported OUTPUT lto_error) @@ -369,7 +372,7 @@ else() Packaging/windows/devilutionx.rc Packaging/apple/LaunchScreen.storyboard) - if(CMAKE_STRIP AND NOT DEVILUTIONX_DISABLE_STRIP) + if(CMAKE_STRIP AND NOT DEVILUTIONX_DISABLE_STRIP AND NOT EMSCRIPTEN) add_custom_command( TARGET ${BIN_TARGET} POST_BUILD COMMAND $<$,$>:${CMAKE_STRIP}> @@ -398,7 +401,14 @@ include(Assets) include(Mods) if(EMSCRIPTEN) - target_link_options(${BIN_TARGET} PRIVATE --preload-file assets) + target_link_options(${BIN_TARGET} PRIVATE + --preload-file assets + -sFORCE_FILESYSTEM=1 + -sALLOW_MEMORY_GROWTH=1 + -sASYNCIFY + ) + # Add JavaScript to load MPQ files from the server directory at runtime + target_link_options(${BIN_TARGET} PRIVATE --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/CMake/emscripten_pre.js) endif() if(NOT USE_SDL1 AND NOT UWP_LIB) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 4f51b7a76..da35c39ed 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -168,7 +168,11 @@ bool gbGameLoopStartup; bool forceSpawn; bool forceDiablo; int sgnTimeoutCurs; +#ifdef __EMSCRIPTEN__ +bool gbShowIntro = false; // Skip intro videos in browser for performance +#else bool gbShowIntro = true; +#endif /** To know if these things have been done when we get to the diablo_deinit() function */ bool was_archives_init = false; /** To know if surfaces have been initialized or not */ diff --git a/Source/engine/dx.cpp b/Source/engine/dx.cpp index 60c792ec8..d7a77acf5 100644 --- a/Source/engine/dx.cpp +++ b/Source/engine/dx.cpp @@ -17,6 +17,10 @@ #include #endif +#ifdef __EMSCRIPTEN__ +#include +#endif + #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "engine/render/primitive_render.hpp" @@ -259,6 +263,11 @@ void RenderPresent() } SDL_RenderPresent(renderer); +#ifdef __EMSCRIPTEN__ + // Yield to browser to allow rendering + emscripten_sleep(1); +#endif + if (*GetOptions().Graphics.frameRateControl != FrameRateControl::VerticalSync) { LimitFrameRate(); } From b8e2dd64c2b1b90d5551a6170a1ea5ddb721b92c Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:15:45 -0800 Subject: [PATCH 02/16] better defaults for now --- CMakeLists.txt | 3 +++ Packaging/emscripten/index.html | 2 ++ Source/options.cpp | 13 ++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d6bc4652..91e4d719e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,9 +406,12 @@ if(EMSCRIPTEN) -sFORCE_FILESYSTEM=1 -sALLOW_MEMORY_GROWTH=1 -sASYNCIFY + --shell-file ${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html ) # Add JavaScript to load MPQ files from the server directory at runtime target_link_options(${BIN_TARGET} PRIVATE --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/CMake/emscripten_pre.js) + # Disable fullscreen by default + target_compile_definitions(${BIN_TARGET} PRIVATE EMSCRIPTEN_NO_FULLSCREEN) endif() if(NOT USE_SDL1 AND NOT UWP_LIB) diff --git a/Packaging/emscripten/index.html b/Packaging/emscripten/index.html index e04a2fc67..397cbe77b 100644 --- a/Packaging/emscripten/index.html +++ b/Packaging/emscripten/index.html @@ -31,6 +31,8 @@ canvas.emscripten { border: 0px none; background-color: black; + width: 640px; + height: 480px; } #emscripten_logo { diff --git a/Source/options.cpp b/Source/options.cpp index 4fe9c88db..c21dff450 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -185,8 +185,9 @@ void SaveIni() #if SDL_VERSION_ATLEAST(2, 0, 0) bool HardwareCursorDefault() { -#if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) +#if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) || defined(__EMSCRIPTEN__) // See https://github.com/diasurgical/devilutionX/issues/2502 + // Emscripten: Software cursor works better in browsers return false; #else return HardwareCursorSupported(); @@ -737,10 +738,16 @@ SDL_AudioDeviceID OptionEntryAudioDevice::id() const GraphicsOptions::GraphicsOptions() : OptionCategoryBase("Graphics", N_("Graphics"), N_("Graphics Settings")) - , fullscreen("Fullscreen", OnlyIfSupportsWindowed | OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fullscreen"), N_("Display the game in windowed or fullscreen mode."), true) + , fullscreen("Fullscreen", OnlyIfSupportsWindowed | OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fullscreen"), N_("Display the game in windowed or fullscreen mode."), +#ifdef __EMSCRIPTEN__ + false // Default to windowed mode for browser +#else + true +#endif + ) #if !defined(USE_SDL1) || defined(__3DS__) , fitToScreen("Fit to Screen", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fit to Screen"), N_("Automatically adjust the game window to your current desktop screen aspect ratio and resolution."), -#ifdef __DJGPP__ +#if defined(__DJGPP__) || defined(__EMSCRIPTEN__) false #else true From b113e56960e72a34a99ee33a91f7602621eeaa87 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:26:01 -0800 Subject: [PATCH 03/16] prevent the game from crashing the tab when clicking away/switching windows hehe --- Source/engine/dx.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/engine/dx.cpp b/Source/engine/dx.cpp index d7a77acf5..3488f709c 100644 --- a/Source/engine/dx.cpp +++ b/Source/engine/dx.cpp @@ -240,7 +240,12 @@ void RenderPresent() SDL_Surface *surface = GetOutputSurface(); if (!gbActive) { +#ifdef __EMSCRIPTEN__ + // Just yield to browser when inactive instead of blocking + emscripten_sleep(1); +#else LimitFrameRate(); +#endif return; } From 24ee55a01f4fa3b7b967b5d8722d0179d3190c4f Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:36:16 -0800 Subject: [PATCH 04/16] add noexit to emscripten --- CMake/platforms/emscripten.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMake/platforms/emscripten.cmake b/CMake/platforms/emscripten.cmake index c5737c081..104773270 100644 --- a/CMake/platforms/emscripten.cmake +++ b/CMake/platforms/emscripten.cmake @@ -4,7 +4,7 @@ set(DISABLE_ZERO_TIER ON) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) - +set(NOEXIT ON) # Emscripten ports do have a bzip2 but it fails to link with this error: # warning: _BZ2_bzDecompress may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library # error: undefined symbol: BZ2_bzDecompressEnd (referenced by top-level compiled C/C++ code) From ef5cff72c3a0013ab43213e3977f765bc3ed7894 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:01:02 -0800 Subject: [PATCH 05/16] disable TCP for emscripten, it no work --- CMake/platforms/emscripten.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/CMake/platforms/emscripten.cmake b/CMake/platforms/emscripten.cmake index 104773270..e43f9f162 100644 --- a/CMake/platforms/emscripten.cmake +++ b/CMake/platforms/emscripten.cmake @@ -1,6 +1,7 @@ set(BUILD_TESTING OFF) set(BUILD_ASSETS_MPQ OFF) set(DISABLE_ZERO_TIER ON) +set(DISABLE_TCP ON) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) From cb3d28e761b5c442e138b7ee85015f5f0e9a0937 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Thu, 4 Dec 2025 05:47:14 -0800 Subject: [PATCH 06/16] move emscripten_pre.js to packaging --- CMakeLists.txt | 2 +- {CMake => Packaging/emscripten}/emscripten_pre.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {CMake => Packaging/emscripten}/emscripten_pre.js (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 91e4d719e..7ffe3ee8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -409,7 +409,7 @@ if(EMSCRIPTEN) --shell-file ${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html ) # Add JavaScript to load MPQ files from the server directory at runtime - target_link_options(${BIN_TARGET} PRIVATE --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/CMake/emscripten_pre.js) + target_link_options(${BIN_TARGET} PRIVATE --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/emscripten_pre.js) # Disable fullscreen by default target_compile_definitions(${BIN_TARGET} PRIVATE EMSCRIPTEN_NO_FULLSCREEN) endif() diff --git a/CMake/emscripten_pre.js b/Packaging/emscripten/emscripten_pre.js similarity index 100% rename from CMake/emscripten_pre.js rename to Packaging/emscripten/emscripten_pre.js From 961490c051757bee8487bffd781cf1279724bb76 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:20:28 -0800 Subject: [PATCH 07/16] saves and settings persist to the browser --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ffe3ee8c..18de6e4f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,6 +406,7 @@ if(EMSCRIPTEN) -sFORCE_FILESYSTEM=1 -sALLOW_MEMORY_GROWTH=1 -sASYNCIFY + -lidbfs.js --shell-file ${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html ) # Add JavaScript to load MPQ files from the server directory at runtime From 1ec529033ba82c6c2d93e8bfeb434f3a2d3e6c67 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:21:04 -0800 Subject: [PATCH 08/16] somehow these didn't get commit - part of save persisting --- Packaging/emscripten/emscripten_pre.js | 101 +++++++++++++++++++++++++ Source/loadsave.cpp | 9 +++ Source/pfile.cpp | 9 +++ 3 files changed, 119 insertions(+) diff --git a/Packaging/emscripten/emscripten_pre.js b/Packaging/emscripten/emscripten_pre.js index ba9cf6177..c7684ddff 100644 --- a/Packaging/emscripten/emscripten_pre.js +++ b/Packaging/emscripten/emscripten_pre.js @@ -1,5 +1,63 @@ // Pre-load MPQ files from the server directory into Emscripten virtual filesystem Module['preRun'] = Module['preRun'] || []; + +// Mount IDBFS for persistent save files +Module['preRun'].push(function() { + console.log('Setting up IDBFS for persistent saves...'); + + // SDL uses //libsdl/ as the base path for Emscripten + // Save files are in //libsdl/diasurgical/devilution/ + // Config files (diablo.ini) would be in //libsdl/diasurgical/ + try { + // Helper function to create directory if it doesn't exist + function mkdirSafe(path) { + try { + // Check if path exists + var stat = FS.stat(path); + // If it exists and is a directory, we're good + if (FS.isDir(stat.mode)) { + return; + } + // If it exists but is not a directory, this is an error + console.error('Path exists but is not a directory: ' + path); + return; + } catch (e) { + // Path doesn't exist, try to create it + try { + FS.mkdir(path); + } catch (mkdirErr) { + // Only throw if it's not an "already exists" error + if (mkdirErr.errno !== 20 && mkdirErr.errno !== 17) { + throw mkdirErr; + } + } + } + } + + // Create SDL directory hierarchy if needed + mkdirSafe('/libsdl'); + mkdirSafe('/libsdl/diasurgical'); + + // Mount the diasurgical directory as IDBFS to persist saves AND settings + FS.mount(IDBFS, {}, '/libsdl/diasurgical'); + console.log('IDBFS mounted successfully at /libsdl/diasurgical'); + + // Sync from IndexedDB to memory (load existing saves) + Module.addRunDependency('syncfs'); + FS.syncfs(true, function(err) { + if (err) { + console.error('Error loading saves from IndexedDB:', err); + } else { + console.log('Existing saves loaded from IndexedDB'); + } + Module.removeRunDependency('syncfs'); + }); + } catch (e) { + console.error('Error setting up IDBFS:', e); + } +}); + +// Load MPQ files from the server directory Module['preRun'].push(function() { // List of MPQ files to try loading (in priority order) var mpqFiles = [ @@ -43,3 +101,46 @@ Module['preRun'].push(function() { Module.removeRunDependency('loadMPQs'); }); }); + +// Track if a sync is in progress to prevent overlapping operations +var syncInProgress = false; + +// Expose function to manually save to IndexedDB +Module['saveToIndexedDB'] = function() { + if (syncInProgress) { + return; + } + + syncInProgress = true; + FS.syncfs(false, function(err) { + syncInProgress = false; + if (err) { + console.error('Error persisting saves to IndexedDB:', err); + } + }); +}; + +// Auto-sync to IndexedDB every 30 seconds as a fallback +Module['postRun'] = Module['postRun'] || []; +Module['postRun'].push(function() { + setInterval(function() { + if (!syncInProgress) { + syncInProgress = true; + FS.syncfs(false, function(err) { + syncInProgress = false; + if (err) { + console.error('Auto-sync error:', err); + } + }); + } + }, 30000); + + // Sync when the page is about to close + window.addEventListener('beforeunload', function() { + if (!syncInProgress) { + FS.syncfs(false, function(err) { + if (err) console.error('Error syncing on page unload:', err); + }); + } + }); +}); diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 75959092e..3cbad5bf0 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -44,6 +44,10 @@ #include "utils/language.h" #include "utils/status_macros.hpp" +#ifdef __EMSCRIPTEN__ +#include +#endif + namespace devilution { bool gbIsHellfireSaveGame; @@ -2931,6 +2935,11 @@ void SaveGame() gbValidSaveFile = true; pfile_write_hero(/*writeGameData=*/true); sfile_write_stash(); + +#ifdef __EMSCRIPTEN__ + // Persist saves to IndexedDB for browser storage + emscripten_run_script("if (typeof Module !== 'undefined' && Module.saveToIndexedDB) Module.saveToIndexedDB();"); +#endif } void SaveLevel(SaveWriter &saveWriter) diff --git a/Source/pfile.cpp b/Source/pfile.cpp index 16325e742..f76944ffb 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -47,6 +47,10 @@ #include "mpq/mpq_reader.hpp" #endif +#ifdef __EMSCRIPTEN__ +#include +#endif + namespace devilution { #define PASSWORD_SPAWN_SINGLE "adslhfb1" @@ -627,6 +631,11 @@ void pfile_write_hero(bool writeGameData) { SaveWriter saveWriter = GetSaveWriter(gSaveNumber); pfile_write_hero(saveWriter, writeGameData); + +#ifdef __EMSCRIPTEN__ + // Persist saves to IndexedDB for browser storage + emscripten_run_script("if (typeof Module !== 'undefined' && Module.saveToIndexedDB) Module.saveToIndexedDB();"); +#endif } #ifndef DISABLE_DEMOMODE From 8a1a6000f1c0c0c3ba8a40d88a89b2fcfcc3b3e1 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:58:10 -0800 Subject: [PATCH 09/16] add file-manager for loading user .mpq files, and resetting of diablo.ini settings --- CMake/platforms/emscripten.cmake | 1 + Packaging/emscripten/file-manager.js | 232 +++++++++++++++++++++++++++ Packaging/emscripten/index.html | 147 +++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 Packaging/emscripten/file-manager.js diff --git a/CMake/platforms/emscripten.cmake b/CMake/platforms/emscripten.cmake index e43f9f162..0e2c1378b 100644 --- a/CMake/platforms/emscripten.cmake +++ b/CMake/platforms/emscripten.cmake @@ -12,3 +12,4 @@ set(NOEXIT ON) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") +file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/file-manager.js" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/Packaging/emscripten/file-manager.js b/Packaging/emscripten/file-manager.js new file mode 100644 index 000000000..c8ab78d20 --- /dev/null +++ b/Packaging/emscripten/file-manager.js @@ -0,0 +1,232 @@ +// File Manager functionality +(function() { + const modal = document.getElementById('fileManagerModal'); + const fileManagerBtn = document.getElementById('fileManagerBtn'); + const closeModalBtn = document.getElementById('closeModal'); + const dropZone = document.getElementById('dropZone'); + const fileInput = document.getElementById('fileInput'); + const browseBtn = document.getElementById('browseBtn'); + const resetSettingsBtn = document.getElementById('resetSettingsBtn'); + const mpqFilesList = document.getElementById('mpqFilesList'); + + // Open/close modal + fileManagerBtn.addEventListener('click', () => { + modal.classList.add('show'); + refreshFileList(); + }); + + closeModalBtn.addEventListener('click', () => { + modal.classList.remove('show'); + }); + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.classList.remove('show'); + } + }); + + // Browse button + browseBtn.addEventListener('click', () => { + fileInput.click(); + }); + + // Drag and drop + dropZone.addEventListener('click', () => { + fileInput.click(); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('dragover'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('dragover'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('dragover'); + handleFiles(e.dataTransfer.files); + }); + + fileInput.addEventListener('change', (e) => { + handleFiles(e.target.files); + }); + + // Handle file upload + function handleFiles(files) { + if (!files || files.length === 0) return; + + // Wait for Module and FS to be ready + if (typeof Module === 'undefined' || typeof FS === 'undefined') { + alert('Game is still loading. Please wait and try again.'); + return; + } + + const mpqFiles = Array.from(files).filter(f => + f.name.toLowerCase().endsWith('.mpq') + ); + + if (mpqFiles.length === 0) { + alert('Please select MPQ files only.'); + return; + } + + let processed = 0; + mpqFiles.forEach(file => { + const reader = new FileReader(); + reader.onload = function(e) { + try { + const data = new Uint8Array(e.target.result); + // Upload to the devilution subdirectory where the game searches + const path = '/libsdl/diasurgical/devilution/' + file.name; // Might want to make this dynamic later, since source mods might rename the paths + + // Create directory if it doesn't exist + try { + FS.mkdir('/libsdl/diasurgical/devilution'); + } catch (e) { + // Directory might already exist, ignore + } + + // Write file to IDBFS-backed directory + FS.writeFile(path, data); + console.log('Uploaded:', file.name, '(' + formatBytes(file.size) + ')'); + + processed++; + if (processed === mpqFiles.length) { + // Sync to IndexedDB + FS.syncfs(false, function(err) { + if (err) { + console.error('Error syncing files:', err); + alert('Error saving files. Check console.'); + } else { + alert('Files uploaded successfully! Reloading game...'); + setTimeout(() => location.reload(), 500); + } + }); + } + } catch (err) { + console.error('Error writing file:', err); + alert('Error uploading file: ' + file.name); + } + }; + reader.readAsArrayBuffer(file); + }); + } + + // Refresh file list + function refreshFileList() { + if (typeof Module === 'undefined' || typeof FS === 'undefined') { + mpqFilesList.innerHTML = '

Game is loading...

'; + return; + } + + try { + // Check if devilution directory exists + try { + FS.stat('/libsdl/diasurgical/devilution'); + } catch (e) { + // Directory doesn't exist yet + mpqFilesList.innerHTML = '

No MPQ files found.

'; + return; + } + + const files = FS.readdir('/libsdl/diasurgical/devilution'); + const mpqFiles = files.filter(f => + f.toLowerCase().endsWith('.mpq') && f !== '.' && f !== '..' + ); + + if (mpqFiles.length === 0) { + mpqFilesList.innerHTML = '

No MPQ files found.

'; + return; + } + + mpqFilesList.innerHTML = mpqFiles.map(filename => { + const path = '/libsdl/diasurgical/devilution/' + filename; + const stat = FS.stat(path); + return ` +
+ ${filename} + ${formatBytes(stat.size)} + +
+ `; + }).join(''); + } catch (err) { + console.error('Error reading files:', err); + mpqFilesList.innerHTML = '

Error reading files.

'; + } + } + + // Delete file + window.deleteFile = function(filename) { + if (!confirm('Delete ' + filename + '? This will reload the game.')) { + return; + } + + try { + const path = '/libsdl/diasurgical/devilution/' + filename; + FS.unlink(path); + + // Sync deletion to IndexedDB + FS.syncfs(false, function(err) { + if (err) { + console.error('Error syncing deletion:', err); + alert('Error deleting file. Check console.'); + } else { + alert('File deleted! Reloading game...'); + setTimeout(() => location.reload(), 500); + } + }); + } catch (err) { + console.error('Error deleting file:', err); + alert('Error deleting file: ' + filename); + } + }; + + // Reset settings + resetSettingsBtn.addEventListener('click', () => { + if (!confirm('Reset game settings? This will delete diablo.ini but keep your saves. The game will reload.')) { + return; + } + + try { + const iniPath = '/libsdl/diasurgical/devilution/diablo.ini'; + + // Check if file exists + try { + FS.stat(iniPath); + // File exists, delete it + FS.unlink(iniPath); + console.log('Deleted diablo.ini'); + } catch (e) { + // File doesn't exist, that's fine + console.log('diablo.ini not found (already reset)'); + } + + // Sync to IndexedDB + FS.syncfs(false, function(err) { + if (err) { + console.error('Error syncing settings reset:', err); + alert('Error resetting settings. Check console.'); + } else { + alert('Settings reset! Reloading game...'); + setTimeout(() => location.reload(), 500); + } + }); + } catch (err) { + console.error('Error resetting settings:', err); + alert('Error resetting settings.'); + } + }); + + // Helper function + function formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + } +})(); diff --git a/Packaging/emscripten/index.html b/Packaging/emscripten/index.html index 397cbe77b..30e117ce2 100644 --- a/Packaging/emscripten/index.html +++ b/Packaging/emscripten/index.html @@ -33,6 +33,11 @@ background-color: black; width: 640px; height: 480px; + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; + -moz-user-select: none; + -ms-user-select: none; } #emscripten_logo { @@ -140,10 +145,148 @@ font-family: 'Lucida Console', Monaco, monospace; outline: none; } + + /* File Manager Styles */ + #fileManagerBtn { + position: fixed; + top: 20px; + right: 20px; + cursor: pointer; + z-index: 1000; + } + + #fileManagerModal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 2000; + justify-content: center; + align-items: center; + } + + #fileManagerModal.show { + display: flex; + } + + .modal-content { + background: white; + border: 1px solid black; + padding: 20px; + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .modal-header h2 { + margin: 0; + } + + .close-btn { + border: 1px solid black; + cursor: pointer; + padding: 5px 10px; + } + + .drop-zone { + border: 1px solid black; + padding: 20px; + text-align: center; + margin: 20px 0; + cursor: pointer; + } + + .drop-zone p { + margin: 10px 0; + } + + .file-list { + margin: 20px 0; + } + + .file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + margin: 5px 0; + border: 1px solid black; + } + + .file-item-name { + flex: 1; + } + + .file-item-size { + margin: 0 15px; + } + + .btn { + padding: 8px 16px; + border: 1px solid black; + cursor: pointer; + } + + .section { + margin: 20px 0; + } + + #fileInput { + display: none; + } + +
+ +
+
Downloading...
@@ -184,6 +327,9 @@ // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 canvas.addEventListener("webglcontextlost", function (e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); + // Prevent canvas from being dragged + canvas.addEventListener("dragstart", function (e) { e.preventDefault(); }, false); + return canvas; })(), setStatus: function (text) { @@ -224,6 +370,7 @@ }; }; + From 763eefb7a5c0dea9a04c13c35d329f37e442ee77 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:00:49 -0800 Subject: [PATCH 10/16] adjust wording settings reset --- Packaging/emscripten/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packaging/emscripten/index.html b/Packaging/emscripten/index.html index 30e117ce2..0b6695414 100644 --- a/Packaging/emscripten/index.html +++ b/Packaging/emscripten/index.html @@ -280,7 +280,7 @@

Settings

- Reset game settings (diablo.ini) without deleting saves. Useful if settings get corrupted. + Reset game settings (diablo.ini) without deleting saves. Useful if settings are accidentally messed up or not working correctly.
From 8323f38c39affa2113e053121db64bd4c87dc2d8 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:48:35 -0800 Subject: [PATCH 11/16] revert disabling intro videos by default, works fine now --- Source/diablo.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index da35c39ed..7d8c4c1b5 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -168,11 +168,8 @@ bool gbGameLoopStartup; bool forceSpawn; bool forceDiablo; int sgnTimeoutCurs; -#ifdef __EMSCRIPTEN__ -bool gbShowIntro = false; // Skip intro videos in browser for performance -#else bool gbShowIntro = true; -#endif + /** To know if these things have been done when we get to the diablo_deinit() function */ bool was_archives_init = false; /** To know if surfaces have been initialized or not */ From 555200a404d03a8bef27c58f447ff5d919a9c40e Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Mon, 8 Dec 2025 05:30:13 -0800 Subject: [PATCH 12/16] only preload spawn, otherwise things break lol --- Packaging/emscripten/emscripten_pre.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Packaging/emscripten/emscripten_pre.js b/Packaging/emscripten/emscripten_pre.js index c7684ddff..d5359e71f 100644 --- a/Packaging/emscripten/emscripten_pre.js +++ b/Packaging/emscripten/emscripten_pre.js @@ -61,15 +61,7 @@ Module['preRun'].push(function() { Module['preRun'].push(function() { // List of MPQ files to try loading (in priority order) var mpqFiles = [ - 'diabdat.mpq', - 'DIABDAT.MPQ', 'spawn.mpq', - 'hellfire.mpq', - 'hfmonk.mpq', - 'hfmusic.mpq', - 'hfvoice.mpq', - 'hfbard.mpq', - 'hfbarb.mpq' ]; // Create a promise-based loading system From 6e2ba5cc00f1289b434d746cce65006878134419 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Mon, 8 Dec 2025 05:30:33 -0800 Subject: [PATCH 13/16] leverage exisiting single-thread stuff to improve web performance --- Source/interfac.cpp | 2 +- Source/utils/sdl_mutex.h | 2 +- Source/utils/sdl_thread.cpp | 2 +- Source/utils/sdl_thread.h | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 2a8efb408..40d9a8a93 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -48,7 +48,7 @@ #include "controls/touch/renderers.h" #endif -#ifdef __DJGPP__ +#if defined(__DJGPP__) || defined(__EMSCRIPTEN__) #define LOAD_ON_MAIN_THREAD #endif diff --git a/Source/utils/sdl_mutex.h b/Source/utils/sdl_mutex.h index 4bf4fca81..cc7f09718 100644 --- a/Source/utils/sdl_mutex.h +++ b/Source/utils/sdl_mutex.h @@ -18,7 +18,7 @@ namespace devilution { * RAII wrapper for SDL_mutex. Satisfies std's "Lockable" (SDL 2) or "BasicLockable" (SDL 1) * requirements so it can be used with std::lock_guard and friends. */ -#ifdef __DJGPP__ +#if defined(__DJGPP__) || defined(__EMSCRIPTEN__) class SdlMutex final { public: SdlMutex() noexcept { } diff --git a/Source/utils/sdl_thread.cpp b/Source/utils/sdl_thread.cpp index 0279f23dc..19fb85d13 100644 --- a/Source/utils/sdl_thread.cpp +++ b/Source/utils/sdl_thread.cpp @@ -2,7 +2,7 @@ namespace devilution { -#ifndef __DJGPP__ +#if !defined(__DJGPP__) && !defined(__EMSCRIPTEN__) int SDLCALL SdlThread::ThreadTranslate(void *ptr) { auto handler = (void (*)())ptr; diff --git a/Source/utils/sdl_thread.h b/Source/utils/sdl_thread.h index f9e0f8a21..517a5e5f9 100644 --- a/Source/utils/sdl_thread.h +++ b/Source/utils/sdl_thread.h @@ -24,7 +24,7 @@ inline SDL_ThreadID get_id() inline SDL_threadID get_id() #endif { -#if defined(__DJGPP__) +#if defined(__DJGPP__) || defined(__EMSCRIPTEN__) return 1; #else return SDL_GetThreadID(nullptr); @@ -32,7 +32,7 @@ inline SDL_threadID get_id() } } // namespace this_sdl_thread -#if defined(__DJGPP__) +#if defined(__DJGPP__) || defined(__EMSCRIPTEN__) class SdlThread final { public: SdlThread(int(SDLCALL *handler)(void *), void *data) From 777150ea957f621b95b9d942db8e20246b303c63 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:51:43 -0800 Subject: [PATCH 14/16] improve default canvas sizing to be a bit more dynamic --- Packaging/emscripten/index.html | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Packaging/emscripten/index.html b/Packaging/emscripten/index.html index 0b6695414..9566d6c4d 100644 --- a/Packaging/emscripten/index.html +++ b/Packaging/emscripten/index.html @@ -25,19 +25,32 @@ div.emscripten_border { border: 1px solid black; + margin: 20px auto; + width: min(90vw, calc((100vh - 200px) * 4 / 3)); + max-width: 1280px; + aspect-ratio: 4 / 3; + display: flex; + justify-content: center; + align-items: center; } /* the canvas *must not* have any border or padding, or mouse coords will be wrong */ canvas.emscripten { border: 0px none; background-color: black; - width: 640px; - height: 480px; - user-select: none; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + object-fit: contain; + image-rendering: pixelated; + image-rendering: crisp-edges; -webkit-user-select: none; - -webkit-user-drag: none; -moz-user-select: none; -ms-user-select: none; + user-select: none; + -webkit-user-drag: none; + pointer-events: auto; } #emscripten_logo { @@ -245,6 +258,12 @@ #fileInput { display: none; } + + @media screen and (max-width: 700px) { + div.emscripten_border { + width: min(calc(100vw - 40px), calc((100vh - 200px) * 4 / 3)); + } + } From 7880085698a100c92f52985f9cda1ba9ceb20f50 Mon Sep 17 00:00:00 2001 From: HoofedEar <1261392+HoofedEar@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:02:43 -0800 Subject: [PATCH 15/16] doing a clang-format for great good --- Source/options.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/options.cpp b/Source/options.cpp index c21dff450..6ee3a31b7 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -740,11 +740,11 @@ GraphicsOptions::GraphicsOptions() : OptionCategoryBase("Graphics", N_("Graphics"), N_("Graphics Settings")) , fullscreen("Fullscreen", OnlyIfSupportsWindowed | OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fullscreen"), N_("Display the game in windowed or fullscreen mode."), #ifdef __EMSCRIPTEN__ - false // Default to windowed mode for browser + false // Default to windowed mode for browser #else - true + true #endif - ) + ) #if !defined(USE_SDL1) || defined(__3DS__) , fitToScreen("Fit to Screen", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fit to Screen"), N_("Automatically adjust the game window to your current desktop screen aspect ratio and resolution."), #if defined(__DJGPP__) || defined(__EMSCRIPTEN__) From e8ab73a31fa2725cdf9e4238ec999ef86930e1de Mon Sep 17 00:00:00 2001 From: "Chris V." Date: Sat, 31 Jan 2026 08:41:26 -0800 Subject: [PATCH 16/16] Update Source/diablo.cpp Co-authored-by: Anders Jenbo --- Source/diablo.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 58befb021..0339a9043 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -169,7 +169,6 @@ bool forceSpawn; bool forceDiablo; int sgnTimeoutCurs; bool gbShowIntro = true; - /** To know if these things have been done when we get to the diablo_deinit() function */ bool was_archives_init = false; /** To know if surfaces have been initialized or not */