Improve Emscripten build (#8312)

This commit is contained in:
Chris V.
2026-05-20 20:39:30 -07:00
committed by GitHub
parent b1299d5649
commit 04689419a8
12 changed files with 616 additions and 12 deletions
+4
View File
@@ -26,6 +26,10 @@ if(CMAKE_SYSTEM_NAME MATCHES "Darwin" AND DARWIN_MAJOR_VERSION VERSION_EQUAL 8)
# localtime_r gmtime_r # localtime_r gmtime_r
find_package(MacportsLegacySupport REQUIRED) find_package(MacportsLegacySupport REQUIRED)
target_link_libraries(lua_static PRIVATE MacportsLegacySupport::MacportsLegacySupport) 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") elseif(TARGET_PLATFORM STREQUAL "dos")
target_compile_definitions(lua_static PUBLIC -DLUA_USE_C89) target_compile_definitions(lua_static PUBLIC -DLUA_USE_C89)
elseif(ANDROID AND ("${ANDROID_ABI}" STREQUAL "armeabi-v7a" OR "${ANDROID_ABI}" STREQUAL "x86")) elseif(ANDROID AND ("${ANDROID_ABI}" STREQUAL "armeabi-v7a" OR "${ANDROID_ABI}" STREQUAL "x86"))
+3 -1
View File
@@ -1,13 +1,15 @@
set(BUILD_TESTING OFF) set(BUILD_TESTING OFF)
set(BUILD_ASSETS_MPQ OFF) set(BUILD_ASSETS_MPQ OFF)
set(DISABLE_ZERO_TIER ON) set(DISABLE_ZERO_TIER ON)
set(DISABLE_TCP ON)
set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF)
set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF)
set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF)
set(NOEXIT ON)
# Emscripten ports do have a bzip2 but it fails to link with this error: # 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 # 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) # error: undefined symbol: BZ2_bzDecompressEnd (referenced by top-level compiled C/C++ code)
set(DEVILUTIONX_SYSTEM_BZIP2 OFF) 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/index.html" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/file-manager.js" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
+17 -4
View File
@@ -271,7 +271,11 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT PS4)
if(APPLE) if(APPLE)
add_link_options("$<$<NOT:$<CONFIG:Debug>>:LINKER:-dead_strip>") add_link_options("$<$<NOT:$<CONFIG:Debug>>:LINKER:-dead_strip>")
else() else()
add_link_options("$<$<NOT:$<CONFIG:Debug>>:LINKER:--gc-sections,--as-needed>") add_link_options("$<$<NOT:$<CONFIG:Debug>>:LINKER:--gc-sections>")
# Emscripten's linker (wasm-ld) does not support --as-needed.
if(NOT EMSCRIPTEN)
add_link_options("$<$<NOT:$<CONFIG:Debug>>:LINKER:--as-needed>")
endif()
endif() endif()
endif() endif()
@@ -301,7 +305,7 @@ endif()
# Not a genexp because CMake doesn't support it # Not a genexp because CMake doesn't support it
# https://gitlab.kitware.com/cmake/cmake/-/issues/20546 # https://gitlab.kitware.com/cmake/cmake/-/issues/20546
if(NOT DISABLE_LTO) if(NOT DISABLE_LTO AND NOT EMSCRIPTEN)
# LTO if supported: # LTO if supported:
include(CheckIPOSupported) include(CheckIPOSupported)
check_ipo_supported(RESULT is_ipo_supported OUTPUT lto_error) check_ipo_supported(RESULT is_ipo_supported OUTPUT lto_error)
@@ -369,7 +373,7 @@ else()
Packaging/windows/devilutionx.rc Packaging/windows/devilutionx.rc
Packaging/apple/LaunchScreen.storyboard) 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( add_custom_command(
TARGET ${BIN_TARGET} POST_BUILD TARGET ${BIN_TARGET} POST_BUILD
COMMAND $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>>:${CMAKE_STRIP}> COMMAND $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>>:${CMAKE_STRIP}>
@@ -398,7 +402,16 @@ include(Assets)
include(Mods) include(Mods)
if(EMSCRIPTEN) 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
-lidbfs.js
--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}/Packaging/emscripten/emscripten_pre.js)
endif() endif()
if(NOT USE_SDL1 AND NOT UWP_LIB) if(NOT USE_SDL1 AND NOT UWP_LIB)
+138
View File
@@ -0,0 +1,138 @@
// 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 = [
'spawn.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');
});
});
// 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);
});
}
});
});
+247
View File
@@ -0,0 +1,247 @@
// 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 = '<p class="info-text">Game is loading...</p>';
return;
}
try {
// Check if devilution directory exists
try {
FS.stat('/libsdl/diasurgical/devilution');
} catch (e) {
// Directory doesn't exist yet
mpqFilesList.innerHTML = '<p class="info-text">No MPQ files found.</p>';
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 = '<p class="info-text">No MPQ files found.</p>';
return;
}
mpqFilesList.innerHTML = '';
mpqFiles.forEach(filename => {
const path = '/libsdl/diasurgical/devilution/' + filename;
const stat = FS.stat(path);
const item = document.createElement('div');
item.className = 'file-item';
const nameSpan = document.createElement('span');
nameSpan.className = 'file-item-name';
nameSpan.textContent = filename;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'file-item-size';
sizeSpan.textContent = formatBytes(stat.size);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-delete';
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => window.deleteFile(filename));
item.appendChild(nameSpan);
item.appendChild(sizeSpan);
item.appendChild(deleteBtn);
mpqFilesList.appendChild(item);
});
} catch (err) {
console.error('Error reading files:', err);
mpqFilesList.innerHTML = '<p class="info-text">Error reading files.</p>';
}
}
// 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];
}
})();
+168
View File
@@ -25,12 +25,32 @@
div.emscripten_border { div.emscripten_border {
border: 1px solid black; 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 */ /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas.emscripten { canvas.emscripten {
border: 0px none; border: 0px none;
background-color: black; background-color: black;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;
image-rendering: pixelated;
image-rendering: crisp-edges;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-drag: none;
pointer-events: auto;
} }
#emscripten_logo { #emscripten_logo {
@@ -138,10 +158,154 @@
font-family: 'Lucida Console', Monaco, monospace; font-family: 'Lucida Console', Monaco, monospace;
outline: none; 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;
}
@media screen and (max-width: 700px) {
div.emscripten_border {
width: min(calc(100vw - 40px), calc((100vh - 200px) * 4 / 3));
}
}
</style> </style>
</head> </head>
<body> <body>
<button id="fileManagerBtn" title="File Manager">File Manager</button>
<div id="fileManagerModal">
<div class="modal-content">
<div class="modal-header">
<h2>File Manager</h2>
<button class="close-btn" id="closeModal">&times;</button>
</div>
<div class="section">
<h3>Upload MPQ Files</h3>
<div class="info-text">
Upload DIABDAT.MPQ or SPAWN.MPQ or other game files. Files will persist across browser sessions.
</div>
<div class="drop-zone" id="dropZone">
<p>📁 Drag and drop MPQ files here</p>
<p>or</p>
<p><button class="btn" id="browseBtn">Browse Files</button></p>
</div>
<input type="file" id="fileInput" accept=".mpq,.MPQ" multiple>
</div>
<div class="section">
<div class="file-list">
<h3>Loaded MPQ Files</h3>
<div id="mpqFilesList"></div>
</div>
</div>
<div class="section">
<h3>Settings</h3>
<div class="info-text">
Reset game settings (diablo.ini) without deleting saves. Useful if settings are accidentally messed up or not working correctly.
</div>
<button class="btn btn-reset" id="resetSettingsBtn">Reset Game Settings</button>
</div>
</div>
</div>
<div class="spinner" id='spinner'></div> <div class="spinner" id='spinner'></div>
<div class="emscripten" id="status">Downloading...</div> <div class="emscripten" id="status">Downloading...</div>
@@ -182,6 +346,9 @@
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 // 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); 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; return canvas;
})(), })(),
setStatus: function (text) { setStatus: function (text) {
@@ -222,6 +389,7 @@
}; };
}; };
</script> </script>
<script type='text/javascript' src="file-manager.js"></script>
<script async type="text/javascript" src="devilutionx.js"></script> <script async type="text/javascript" src="devilutionx.js"></script>
</body> </body>
+15
View File
@@ -17,6 +17,10 @@
#include <SDL.h> #include <SDL.h>
#endif #endif
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#include "controls/control_mode.hpp" #include "controls/control_mode.hpp"
#include "controls/plrctrls.h" #include "controls/plrctrls.h"
#include "engine/render/primitive_render.hpp" #include "engine/render/primitive_render.hpp"
@@ -236,7 +240,12 @@ void RenderPresent()
SDL_Surface *surface = GetOutputSurface(); SDL_Surface *surface = GetOutputSurface();
if (!gbActive) { if (!gbActive) {
#ifdef __EMSCRIPTEN__
// Just yield to browser when inactive instead of blocking
emscripten_sleep(1);
#else
LimitFrameRate(); LimitFrameRate();
#endif
return; return;
} }
@@ -259,6 +268,12 @@ void RenderPresent()
} }
SDL_RenderPresent(renderer); SDL_RenderPresent(renderer);
#ifdef __EMSCRIPTEN__
// TODO: Refactor to use emscripten_set_main_loop or requestAnimationFrame instead.
// For now, yield to browser to allow rendering via ASYNCIFY sleep.
emscripten_sleep(1);
#endif
if (*GetOptions().Graphics.frameRateControl != FrameRateControl::VerticalSync) { if (*GetOptions().Graphics.frameRateControl != FrameRateControl::VerticalSync) {
LimitFrameRate(); LimitFrameRate();
} }
+2 -1
View File
@@ -48,7 +48,8 @@
#include "controls/touch/renderers.h" #include "controls/touch/renderers.h"
#endif #endif
#ifdef __DJGPP__ // Emscripten: ASYNCIFY does not support unwinding across threads, so loading must happen on the main thread.
#if defined(__DJGPP__) || defined(__EMSCRIPTEN__)
#define LOAD_ON_MAIN_THREAD #define LOAD_ON_MAIN_THREAD
#endif #endif
+10 -3
View File
@@ -185,8 +185,9 @@ void SaveIni()
#if SDL_VERSION_ATLEAST(2, 0, 0) #if SDL_VERSION_ATLEAST(2, 0, 0)
bool HardwareCursorDefault() 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 // See https://github.com/diasurgical/devilutionX/issues/2502
// Emscripten: Software cursor works better in browsers
return false; return false;
#else #else
return HardwareCursorSupported(); return HardwareCursorSupported();
@@ -739,10 +740,16 @@ SDL_AudioDeviceID OptionEntryAudioDevice::id() const
GraphicsOptions::GraphicsOptions() GraphicsOptions::GraphicsOptions()
: OptionCategoryBase("Graphics", N_("Graphics"), N_("Graphics Settings")) : 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__) #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."), , 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 false
#else #else
true true
+9
View File
@@ -47,6 +47,10 @@
#include "mpq/mpq_reader.hpp" #include "mpq/mpq_reader.hpp"
#endif #endif
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace devilution { namespace devilution {
#define PASSWORD_SPAWN_SINGLE "adslhfb1" #define PASSWORD_SPAWN_SINGLE "adslhfb1"
@@ -627,6 +631,11 @@ void pfile_write_hero(bool writeGameData)
{ {
SaveWriter saveWriter = GetSaveWriter(gSaveNumber, /*carryForward=*/writeGameData); SaveWriter saveWriter = GetSaveWriter(gSaveNumber, /*carryForward=*/writeGameData);
pfile_write_hero(saveWriter, writeGameData); 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 #ifndef DISABLE_DEMOMODE
+1 -1
View File
@@ -18,7 +18,7 @@ namespace devilution {
* RAII wrapper for SDL_mutex. Satisfies std's "Lockable" (SDL 2) or "BasicLockable" (SDL 1) * 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. * requirements so it can be used with std::lock_guard and friends.
*/ */
#ifdef __DJGPP__ #if defined(__DJGPP__) || defined(__EMSCRIPTEN__)
class SdlMutex final { class SdlMutex final {
public: public:
SdlMutex() noexcept { } SdlMutex() noexcept { }
+2 -2
View File
@@ -24,7 +24,7 @@ inline SDL_ThreadID get_id()
inline SDL_threadID get_id() inline SDL_threadID get_id()
#endif #endif
{ {
#if defined(__DJGPP__) #if defined(__DJGPP__) || defined(__EMSCRIPTEN__)
return 1; return 1;
#else #else
return SDL_GetThreadID(nullptr); return SDL_GetThreadID(nullptr);
@@ -32,7 +32,7 @@ inline SDL_threadID get_id()
} }
} // namespace this_sdl_thread } // namespace this_sdl_thread
#if defined(__DJGPP__) #if defined(__DJGPP__) || defined(__EMSCRIPTEN__)
class SdlThread final { class SdlThread final {
public: public:
SdlThread(int(SDLCALL *handler)(void *), void *data) SdlThread(int(SDLCALL *handler)(void *), void *data)