Browse Source

add file-manager for loading user .mpq files, and resetting of diablo.ini settings

pull/8312/head
HoofedEar 4 months ago
parent
commit
8a1a6000f1
  1. 1
      CMake/platforms/emscripten.cmake
  2. 232
      Packaging/emscripten/file-manager.js
  3. 147
      Packaging/emscripten/index.html

1
CMake/platforms/emscripten.cmake

@ -12,3 +12,4 @@ set(NOEXIT ON)
set(DEVILUTIONX_SYSTEM_BZIP2 OFF)
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/file-manager.js" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")

232
Packaging/emscripten/file-manager.js

@ -0,0 +1,232 @@
// File Manager functionality
(function() {
const modal = document.getElementById('fileManagerModal');
const fileManagerBtn = document.getElementById('fileManagerBtn');
const closeModalBtn = document.getElementById('closeModal');
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const resetSettingsBtn = document.getElementById('resetSettingsBtn');
const mpqFilesList = document.getElementById('mpqFilesList');
// Open/close modal
fileManagerBtn.addEventListener('click', () => {
modal.classList.add('show');
refreshFileList();
});
closeModalBtn.addEventListener('click', () => {
modal.classList.remove('show');
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
}
});
// Browse button
browseBtn.addEventListener('click', () => {
fileInput.click();
});
// Drag and drop
dropZone.addEventListener('click', () => {
fileInput.click();
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
// Handle file upload
function handleFiles(files) {
if (!files || files.length === 0) return;
// Wait for Module and FS to be ready
if (typeof Module === 'undefined' || typeof FS === 'undefined') {
alert('Game is still loading. Please wait and try again.');
return;
}
const mpqFiles = Array.from(files).filter(f =>
f.name.toLowerCase().endsWith('.mpq')
);
if (mpqFiles.length === 0) {
alert('Please select MPQ files only.');
return;
}
let processed = 0;
mpqFiles.forEach(file => {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
// Upload to the devilution subdirectory where the game searches
const path = '/libsdl/diasurgical/devilution/' + file.name; // Might want to make this dynamic later, since source mods might rename the paths
// Create directory if it doesn't exist
try {
FS.mkdir('/libsdl/diasurgical/devilution');
} catch (e) {
// Directory might already exist, ignore
}
// Write file to IDBFS-backed directory
FS.writeFile(path, data);
console.log('Uploaded:', file.name, '(' + formatBytes(file.size) + ')');
processed++;
if (processed === mpqFiles.length) {
// Sync to IndexedDB
FS.syncfs(false, function(err) {
if (err) {
console.error('Error syncing files:', err);
alert('Error saving files. Check console.');
} else {
alert('Files uploaded successfully! Reloading game...');
setTimeout(() => location.reload(), 500);
}
});
}
} catch (err) {
console.error('Error writing file:', err);
alert('Error uploading file: ' + file.name);
}
};
reader.readAsArrayBuffer(file);
});
}
// Refresh file list
function refreshFileList() {
if (typeof Module === 'undefined' || typeof FS === 'undefined') {
mpqFilesList.innerHTML = '<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];
}
})();

147
Packaging/emscripten/index.html

@ -33,6 +33,11 @@
background-color: black;
width: 640px;
height: 480px;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
-moz-user-select: none;
-ms-user-select: none;
}
#emscripten_logo {
@ -140,10 +145,148 @@
font-family: 'Lucida Console', Monaco, monospace;
outline: none;
}
/* File Manager Styles */
#fileManagerBtn {
position: fixed;
top: 20px;
right: 20px;
cursor: pointer;
z-index: 1000;
}
#fileManagerModal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
#fileManagerModal.show {
display: flex;
}
.modal-content {
background: white;
border: 1px solid black;
padding: 20px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
margin: 0;
}
.close-btn {
border: 1px solid black;
cursor: pointer;
padding: 5px 10px;
}
.drop-zone {
border: 1px solid black;
padding: 20px;
text-align: center;
margin: 20px 0;
cursor: pointer;
}
.drop-zone p {
margin: 10px 0;
}
.file-list {
margin: 20px 0;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin: 5px 0;
border: 1px solid black;
}
.file-item-name {
flex: 1;
}
.file-item-size {
margin: 0 15px;
}
.btn {
padding: 8px 16px;
border: 1px solid black;
cursor: pointer;
}
.section {
margin: 20px 0;
}
#fileInput {
display: none;
}
</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">&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 get corrupted.
</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>
@ -184,6 +327,9 @@
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvas.addEventListener("webglcontextlost", function (e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
// Prevent canvas from being dragged
canvas.addEventListener("dragstart", function (e) { e.preventDefault(); }, false);
return canvas;
})(),
setStatus: function (text) {
@ -224,6 +370,7 @@
};
};
</script>
<script type='text/javascript' src="file-manager.js"></script>
<script async type="text/javascript" src="devilutionx.js"></script>
</body>

Loading…
Cancel
Save