14 changed files with 631 additions and 34 deletions
@ -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}") |
||||
|
||||
@ -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,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 = '<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.map(filename => { |
||||
const path = '/libsdl/diasurgical/devilution/' + filename; |
||||
const stat = FS.stat(path); |
||||
return ` |
||||
<div class="file-item"> |
||||
<span class="file-item-name">${filename}</span> |
||||
<span class="file-item-size">${formatBytes(stat.size)}</span> |
||||
<button class="btn btn-delete" onclick="deleteFile('${filename}')">Delete</button> |
||||
</div> |
||||
`;
|
||||
}).join(''); |
||||
} 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]; |
||||
} |
||||
})(); |
||||
@ -1,22 +1,22 @@
|
||||
#include "utils/sdl_thread.h" |
||||
|
||||
namespace devilution { |
||||
|
||||
#ifndef __DJGPP__ |
||||
int SDLCALL SdlThread::ThreadTranslate(void *ptr) |
||||
{ |
||||
auto handler = (void (*)())ptr; |
||||
|
||||
handler(); |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
void SdlThread::ThreadDeleter(SDL_Thread *thread) |
||||
{ |
||||
if (thread != nullptr) |
||||
app_fatal("Joinable thread destroyed"); |
||||
} |
||||
#endif |
||||
|
||||
} // namespace devilution
|
||||
#include "utils/sdl_thread.h" |
||||
|
||||
namespace devilution { |
||||
|
||||
#ifndef __DJGPP__ |
||||
int SDLCALL SdlThread::ThreadTranslate(void *ptr) |
||||
{ |
||||
auto handler = (void (*)())ptr; |
||||
|
||||
handler(); |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
void SdlThread::ThreadDeleter(SDL_Thread *thread) |
||||
{ |
||||
if (thread != nullptr) |
||||
app_fatal("Joinable thread destroyed"); |
||||
} |
||||
#endif |
||||
|
||||
} // namespace devilution
|
||||
|
||||
Loading…
Reference in new issue