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
|
# 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"))
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
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">×</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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 { }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user