PHP Classes

File: templates/jsdosdoor_play.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/jsdosdoor_play.twig   Download  
File: templates/jsdosdoor_play.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 42,786 bytes
 

Contents

Class file image Download
<!DOCTYPE html> <html lang="{{ locale|e }}"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ game.name }} - {{ t('ui.jsdosdoor.page_title_suffix', {}, locale, ['common']) }}</title> <link rel="stylesheet" href="/vendor/bootstrap-5.3.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/vendor/fontawesome-6.4.0/css/all.min.css"> <style> html, body { height: 100%; margin: 0; background: #111; overflow: hidden; } body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #jsdos-prelauncher { min-height: 100vh; display: flex; align-items: center; } #jsdos-stage { background: #000; } #jsdos-canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; } #jsdos-stage canvas, #jsdos-stage video { display: block; max-width: 100%; max-height: 100%; image-rendering: pixelated; } </style> </head> <body> <div id="jsdos-prelauncher" class="container"> <div class="row justify-content-center"> <div class="col-lg-8"> <div class="card shadow"> <div class="card-body text-center p-5"> <img src="/jsdos-doors/{{ game_path }}/{{ game.icon ?? 'icon.png' }}" alt="{{ game.name }}" class="rounded mb-4" style="width: 96px; height: 96px; object-fit: cover;" onerror="this.src='/favicon.svg'"> <h2 class="mb-1"> {{ game.name }} {% if mode_id != 'play' %} <span class="badge bg-warning text-dark ms-2">{{ mode.label }}</span> {% endif %} </h2> {% if game.author %} <p class="text-muted mb-3"> {{ t('ui.webdoors.by_author', {'author': game.author}, locale, ['common']) }} {% if game.version %} &mdash; v{{ game.version }}{% endif %} </p> {% endif %} {% if mode.description ?? game.description %} <p class="lead mb-4">{{ mode.description ?? game.description }}</p> {% endif %} <div id="jsdos-launch-error" class="alert alert-danger d-none mb-3" role="alert"></div> <button id="jsdos-launch-btn" class="btn btn-success btn-lg"> <i class="fas fa-play me-2"></i> {% if mode_id == 'config' %} {{ t('ui.jsdosdoor.launch_config', {}, locale, ['common']) }} {% else %} {{ t('ui.jsdosdoor.launch_game', {}, locale, ['common']) }} {% endif %} </button> </div> </div> </div> </div> </div> <div id="jsdos-player" class="d-none" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; z-index: 9999;"> <div id="jsdos-toolbar" style="position: absolute; top: 0; right: 0; z-index: 10000; padding: 6px 8px; display: flex; gap: 4px;"> <button id="jsdos-fullscreen-btn" class="btn btn-sm btn-secondary" title="{{ t('ui.jsdosdoor.fullscreen', {}, locale, ['common']) }}"> <i class="fas fa-expand"></i> </button> <button id="jsdos-send-esc-btn" class="btn btn-sm btn-secondary" title="{{ t('ui.jsdosdoor.send_escape', {}, locale, ['common']) }}"> <i class="fas fa-keyboard me-1"></i>{{ t('ui.jsdosdoor.send_escape_short', {}, locale, ['common']) }} </button> <button id="jsdos-exit-btn" class="btn btn-sm btn-danger"> <i class="fas fa-times me-1"></i>{{ t('ui.jsdosdoor.exit_game', {}, locale, ['common']) }} </button> </div> <div id="jsdos-status" style="position: absolute; bottom: 0; left: 0; right: 0; text-align: center; color: #aaa; padding: 8px; font-size: 0.85rem; pointer-events: none;"> </div> <div id="jsdos-stage" style="position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden;"> <canvas id="jsdos-canvas" style="width: 100%; height: 100%; max-width: 100%; max-height: 100%; display: block; image-rendering: pixelated;"></canvas> </div> </div> <script src="/js/jsdos/js-dos.js"></script> <script> (function () { 'use strict'; var trackedAudioContexts = []; var audioTrackingInstalled = false; var activeFs = null; var initialWritableFiles = {}; var discoveredWritablePaths = []; var warnedAboutWildcardSync = false; var exitMarkerPollTimer = null; var autosaveTimer = null; var saveInFlight = false; function installAudioContextTracking() { if (audioTrackingInstalled) { return; } audioTrackingInstalled = true; ['AudioContext', 'webkitAudioContext'].forEach(function (name) { var NativeCtor = window[name]; if (typeof NativeCtor !== 'function' || NativeCtor.__jsdosTracked) { return; } function TrackedAudioContext() { var instance = new NativeCtor(); trackedAudioContexts.push(instance); return instance; } TrackedAudioContext.prototype = NativeCtor.prototype; Object.defineProperty(TrackedAudioContext, '__jsdosTracked', { value: true }); window[name] = TrackedAudioContext; }); } function resumeTrackedAudioContexts() { trackedAudioContexts.forEach(function (context) { if (!context || typeof context.resume !== 'function' || context.state === 'running') { return; } context.resume().catch(function () { return null; }); }); } function normalizeDosPath(path) { var normalized = String(path || '').replace(/\\/g, '/').replace(/\/+/g, '/').trim(); var driveMatch = normalized.match(/^([a-z]):/i); if (driveMatch) { normalized = driveMatch[1].toUpperCase() + normalized.slice(1); } else if (normalized.charAt(0) === '/') { normalized = 'C:' + normalized; } return normalized; } function dosPathToFsPath(path) { return normalizeDosPath(path).replace(/^[A-Z]:/, ''); } function escapeRegex(text) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function globToRegex(pattern) { var normalized = normalizeDosPath(pattern); var regex = escapeRegex(normalized) .replace(/\\\*/g, '.*') .replace(/\\\?/g, '.'); return new RegExp('^' + regex + '$', 'i'); } function uniqueArray(values) { return Array.from(new Set(values)); } function textToUint8Array(value) { return new TextEncoder().encode(String(value || '')); } function uint8ArrayToBase64(data) { var bytes = data instanceof Uint8Array ? data : new Uint8Array(data); var chunkSize = 0x8000; var chunks = []; for (var index = 0; index < bytes.length; index += chunkSize) { chunks.push(String.fromCharCode.apply(null, bytes.subarray(index, index + chunkSize))); } return btoa(chunks.join('')); } function base64ToUint8Array(value) { var decoded = atob(value); var bytes = new Uint8Array(decoded.length); for (var index = 0; index < decoded.length; index++) { bytes[index] = decoded.charCodeAt(index); } return bytes; } installAudioContextTracking(); var gameId = {{ game_id | json_encode | raw }}; var gamePath = {{ game_path | json_encode | raw }}; var modeId = {{ mode_id | json_encode | raw }}; var keepOpen = {{ (mode.keep_open ?? false) | json_encode | raw }}; var gameFiles = {{ (mode.emulator_config.game_files ?? []) | json_encode | raw }}; var autoexec = {{ (mode.emulator_config.autoexec ?? []) | json_encode | raw }}; var cpuCycles = {{ (mode.emulator_config.cpu_cycles ?? 'auto') | json_encode | raw }}; var machine = {{ (mode.emulator_config.machine ?? 'svga_s3') | json_encode | raw }}; var memoryMb = {{ (mode.emulator_config.memory_mb ?? 16) | json_encode | raw }}; var emulatorOutput = {{ (mode.emulator_config.output ?? 'surface') | json_encode | raw }}; var renderScaler = {{ (mode.emulator_config.scaler ?? '') | json_encode | raw }}; var autoLock = {{ (mode.emulator_config.autolock ?? true) | json_encode | raw }}; var writablePatterns = {{ (mode.saves.save_paths ?? []) | json_encode | raw }}; var writableSyncEnabled = {{ (mode.saves.enabled ?? false) | json_encode | raw }}; var launchBatchPath = 'C:/BTPLAY.BAT'; var exitMarkerPath = 'C:/BTEXIT.FLG'; var sessionId = null; var dosPlayer = null; var isExiting = false; var loadingAssetsMsg = {{ t('ui.jsdosdoor.loading_assets', {}, locale, ['common']) | json_encode | raw }}; var loadingEmulatorMsg = {{ t('ui.jsdosdoor.loading_emulator', {}, locale, ['common']) | json_encode | raw }}; var unlockHintMsg = {{ t('ui.jsdosdoor.escape_unlock_hint', {}, locale, ['common']) | json_encode | raw }}; var sessionErrorMsg = {{ t('errors.jsdosdoor.session_create_failed', {}, locale, ['errors']) | json_encode | raw }}; var assetErrorMsg = {{ t('errors.jsdosdoor.asset_load_failed', {}, locale, ['errors']) | json_encode | raw }}; var fileSyncErrorMsg = {{ t('errors.jsdosdoor.file_sync_failed', {}, locale, ['errors']) | json_encode | raw }}; var launchBtn = document.getElementById('jsdos-launch-btn'); var errorPanel = document.getElementById('jsdos-launch-error'); var playerDiv = document.getElementById('jsdos-player'); var preLaunchDiv = document.getElementById('jsdos-prelauncher'); var playerStage = document.getElementById('jsdos-stage'); var statusEl = document.getElementById('jsdos-status'); var playerCanvas = document.getElementById('jsdos-canvas'); var sendEscBtn = document.getElementById('jsdos-send-esc-btn'); function setStatus(message) { statusEl.textContent = message; } function showError(message) { errorPanel.textContent = message; errorPanel.classList.remove('d-none'); } function hideError() { errorPanel.classList.add('d-none'); } function sizeHostCanvas() { var stageWidth = playerStage.clientWidth || 0; var stageHeight = playerStage.clientHeight || 0; if (!stageWidth || !stageHeight) { return; } playerCanvas.width = stageWidth; playerCanvas.height = stageHeight; } function focusPlayerCanvas() { if (playerCanvas && typeof playerCanvas.focus === 'function') { playerCanvas.focus({preventScroll: true}); } } function notifyParentExit() { try { if (window.top && window.top !== window && window.top.location.origin === window.location.origin) { window.top.location.href = '/games'; return true; } } catch (error) { // Fall back to postMessage below if direct top navigation is not available. } if (window.parent && window.parent !== window) { window.parent.postMessage({type: 'jsdos-exit'}, window.location.origin); return true; } return false; } function simulateKeyOnElement(target, keyCode) { if (!target || typeof target.dispatchEvent !== 'function') { return; } ['keydown', 'keyup'].forEach(function (eventType) { var event = new KeyboardEvent(eventType, { key: keyCode === 27 ? 'Escape' : '', code: keyCode === 27 ? 'Escape' : '', keyCode: keyCode, which: keyCode, bubbles: true, cancelable: true }); Object.defineProperty(event, 'keyCode', {get: function () { return keyCode; }}); Object.defineProperty(event, 'which', {get: function () { return keyCode; }}); target.dispatchEvent(event); }); } function sendEscapeToGame() { if (dosPlayer && typeof dosPlayer.simulateKeyPress === 'function') { dosPlayer.simulateKeyPress(27); return; } if (dosPlayer && typeof dosPlayer.sendKeyPress === 'function') { dosPlayer.sendKeyPress(27); return; } focusPlayerCanvas(); simulateKeyOnElement(playerCanvas, 27); simulateKeyOnElement(window, 27); setStatus(''); } function getAutoexecLines() { var lines = autoexec.slice(); var lastCommand = ''; if (!keepOpen) { return ['C:', 'BTPLAY.BAT']; } for (var index = lines.length - 1; index >= 0; index--) { var candidate = String(lines[index] || '').trim(); if (candidate !== '') { lastCommand = candidate.toLowerCase(); break; } } if (!keepOpen && lastCommand !== 'exit') { lines.push('exit'); } return lines; } function getLaunchBatchContents() { var lines = ['@echo off']; var commands = autoexec.slice(); while (commands.length > 0) { var tail = String(commands[commands.length - 1] || '').trim().toLowerCase(); if (tail === '') { commands.pop(); continue; } if (tail === 'exit') { commands.pop(); } break; } commands.forEach(function (command) { if (String(command || '').trim() !== '') { lines.push(command); } }); lines.push('echo done > C:\\BTEXIT.FLG'); lines.push('exit'); return lines.join('\r\n') + '\r\n'; } function buildDosboxConf() { var autoexecLines = getAutoexecLines(); var lines = [ '[sdl]', 'autolock=' + (autoLock ? 'true' : 'false'), 'output=' + emulatorOutput, '', '[dosbox]', 'machine=' + machine, 'memsize=' + memoryMb, '', '[render]', 'aspect=true', '', '[cpu]', 'cycles=' + cpuCycles, '', '[dos]', 'xms=true', 'ems=true', '', '[autoexec]', 'mount c .' ]; if (String(renderScaler || '').trim() !== '') { lines.splice(10, 0, 'scaler=' + renderScaler); } for (var index = 0; index < autoexecLines.length; index++) { lines.push(autoexecLines[index]); } return lines.join('\n'); } function createSession() { return fetch('/api/jsdoor/session', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({game_id: gameId, mode: modeId}) }) .then(function (response) { return response.json(); }) .then(function (data) { if (data.success) { return data.session_id; } var message = (window.getApiErrorMessage) ? window.getApiErrorMessage(data, sessionErrorMsg) : (data.error || sessionErrorMsg); throw new Error(message); }); } function fetchSyncedFiles() { if (!sessionId) { return Promise.resolve([]); } return fetch('/api/jsdoor/files/' + encodeURIComponent(gameId) + '?session_id=' + encodeURIComponent(sessionId)) .then(function (response) { return response.json(); }) .then(function (data) { if (data.success && Array.isArray(data.files)) { return data.files; } var message = (window.getApiErrorMessage) ? window.getApiErrorMessage(data, fileSyncErrorMsg) : (data.error || fileSyncErrorMsg); throw new Error(message); }); } function endSession(useBeacon) { if (!sessionId) { return Promise.resolve(); } var sid = sessionId; sessionId = null; var url = '/api/jsdoor/session/' + encodeURIComponent(sid) + '/end'; if (useBeacon && navigator.sendBeacon) { navigator.sendBeacon(url); return Promise.resolve(); } return fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'} }).catch(function () { return null; }); } function resetPlayerCanvas() { clearExitMarkerPoll(); clearAutosaveTimer(); var nextCanvas = document.createElement('canvas'); nextCanvas.id = 'jsdos-canvas'; nextCanvas.tabIndex = 0; nextCanvas.style.width = '100%'; nextCanvas.style.height = '100%'; nextCanvas.style.maxWidth = '100%'; nextCanvas.style.maxHeight = '100%'; nextCanvas.style.display = 'block'; nextCanvas.style.imageRendering = 'pixelated'; if (playerCanvas && playerCanvas.parentNode === playerStage) { playerStage.replaceChild(nextCanvas, playerCanvas); } else { playerStage.innerHTML = ''; playerStage.appendChild(nextCanvas); } playerCanvas = nextCanvas; sizeHostCanvas(); activeFs = null; initialWritableFiles = {}; discoveredWritablePaths = []; } function stopPlayer() { clearExitMarkerPoll(); clearAutosaveTimer(); var player = dosPlayer; dosPlayer = null; if (!player || typeof player.stop !== 'function') { resetPlayerCanvas(); return Promise.resolve(); } return Promise.resolve(player.stop()).catch(function () { return null; }).then(function () { resetPlayerCanvas(); }); } function leavePlayer() { setStatus(''); playerDiv.classList.add('d-none'); preLaunchDiv.classList.remove('d-none'); launchBtn.disabled = false; isExiting = false; } function hasWildcard(pattern) { return /[*?]/.test(String(pattern || '')); } function matchesWritablePattern(dosPath) { return writablePatterns.some(function (pattern) { return globToRegex(pattern).test(normalizeDosPath(dosPath)); }); } function getExplicitWritablePaths() { return uniqueArray(writablePatterns.filter(function (pattern) { return !hasWildcard(pattern); }).map(normalizeDosPath)); } function getReadableFs(fs) { if (fs && typeof fs.readFile === 'function') { return fs; } if (fs && fs.fs && typeof fs.fs.readFile === 'function') { return fs.fs; } return null; } function getDirectoryFs(fs) { if (fs && typeof fs.readdir === 'function') { return fs; } if (fs && fs.fs && typeof fs.fs.readdir === 'function') { return fs.fs; } return null; } function getInternalFs(fs) { if (fs && fs.fs) { return fs.fs; } return fs || null; } function ensureDosParentDirectory(fs, dosPath) { var internalFs = getInternalFs(fs); var fsPath = dosPathToFsPath(dosPath); var lastSlash = fsPath.lastIndexOf('/'); if (!internalFs || lastSlash <= 0) { return; } var parentPath = fsPath.slice(0, lastSlash); if (!parentPath || parentPath === '/') { return; } if (typeof internalFs.mkdirTree === 'function') { try { internalFs.mkdirTree(parentPath); return; } catch (error) { // Fall through to manual creation below. } } var segments = parentPath.split('/').filter(Boolean); var currentPath = ''; segments.forEach(function (segment) { currentPath += '/' + segment; try { if (typeof internalFs.analyzePath === 'function' && internalFs.analyzePath(currentPath).exists) { return; } } catch (error) { // Ignore analyze errors and attempt mkdir directly. } if (typeof internalFs.mkdir === 'function') { try { internalFs.mkdir(currentPath); } catch (error) { // Ignore EEXIST-style failures from races or partial state. } } }); } function writeDosFile(fs, dosPath, contents) { var internalFs = getInternalFs(fs); var fsPath = dosPathToFsPath(dosPath); ensureDosParentDirectory(fs, dosPath); if (internalFs && typeof internalFs.analyzePath === 'function') { try { if (internalFs.analyzePath(fsPath).exists && typeof internalFs.unlink === 'function') { internalFs.unlink(fsPath); } } catch (error) { try { return fs.createFile(dosPath, contents); } catch (createError) { throw new Error('Failed to write DOS file ' + dosPath + ': ' + (createError.message || createError)); } } } try { return fs.createFile(dosPath, contents); } catch (error) { throw new Error('Failed to write DOS file ' + dosPath + ': ' + (error.message || error)); } } function removeDosFile(fs, dosPath) { var internalFs = getInternalFs(fs); var fsPath = dosPathToFsPath(dosPath); if (!internalFs || typeof internalFs.analyzePath !== 'function' || typeof internalFs.unlink !== 'function') { return; } try { if (internalFs.analyzePath(fsPath).exists) { internalFs.unlink(fsPath); } } catch (error) { return; } } function dosFileExists(fs, dosPath) { var internalFs = getInternalFs(fs); if (!internalFs || typeof internalFs.analyzePath !== 'function') { return false; } try { return !!internalFs.analyzePath(dosPathToFsPath(dosPath)).exists; } catch (error) { return false; } } function clearExitMarkerPoll() { if (exitMarkerPollTimer) { window.clearInterval(exitMarkerPollTimer); exitMarkerPollTimer = null; } } function clearAutosaveTimer() { if (autosaveTimer) { window.clearInterval(autosaveTimer); autosaveTimer = null; } } function flushModifiedFiles() { if (saveInFlight) { return Promise.resolve(); } saveInFlight = true; return saveModifiedFiles().finally(function () { saveInFlight = false; }); } function startAutosaveLoop() { clearAutosaveTimer(); if (!writableSyncEnabled || writablePatterns.length === 0) { return; } autosaveTimer = window.setInterval(function () { if (isExiting || !activeFs || !sessionId) { return; } flushModifiedFiles().catch(function (error) { console.error('jsdos autosave error', error); }); }, 2000); } function startExitMarkerPoll(fs) { clearExitMarkerPoll(); if (keepOpen) { return; } exitMarkerPollTimer = window.setInterval(function () { if (isExiting || !activeFs) { return; } if (dosFileExists(fs, exitMarkerPath)) { clearExitMarkerPoll(); exitGame({fromEmulator: true}); } }, 500); } function readWritableFile(fs, dosPath, snapshot) { var readableFs = getReadableFs(fs); if (!readableFs) { return Promise.resolve(); } return Promise.resolve().then(function () { return readableFs.readFile(dosPathToFsPath(dosPath)); }).then(function (data) { snapshot[dosPath] = uint8ArrayToBase64(data); }).catch(function () { return null; }); } function getWritableRoots() { return uniqueArray(writablePatterns.filter(hasWildcard).map(function (pattern) { var normalized = normalizeDosPath(pattern).replace(/^[A-Z]:/, ''); var segments = normalized.split('/').filter(Boolean); var rootSegments = []; for (var index = 0; index < segments.length; index++) { if (/[*?]/.test(segments[index])) { break; } rootSegments.push(segments[index]); } if (rootSegments.length === segments.length && rootSegments.length > 0) { rootSegments.pop(); } return rootSegments.length ? ('/' + rootSegments.join('/')) : '/'; })); } function walkFsDirectory(fs, path, discoveredPaths) { var directoryFs = getDirectoryFs(fs); if (!directoryFs) { return Promise.resolve(); } return Promise.resolve().then(function () { return directoryFs.readdir(path); }).then(function (entries) { if (!Array.isArray(entries)) { return null; } return Promise.all(entries.filter(function (entry) { return entry !== '.' && entry !== '..'; }).map(function (entry) { var childPath = path === '/' ? ('/' + entry) : (path + '/' + entry); return Promise.resolve().then(function () { return directoryFs.readdir(childPath); }).then(function () { return walkFsDirectory(fs, childPath, discoveredPaths); }).catch(function () { var dosPath = normalizeDosPath('C:' + childPath); if (matchesWritablePattern(dosPath)) { discoveredPaths.push(dosPath); } return null; }); })); }).catch(function () { return null; }); } function snapshotWritableFiles(fs) { if (!writableSyncEnabled || writablePatterns.length === 0) { return Promise.resolve({}); } var snapshot = {}; var trackedPaths = uniqueArray(getExplicitWritablePaths().concat(discoveredWritablePaths)); var tasks = trackedPaths.map(function (dosPath) { return readWritableFile(fs, dosPath, snapshot); }); if (getDirectoryFs(fs)) { var wildcardDiscoveredPaths = []; tasks.push(Promise.all(getWritableRoots().map(function (root) { return walkFsDirectory(fs, root, wildcardDiscoveredPaths); })).then(function () { wildcardDiscoveredPaths = uniqueArray(wildcardDiscoveredPaths); discoveredWritablePaths = uniqueArray(discoveredWritablePaths.concat(wildcardDiscoveredPaths)); return Promise.all(wildcardDiscoveredPaths.map(function (dosPath) { return readWritableFile(fs, dosPath, snapshot); })); })); } else if (!warnedAboutWildcardSync && writablePatterns.some(hasWildcard)) { warnedAboutWildcardSync = true; console.warn('jsdos fs.readdir is unavailable; wildcard save discovery is disabled for this build'); } return Promise.all(tasks).then(function () { return snapshot; }); } function snapshotWritableFilesSync(fs) { if (!writableSyncEnabled || writablePatterns.length === 0) { return {}; } var readableFs = getReadableFs(fs); var directoryFs = getDirectoryFs(fs); if (!readableFs || typeof readableFs.readFile !== 'function') { return {}; } var snapshot = {}; var trackedPaths = uniqueArray(getExplicitWritablePaths().concat(discoveredWritablePaths)); function walkSync(path) { if (!directoryFs || typeof directoryFs.readdir !== 'function') { return; } var entries; try { entries = directoryFs.readdir(path); } catch (error) { return; } if (!Array.isArray(entries)) { return; } entries.forEach(function (entry) { if (entry === '.' || entry === '..') { return; } var childPath = path === '/' ? ('/' + entry) : (path + '/' + entry); try { directoryFs.readdir(childPath); walkSync(childPath); } catch (error) { var dosPath = normalizeDosPath('C:' + childPath); if (matchesWritablePattern(dosPath)) { trackedPaths.push(dosPath); } } }); } getWritableRoots().forEach(walkSync); trackedPaths = uniqueArray(trackedPaths); discoveredWritablePaths = uniqueArray(discoveredWritablePaths.concat(trackedPaths.filter(matchesWritablePattern))); trackedPaths.forEach(function (dosPath) { try { var data = readableFs.readFile(dosPathToFsPath(dosPath)); snapshot[dosPath] = uint8ArrayToBase64(data); } catch (error) { return; } }); return snapshot; } function buildChangedFiles(currentSnapshot) { var allPaths = uniqueArray(Object.keys(initialWritableFiles).concat(Object.keys(currentSnapshot))); var changedFiles = []; allPaths.forEach(function (dosPath) { var hadBefore = Object.prototype.hasOwnProperty.call(initialWritableFiles, dosPath); var hasNow = Object.prototype.hasOwnProperty.call(currentSnapshot, dosPath); if (hadBefore && !hasNow) { changedFiles.push({dos_path: dosPath, deleted: true}); return; } if (hasNow && initialWritableFiles[dosPath] !== currentSnapshot[dosPath]) { changedFiles.push({ dos_path: dosPath, content_b64: currentSnapshot[dosPath] }); } }); return changedFiles; } function sendSaveBeacon(changedFiles, endSessionAfterSave) { if (!sessionId || !navigator.sendBeacon) { return false; } var payload = JSON.stringify({ session_id: sessionId, files: changedFiles, end_session: !!endSessionAfterSave }); var blob = new Blob([payload], {type: 'application/json'}); return navigator.sendBeacon('/api/jsdoor/files/' + encodeURIComponent(gameId), blob); } function saveOnExitViaBeacon() { if (!sessionId) { return false; } if (activeFs && writableSyncEnabled && writablePatterns.length > 0) { var exitSnapshot = snapshotWritableFilesSync(activeFs); var exitChangedFiles = buildChangedFiles(exitSnapshot); if (sendSaveBeacon(exitChangedFiles, true)) { initialWritableFiles = exitSnapshot; sessionId = null; return true; } } return false; } function saveModifiedFiles() { if (!sessionId || !activeFs || !writableSyncEnabled || writablePatterns.length === 0) { return Promise.resolve(); } return snapshotWritableFiles(activeFs).then(function (currentSnapshot) { var changedFiles = buildChangedFiles(currentSnapshot); if (changedFiles.length === 0) { return null; } return fetch('/api/jsdoor/files/' + encodeURIComponent(gameId), { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ session_id: sessionId, files: changedFiles }) }).then(function (response) { return response.json(); }) .then(function (data) { if (!data.success) { var message = (window.getApiErrorMessage) ? window.getApiErrorMessage(data, fileSyncErrorMsg) : (data.error || fileSyncErrorMsg); throw new Error(message); } initialWritableFiles = currentSnapshot; }); }); } function exitGame(options) { options = options || {}; var fromEmulator = !!options.fromEmulator; if (isExiting) { return; } isExiting = true; if (saveOnExitViaBeacon()) { if (fromEmulator && notifyParentExit()) { return; } stopPlayer().finally(function () { if (!notifyParentExit()) { leavePlayer(); } }); return; } flushModifiedFiles().catch(function (error) { console.error('jsdos file sync error', error); showError(error.message || fileSyncErrorMsg); return null; }).then(function () { if (fromEmulator && notifyParentExit()) { return null; } return stopPlayer(); }).then(function () { return endSession(false); }).finally(function () { if (!notifyParentExit()) { leavePlayer(); } }); } function handlePlayerEvent(event) { if (event === 'exit' || event === 'stop') { exitGame({fromEmulator: true}); } } function handleLaunchFailure(error) { console.error('jsdos launch error', error); stopPlayer().then(function () { return endSession(false); }).finally(leavePlayer); showError(error && error.message ? error.message : assetErrorMsg); } function fetchAsset(dosPath, assetUrl, optional) { return fetch(assetUrl, {cache: 'no-cache'}).then(function (response) { if (!response.ok) { if (optional && response.status === 404) { console.warn('Skipping optional jsdos asset', assetUrl); return null; } throw new Error('HTTP ' + response.status + ' fetching ' + assetUrl); } return response.arrayBuffer(); }).then(function (buffer) { if (buffer === null) { return null; } return { dosPath: normalizeDosPath(dosPath), data: new Uint8Array(buffer) }; }); } function startEmulator() { var dosboxConf = buildDosboxConf(); resetPlayerCanvas(); setStatus(loadingAssetsMsg); var assetFetches = gameFiles.map(function (file) { var assetUrl = '/jsdos-doors/' + gamePath + '/' + file.asset_path; return fetchAsset(file.dos_path, assetUrl, !!file.optional); }); Promise.all([Promise.all(assetFetches), fetchSyncedFiles()]).then(function (results) { var assets = results[0].filter(function (asset) { return asset !== null; }); var syncedFiles = results[1]; discoveredWritablePaths = uniqueArray(syncedFiles.map(function (file) { return normalizeDosPath(file.dos_path); }).filter(matchesWritablePattern)); setStatus(loadingEmulatorMsg); sizeHostCanvas(); dosPlayer = Dos(playerCanvas, { wdosboxUrl: '/js/jsdos/wdosbox.js', renderAspect: 'Fit', imageRendering: 'pixelated', mouseCapture: true, autolock: true, kiosk: true, noCloud: true, noNetworking: true, onEvent: handlePlayerEvent }); if (dosPlayer && typeof dosPlayer.on === 'function') { dosPlayer.on('exit', exitGame); dosPlayer.on('stop', exitGame); } if (dosPlayer && typeof dosPlayer.setRenderAspect === 'function') { dosPlayer.setRenderAspect('Fit'); } if (dosPlayer && typeof dosPlayer.setImageRendering === 'function') { dosPlayer.setImageRendering('pixelated'); } if (dosPlayer && typeof dosPlayer.setMouseCapture === 'function') { dosPlayer.setMouseCapture(true); } dosPlayer.ready(function (fs, main) { Promise.resolve().then(function () { activeFs = fs; focusPlayerCanvas(); if (fs && fs.dos && typeof fs.dos.registerTerminateListener === 'function') { fs.dos.registerTerminateListener(function () { exitGame({fromEmulator: true}); }); } if (!keepOpen) { removeDosFile(fs, exitMarkerPath); writeDosFile(fs, launchBatchPath, textToUint8Array(getLaunchBatchContents())); } writeDosFile(fs, 'C:/dosbox.conf', dosboxConf); assets.forEach(function (asset) { writeDosFile(fs, asset.dosPath, asset.data); }); syncedFiles.forEach(function (file) { writeDosFile(fs, file.dos_path, base64ToUint8Array(file.content_b64)); }); return snapshotWritableFiles(fs).catch(function (error) { console.error('jsdos initial snapshot error', error); return {}; }).then(function (snapshot) { initialWritableFiles = snapshot; startExitMarkerPoll(fs); startAutosaveLoop(); main(['-conf', 'dosbox.conf']); window.setTimeout(focusPlayerCanvas, 50); setStatus(''); }); }).catch(handleLaunchFailure); }); }).catch(handleLaunchFailure); } function launchGame() { launchBtn.disabled = true; hideError(); resumeTrackedAudioContexts(); createSession().then(function (sid) { sessionId = sid; preLaunchDiv.classList.add('d-none'); playerDiv.classList.remove('d-none'); startEmulator(); }).catch(function (error) { console.error('jsdos session error', error); launchBtn.disabled = false; showError(error.message || sessionErrorMsg); }); } launchBtn.addEventListener('click', launchGame); document.getElementById('jsdos-exit-btn').addEventListener('click', exitGame); sendEscBtn.addEventListener('click', function () { sendEscapeToGame(); window.setTimeout(focusPlayerCanvas, 50); }); playerStage.addEventListener('pointerdown', function () { resumeTrackedAudioContexts(); focusPlayerCanvas(); setStatus(''); }); document.getElementById('jsdos-fullscreen-btn').addEventListener('click', function () { resumeTrackedAudioContexts(); focusPlayerCanvas(); if (dosPlayer && typeof dosPlayer.setFullScreen === 'function') { dosPlayer.setFullScreen(true); window.setTimeout(function () { sizeHostCanvas(); focusPlayerCanvas(); }, 100); return; } if (playerDiv.requestFullscreen) { playerDiv.requestFullscreen(); } else if (playerDiv.webkitRequestFullscreen) { playerDiv.webkitRequestFullscreen(); } else if (playerDiv.msRequestFullscreen) { playerDiv.msRequestFullscreen(); } }); window.addEventListener('beforeunload', function () { if (saveOnExitViaBeacon()) { return; } endSession(true); }); window.addEventListener('resize', function () { sizeHostCanvas(); focusPlayerCanvas(); }); document.addEventListener('fullscreenchange', function () { window.setTimeout(function () { sizeHostCanvas(); focusPlayerCanvas(); }, 100); }); document.addEventListener('pointerlockchange', function () { if (playerDiv.classList.contains('d-none')) { return; } if (!document.pointerLockElement) { setStatus(unlockHintMsg); } else { setStatus(''); } }); ['pointerdown', 'keydown', 'touchstart'].forEach(function (eventName) { document.addEventListener(eventName, resumeTrackedAudioContexts, {passive: true}); }); }()); </script> </body> </html>