mirror of
https://github.com/diasurgical/DevilutionX.git
synced 2026-05-21 05:40:35 +00:00
Improve Emscripten build (#8312)
This commit is contained in:
Vendored
+4
@@ -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"))
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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)
|
||||
|
||||
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)
|
||||
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}")
|
||||
|
||||
+17
-4
@@ -271,7 +271,11 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT PS4)
|
||||
if(APPLE)
|
||||
add_link_options("$<$<NOT:$<CONFIG:Debug>>:LINKER:-dead_strip>")
|
||||
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()
|
||||
|
||||
@@ -301,7 +305,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 +373,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 $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>>:${CMAKE_STRIP}>
|
||||
@@ -398,7 +402,16 @@ 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
|
||||
-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()
|
||||
|
||||
if(NOT USE_SDL1 AND NOT UWP_LIB)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
})();
|
||||
@@ -25,12 +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: 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 {
|
||||
@@ -138,10 +158,154 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
div.emscripten_border {
|
||||
width: min(calc(100vw - 40px), calc((100vh - 200px) * 4 / 3));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<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">×</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="emscripten" id="status">Downloading...</div>
|
||||
|
||||
@@ -182,6 +346,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) {
|
||||
@@ -222,6 +389,7 @@
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<script type='text/javascript' src="file-manager.js"></script>
|
||||
<script async type="text/javascript" src="devilutionx.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
#include <SDL.h>
|
||||
#endif
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
#include "controls/control_mode.hpp"
|
||||
#include "controls/plrctrls.h"
|
||||
#include "engine/render/primitive_render.hpp"
|
||||
@@ -236,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;
|
||||
}
|
||||
|
||||
@@ -259,6 +268,12 @@ void RenderPresent()
|
||||
}
|
||||
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) {
|
||||
LimitFrameRate();
|
||||
}
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@
|
||||
#include "controls/touch/renderers.h"
|
||||
#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
|
||||
#endif
|
||||
|
||||
|
||||
+10
-3
@@ -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();
|
||||
@@ -739,10 +740,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
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
#include "mpq/mpq_reader.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
namespace devilution {
|
||||
|
||||
#define PASSWORD_SPAWN_SINGLE "adslhfb1"
|
||||
@@ -627,6 +631,11 @@ void pfile_write_hero(bool writeGameData)
|
||||
{
|
||||
SaveWriter saveWriter = GetSaveWriter(gSaveNumber, /*carryForward=*/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
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user