974 lines
37 KiB
JavaScript
974 lines
37 KiB
JavaScript
/*
|
|
* dosee-loader.js
|
|
* DOSee emulator
|
|
*
|
|
* Fork of https://github.com/db48x/emularity/blob/master/loader.js
|
|
* Last commit synced to: Feb 15, 2018
|
|
*
|
|
* Major differences:
|
|
* Requires ES6 compatible browser
|
|
* Only DOS emulation
|
|
* No local save game states
|
|
* No Web Assembly builds [todo]
|
|
*/
|
|
|
|
/* global storageAvailable Module BrowserFS ES6Promise FS Promise */
|
|
/* eslint strict: ["error", "safe"] */
|
|
/* eslint no-global-assign: ["error", {"exceptions": ["Module"]}] */
|
|
Module = null;
|
|
|
|
(function (Promise) {
|
|
"use strict"
|
|
|
|
const doseeVersion = `1.8p`
|
|
|
|
// DOSBox requires a valid IndexedDB
|
|
// See: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
|
|
if (window.indexedDB) {
|
|
document.getElementById(`doseeCrashed`).style.display = `none`
|
|
}
|
|
|
|
|
|
// Common API functions
|
|
// DOSee is based off The Emularity which supports multiple emulators.
|
|
// The BaseLoader naming convention was used to highlight the shared functions.
|
|
function DoseeAPI() {
|
|
return Array.prototype.reduce.call(arguments, extend)
|
|
}
|
|
|
|
// HTML <canvas> element used to display the emulation
|
|
DoseeAPI.canvas = function (id) {
|
|
const elem = id instanceof Element ? id : document.getElementById(id)
|
|
return { canvas: elem }
|
|
}
|
|
|
|
DoseeAPI.emulatorJS = function (url) {
|
|
return { emulatorJS: url }
|
|
}
|
|
|
|
DoseeAPI.locateAdditionalEmulatorJS = function (func) {
|
|
return { locateAdditionalJS: func }
|
|
}
|
|
|
|
DoseeAPI.nativeResolution = function (width, height) {
|
|
if (typeof width !== `number` || typeof height !== `number`)
|
|
throw new Error(`Width and height must be numbers`)
|
|
return {
|
|
nativeResolution: {
|
|
width: Math.floor(width),
|
|
height: Math.floor(height),
|
|
}
|
|
}
|
|
}
|
|
|
|
DoseeAPI.aspectRatio = function (ratio) {
|
|
if (typeof ratio !== `number`)
|
|
throw new Error(`Aspect ratio must be a number`)
|
|
return { aspectRatio: ratio }
|
|
}
|
|
|
|
DoseeAPI.mountZip = function (drive, file) {
|
|
return {
|
|
files: [{
|
|
drive: drive,
|
|
mountpoint: `/${drive}`,
|
|
file: file,
|
|
}]
|
|
}
|
|
}
|
|
|
|
DoseeAPI.mountFile = function (filename, file) {
|
|
return {
|
|
files: [{
|
|
mountpoint: filename,
|
|
file: file,
|
|
}]
|
|
}
|
|
}
|
|
|
|
DoseeAPI.fetchFile = function (title, url) {
|
|
return {
|
|
title: title,
|
|
url: url,
|
|
}
|
|
}
|
|
|
|
DoseeAPI.fetchOptionalFile = function (title, url) {
|
|
return {
|
|
title: title,
|
|
url: url,
|
|
optional: true,
|
|
}
|
|
}
|
|
|
|
DoseeAPI.localFile = function (title, data) {
|
|
return {
|
|
title: title,
|
|
data: data,
|
|
}
|
|
}
|
|
|
|
function DoseeLoader() {
|
|
const config = Array.prototype.reduce.call(arguments, extend)
|
|
config.emulator_arguments = build_dosbox_arguments(config.emulatorStart, config.files, config.emulatorCPU)
|
|
return config
|
|
}
|
|
Object.setPrototypeOf(DoseeLoader, DoseeAPI)
|
|
|
|
DoseeLoader.startExe = function (path) {
|
|
return { emulatorStart: path }
|
|
}
|
|
|
|
const build_dosbox_arguments = function (emulator_start, files) {
|
|
console.log(`Initialisation of DOSee ` + doseeVersion)
|
|
let verbose = `with the following configuration:`
|
|
|
|
// get guest program path
|
|
const path = emulator_start.split(/\\|\//) // I have LTS already
|
|
// get guest program file name
|
|
let prog = path.pop()
|
|
|
|
// dosbox command line arguments
|
|
const args = []
|
|
|
|
// parse URL query string
|
|
if (`URLSearchParams` in window == false) {
|
|
console.log(`DOSee needs the URLSearchParams interface to read URL query string values`)
|
|
return args
|
|
}
|
|
const urlParams = new URLSearchParams(window.location.href)
|
|
|
|
// graphic engine scalers (https://www.dosbox.com/wiki/Scaler)
|
|
let scaler = null
|
|
if (storageAvailable(`local`)) scaler = localStorage.getItem(`doseeScaler`) // look for saved option
|
|
if (scaler === null) scaler = `none`
|
|
switch (scaler) {
|
|
case `advinterp3x`:
|
|
verbose += ` Advanced interpolation engine (advinterp3x).`
|
|
args.push(`-conf`, `/dos/s/engine-advinterp3x.con`)
|
|
document.getElementById(`dosscale5`).checked = true
|
|
break
|
|
case `hq3x`:
|
|
verbose += ` High Quality 3 magnification (hq3x).`
|
|
args.push(`-conf`, `/dos/s/engine-hq3x.con`)
|
|
document.getElementById(`dosscale4`).checked = true
|
|
break
|
|
case `rgb3x`:
|
|
verbose += ` RGB engine (rgb3x).`
|
|
args.push(`-conf`, `/dos/s/engine-rgb3x.con`)
|
|
document.getElementById(`dosscale3`).checked = true
|
|
break
|
|
case `super2xsai`:
|
|
verbose += ` Super scale and interpolation engine (super2xsai).`
|
|
args.push(`-conf`, `/dos/s/engine-super2xsai.con`)
|
|
document.getElementById(`dosscale1`).checked = true
|
|
break
|
|
case `tv3x`:
|
|
verbose += ` TV 3x scale engine (tv3x).`
|
|
args.push(`-conf`, `/dos/s/engine-tv3x.con`)
|
|
document.getElementById(`dosscale2`).checked = true
|
|
break
|
|
default: document.getElementById(`dosscale0`).checked = true
|
|
break
|
|
}
|
|
|
|
// impose aspect ratio correction
|
|
let aspect = null
|
|
if (storageAvailable(`local`)) aspect = localStorage.getItem(`doseeAspect`) // look for saved option
|
|
if (aspect === null) aspect = `true`
|
|
if (aspect !== `false`) {
|
|
verbose += ` With aspect correction.`
|
|
args.push(`-conf`, `/dos/s/render.con`) // aspect=true
|
|
} else {
|
|
document.getElementById(`doseeAspect`).checked = true
|
|
verbose += ` No aspect correction.`
|
|
}
|
|
// emulation cpu speed
|
|
const cpuspeed = urlParams.get(`dosspeed`)
|
|
switch (cpuspeed) {
|
|
case `8086`:
|
|
verbose += ` 8086 real mode CPU.`
|
|
args.push(`-conf`, `/dos/s/cpu-8086.con`)
|
|
document.getElementById(`dosspeed4`).checked = true
|
|
break
|
|
case `386`:
|
|
verbose += ` 386 protect mode CPU.`
|
|
args.push(`-conf`, `/dos/s/cpu-386.con`)
|
|
document.getElementById(`dosspeed3`).checked = true
|
|
break
|
|
case `486`: case `max`:
|
|
verbose += ` Unlocked CPU speed.`
|
|
args.push(`-conf`, `/dos/s/cpu-max.con`)
|
|
document.getElementById(`dosspeed2`).checked = true
|
|
break
|
|
default:
|
|
verbose += ` Automatic CPU speed.`
|
|
args.push(`-conf`, `/dos/s/cpu-auto.con`)
|
|
document.getElementById(`dosspeed1`).checked = true
|
|
break
|
|
}
|
|
|
|
// emulation sound cards
|
|
const sound = urlParams.get(`dosaudio`)
|
|
switch (sound) {
|
|
case `none`:
|
|
verbose += ` No audio.`
|
|
args.push(`-conf`, `/dos/s/noaudio.con`)
|
|
document.getElementById(`dosaudio5`).checked = true
|
|
break
|
|
case `sb1`:
|
|
verbose += ` Sound Blaster 1.0 audio.`
|
|
args.push(`-conf`, `/dos/s/sb1.con`)
|
|
document.getElementById(`dosaudio3`).checked = true
|
|
break
|
|
case `gus`:
|
|
verbose += ` Gravis Ultrasound audio.`
|
|
args.push(`-conf`, `/dos/g/gus.con`)
|
|
document.getElementById(`dosaudio1`).checked = true
|
|
break
|
|
case `covox`:
|
|
verbose += ` Covox Speech Accelerator audio.`
|
|
args.push(`-conf`, `/dos/s/covox.con`)
|
|
document.getElementById(`dosaudio4`).checked = true
|
|
break
|
|
default:
|
|
verbose += ` Sound Blaster 16 audio.`
|
|
args.push(`-conf`, `/dos/s/sb16.con`)
|
|
document.getElementById(`dosaudio2`).checked = true
|
|
break
|
|
}
|
|
|
|
// emulation graphics or machine type
|
|
const machine = urlParams.get(`dosmachine`)
|
|
switch (machine) {
|
|
case `svga`:
|
|
verbose += ` SVGA s3 graphics.`
|
|
args.push(`-conf`, `/dos/s/svga.con`)
|
|
document.getElementById(`dosmachine1`).checked = true
|
|
document.getElementById(`svgaEffectsMsg`).style.display = `none`
|
|
break
|
|
case `cga`:
|
|
verbose += ` CGA graphics.`
|
|
args.push(`-conf`, `/dos/s/cga.con`)
|
|
document.getElementById(`dosmachine5`).checked = true
|
|
break
|
|
case `ega`:
|
|
verbose += ` EGA graphics.`
|
|
args.push(`-conf`, `/dos/s/ega.con`)
|
|
document.getElementById(`dosmachine3`).checked = true
|
|
break
|
|
case `herc`:
|
|
verbose += ` Hercules graphics.`
|
|
args.push(`-conf`, `/dos/s/herc.con`)
|
|
document.getElementById(`dosmachine6`).checked = true
|
|
break
|
|
case `tandy`:
|
|
verbose += ` Tandy graphics.`
|
|
args.push(`-conf`, `/dos/s/tandy.con`)
|
|
document.getElementById(`dosmachine4`).checked = true
|
|
break
|
|
case `et3000`:
|
|
verbose += ` SVGA ET3000 graphics.`
|
|
args.push(`-conf`, `/dos/s/et3000.con`)
|
|
document.getElementById(`dosmachine1`).checked = true
|
|
break
|
|
case `et4000`:
|
|
verbose += ` SVGA ET4000 graphics.`
|
|
args.push(`-conf`, `/dos/s/et4000.con`)
|
|
break
|
|
case `paradise`:
|
|
verbose += ` Paradise PVGA1A graphics.`
|
|
args.push(`-conf`, `/dos/s/paradise.con`)
|
|
break
|
|
case `nolfb`:
|
|
verbose += ` SVGA s3 graphics with no-line frame buffer hack.`
|
|
args.push(`-conf`, `/dos/s/nolfb.con`)
|
|
break
|
|
case `oldvbe`:
|
|
verbose += ` VESA 1.3 graphics.`
|
|
args.push(`-conf`, `/dos/s/oldvbe.con`)
|
|
break
|
|
default:
|
|
verbose += ` VGA graphics.`
|
|
args.push(`-conf`, `/dos/s/vgaonly.con`)
|
|
document.getElementById(`dosmachine2`).checked = true
|
|
break
|
|
}
|
|
|
|
// dosbox memory managers
|
|
const ems = urlParams.get(`dosems`)
|
|
if (ems === `false`) {
|
|
verbose += ` ✗ Expanded (EMS) memory.`
|
|
args.push(`-conf`, `/dos/s/noems.con`)
|
|
}
|
|
const umb = urlParams.get(`dosumb`)
|
|
if (umb === `false`) {
|
|
verbose += ` ✗ Upper Memory Block (UMB) access.`
|
|
args.push(`-conf`, `/dos/s/noumb.con`)
|
|
}
|
|
const xms = urlParams.get(`dosxms`)
|
|
if (xms === `false`) {
|
|
verbose += ` ✗ Extended (XMS) memory.`
|
|
args.push(`-conf`, `/dos/s/noxms.con`)
|
|
}
|
|
|
|
// dosbox mount points (dos drive letters)
|
|
for (const i in files) {
|
|
if (`drive` in files[i]) {
|
|
args.push(`-c`, `mount ${files[i].drive} /dos${files[i].mountpoint}`)
|
|
}
|
|
}
|
|
|
|
// dosbox default drive letter
|
|
args.push(`-c`, /^[a-zA-Z]:$/.test(path[0]) ? path.shift() : `C:`)
|
|
|
|
// load drivers not natively supplied by DOSBox and after emulated drives are mounted
|
|
// paths to drivers has been set in the sb1.con under [autoexec]
|
|
if (sound === `sb1`) {
|
|
args.push(`-c`, `SBFMDRV.COM`)
|
|
args.push(`-c`, `SOUND.COM`)
|
|
}
|
|
|
|
// dos utilities with PATH setup
|
|
const dosutilities = urlParams.get(`dosutils`)
|
|
if (dosutilities === `true`) {
|
|
args.push(`-conf`, `/dos/s/utils.con`)
|
|
}
|
|
|
|
// some programs don't run correctly unless their root directory is active
|
|
if (path && path.length) {
|
|
let pathStr = path.toString()
|
|
pathStr = pathStr.replace(`,`, `\\`)
|
|
console.log(`Execute path ${pathStr}`)
|
|
args.push(`-c`, `CD ${pathStr}`)
|
|
}
|
|
|
|
// automatically run the guest program
|
|
const skiprun = urlParams.get(`dosautorun`)
|
|
if (skiprun !== `false`) {
|
|
prog = prog.replace(` :`, ` /`) // hack to implement program options
|
|
verbose = `Will execute \`${prog}\` ${verbose}`
|
|
args.push(`-c`, prog)
|
|
|
|
// partial ansi escape code generator for coloured text
|
|
const ansi = function (code, str) {
|
|
let c = String.fromCharCode(27, 91, 48, 109) // esc[0m
|
|
switch (code) {
|
|
case `bold`: c = String.fromCharCode(27, 91, 49, 109); break // esc[1m
|
|
case `blue`: c = String.fromCharCode(27, 91, 51, 52, 109); break // esc[34m
|
|
case `white`: c = String.fromCharCode(27, 91, 51, 55, 109); break // esc[37m
|
|
}
|
|
if (str === undefined) return c
|
|
return `${c}${str}`
|
|
}
|
|
|
|
// test to display after guest program is complete
|
|
if (urlParams.get(`name`) !== `waitingapproval`) {
|
|
const finCmd = `@echo ${prog} has finished. -${ansi(`bold`)}${ansi(`blue`, `d`)}${ansi(`white`, `e`)}\
|
|
${ansi(`blue`, `f`)}${ansi(`white`, `acto`)}${ansi(`blue`, `2`)}${ansi(`white`, `.net`)}${ansi()}-`
|
|
args.push(`-c`, finCmd)
|
|
}
|
|
}
|
|
console.log(verbose)
|
|
return args
|
|
}
|
|
|
|
/**
|
|
* Emulator
|
|
*/
|
|
function Emulator(canvas, callbacks, loadFiles) {
|
|
if (typeof callbacks !== `object`) {
|
|
callbacks = {
|
|
before_emulator: null,
|
|
before_run: callbacks,
|
|
}
|
|
}
|
|
let has_started = false
|
|
const defaultSplashColors = {
|
|
foreground: `white`,
|
|
background: `black`,
|
|
failure: `red`,
|
|
success: `green`,
|
|
}
|
|
const splash = {
|
|
loading_text: ``,
|
|
spinning: true,
|
|
finished_loading: false,
|
|
colors: defaultSplashColors,
|
|
table: null,
|
|
splashimg: new Image(),
|
|
}
|
|
|
|
let cssResolution, scale, aspectRatio
|
|
// right off the bat we set the canvas's inner dimensions to
|
|
// whatever it's current css dimensions are this isn't likely to be
|
|
// the same size that dosbox will set it to, but it avoids
|
|
// the case where the size was left at the default 300x150
|
|
if (!canvas.hasAttribute(`width`)) {
|
|
const style = getComputedStyle(canvas)
|
|
canvas.width = parseInt(style.width, 10)
|
|
canvas.height = parseInt(style.height, 10)
|
|
}
|
|
|
|
this.setSplashImage = function (_splashimg) {
|
|
if (_splashimg) {
|
|
if (_splashimg instanceof Image) {
|
|
if (splash.splashimg.parentNode) {
|
|
splash.splashimg.src = _splashimg.src
|
|
} else {
|
|
splash.splashimg = _splashimg
|
|
}
|
|
} else {
|
|
splash.splashimg.src = _splashimg
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
this.setCallbacks = function (_callbacks) {
|
|
if (typeof _callbacks !== `object`) {
|
|
callbacks = {
|
|
before_emulator: null,
|
|
before_run: _callbacks,
|
|
}
|
|
} else {
|
|
callbacks = _callbacks
|
|
}
|
|
return this
|
|
}
|
|
|
|
this.setSplashColors = function (colors) {
|
|
splash.colors = colors
|
|
return this
|
|
}
|
|
|
|
this.setLoad = function (loadFunc) {
|
|
loadFiles = loadFunc
|
|
return this
|
|
}
|
|
|
|
const start = function (options) {
|
|
if (has_started) return false
|
|
has_started = true
|
|
if (typeof options !== `object`) {
|
|
options = { waitAfterDownloading: false }
|
|
}
|
|
let k, c, game_data
|
|
setupSplash(canvas, splash)
|
|
drawsplash()
|
|
|
|
let loading
|
|
if (typeof loadFiles === `function`) {
|
|
loading = loadFiles(fetch_file, splash)
|
|
} else {
|
|
loading = Promise.resolve(loadFiles)
|
|
}
|
|
|
|
loading.then(loadHardDrive)
|
|
.then(loadBranding, errBranding)
|
|
.then(loadDosWarp, errDosWarp)
|
|
|
|
// hide long load time messages once emulator has loaded
|
|
{
|
|
const sdlm = document.getElementById(`doseeSlowLoad`)
|
|
if (sdlm !== `undefined`) {
|
|
sdlm.style.display = `none`
|
|
}
|
|
}
|
|
return this
|
|
|
|
function loadHardDrive(_game_data) {
|
|
return new Promise(function (resolve, reject) {
|
|
const deltaFS = new BrowserFS.FileSystem.InMemory()
|
|
finish()
|
|
|
|
function finish() {
|
|
game_data = _game_data
|
|
|
|
// Any file system writes to MountableFileSystem will be written to the
|
|
// deltaFS, letting us mount read-only zip files into the MountableFileSystem
|
|
// while being able to 'write' to them.
|
|
game_data.fs = new BrowserFS.FileSystem.OverlayFS(deltaFS,
|
|
new BrowserFS.FileSystem.MountableFileSystem())
|
|
game_data.fs.initialize(function () {
|
|
const Buffer = BrowserFS.BFSRequire(`buffer`).Buffer
|
|
|
|
function fetch(file) {
|
|
if (`data` in file && file.data !== null && typeof file.data !== `undefined`) {
|
|
return Promise.resolve(file.data)
|
|
}
|
|
return fetch_file(file.title, file.url, `arraybuffer`, file.optional)
|
|
}
|
|
|
|
function mountAt(drive) {
|
|
return function (data) {
|
|
if (data !== null) {
|
|
drive = drive.toLowerCase()
|
|
const mountpoint = `/${drive}`
|
|
// Mount into RO MFS.
|
|
game_data.fs.getOverlayedFileSystems().readable.mount(mountpoint, BFSOpenZip(new Buffer(data)))
|
|
}
|
|
}
|
|
}
|
|
|
|
Promise.all(game_data.files
|
|
.map(function (f) {
|
|
if (f && f.file) {
|
|
if (f.drive) return fetch(f.file).then(mountAt(f.drive))
|
|
}
|
|
return null
|
|
})).then(resolve, reject)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function loadBranding() {
|
|
if (!game_data || splash.failed_loading) return null
|
|
if (options.waitAfterDownloading) {
|
|
return new Promise(function (resolve) {
|
|
splash.setTitle(`✔ Click here to run`)
|
|
splash.spinning = false
|
|
// stashes these event listeners so that we can remove them after
|
|
window.addEventListener(`keydown`, k = keyevent(resolve))
|
|
canvas.addEventListener(`click`, c = resolve)
|
|
splash.splashElt.addEventListener(`click`, c)
|
|
})
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
function loadDosWarp() {
|
|
if (!game_data || splash.failed_loading) return
|
|
splash.spinning = true
|
|
window.removeEventListener(`keypress`, k)
|
|
canvas.removeEventListener(`click`, c)
|
|
splash.splashElt.removeEventListener(`click`, c)
|
|
|
|
// Don't let arrow, pg up/down, home, end affect page position
|
|
blockSomeKeys()
|
|
setupFullScreen()
|
|
disableRightClickContextMenu(canvas)
|
|
|
|
// Emscripten doesn't use the proper prefixed functions for fullscreen requests,
|
|
// so let's map the prefixed versions to the correct function.
|
|
canvas.requestPointerLock = getpointerlockenabler()
|
|
|
|
moveConfigToRoot(game_data.fs)
|
|
Module = init_module(
|
|
game_data.emulator_arguments,
|
|
game_data.fs,
|
|
game_data.locateAdditionalJS,
|
|
game_data.nativeResolution,
|
|
game_data.aspectRatio)
|
|
|
|
if (callbacks && callbacks.before_emulator) {
|
|
try {
|
|
callbacks.before_emulator()
|
|
} catch (x) {
|
|
console.log(x)
|
|
}
|
|
}
|
|
if (game_data.emulatorJS) {
|
|
// enable the operator screenshot and upload button plus jump to the emulation canvas
|
|
{
|
|
const oscb = document.getElementById(`doseeCaptureUpload`)
|
|
if (oscb !== null) {
|
|
oscb.disabled = false
|
|
}
|
|
window.location.href = `#emulator`
|
|
}
|
|
splash.setTitle(`Warping to DOS`)
|
|
attach_script(game_data.emulatorJS)
|
|
} else {
|
|
splash.setTitle(`Non-system disk or disk error`)
|
|
}
|
|
}
|
|
|
|
function errBranding() {
|
|
if (splash.failed_loading) return null
|
|
splash.setTitle(`The emulator broke ${String.fromCharCode(9785)}`) // frown face
|
|
splash.failed_loading = true
|
|
}
|
|
|
|
function errDosWarp() {
|
|
if (splash.failed_loading) return
|
|
splash.setTitle(`Invalid media, track 0 bad or unusable`)
|
|
splash.failed_loading = true
|
|
}
|
|
}
|
|
this.start = start
|
|
|
|
const init_module = function (args, fs, locateAdditionalJS, nativeResolution, aspectRatio) {
|
|
return {
|
|
arguments: args,
|
|
screenIsReadOnly: true,
|
|
print: function (text) {
|
|
// feedback from DOSBox
|
|
console.log(text)
|
|
},
|
|
canvas: canvas,
|
|
noInitialRun: false,
|
|
locateFile: locateAdditionalJS,
|
|
preInit: function () {
|
|
splash.setTitle(`Loading program into the file system`)
|
|
// Re-initialize BFS to just use the writeable in-memory storage.
|
|
BrowserFS.initialize(fs)
|
|
const BFS = new BrowserFS.EmscriptenFS()
|
|
// Mount the file system into Emscripten.
|
|
FS.mkdir(`/dos`)
|
|
FS.mount(BFS, { root: `/` }, `/dos`)
|
|
splash.finished_loading = true
|
|
splash.hide()
|
|
setTimeout(function () {
|
|
resizeCanvas(canvas,
|
|
scale = scale || scale,
|
|
cssResolution = nativeResolution || cssResolution,
|
|
aspectRatio = aspectRatio || aspectRatio)
|
|
})
|
|
if (callbacks && callbacks.before_run) {
|
|
window.setTimeout(function () {
|
|
callbacks.before_run()
|
|
}, 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const formatSize = function (event) {
|
|
if (event.lengthComputable)
|
|
return `(${(event.total ? (event.loaded / event.total * 100).toFixed(0) : `100`)}% \
|
|
${formatBytes(event.loaded)} of ${formatBytes(event.total)})`
|
|
return `(${formatBytes(event.loaded)})`
|
|
}
|
|
|
|
const formatBytes = function (bytes, base10) {
|
|
if (bytes === 0) return `0 B`
|
|
const unit = base10 ? 1000 : 1024,
|
|
units = base10 ? [`B`, `kB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB`] : [`B`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB`, `ZiB`, `YiB`],
|
|
exp = parseInt((Math.log(bytes) / Math.log(unit))),
|
|
size = bytes / Math.pow(unit, exp)
|
|
return `${size.toFixed(1)} ${units[exp]}`
|
|
}
|
|
|
|
const fetch_file = function (title, url, rt, optional) {
|
|
const row = addRow(splash.table)
|
|
const titleCell = row[0],
|
|
statusCell = row[1]
|
|
titleCell.textContent = title
|
|
return new Promise(function (resolve, reject) {
|
|
const xhr = new XMLHttpRequest()
|
|
xhr.open(`GET`, url, true)
|
|
xhr.responseType = rt || `arraybuffer`
|
|
xhr.onprogress = function (e) {
|
|
titleCell.textContent = `${title} ${formatSize(e)}`
|
|
}
|
|
xhr.onload = function () {
|
|
if (xhr.status === 200 || xhr.status === 0) {
|
|
success()
|
|
resolve(xhr.response)
|
|
} else if (optional) {
|
|
success()
|
|
resolve(null)
|
|
} else {
|
|
failure()
|
|
reject()
|
|
}
|
|
}
|
|
xhr.onerror = function () {
|
|
if (optional) {
|
|
success()
|
|
resolve(null)
|
|
} else {
|
|
failure()
|
|
reject()
|
|
}
|
|
}
|
|
|
|
function success() {
|
|
statusCell.textContent = `✔`
|
|
statusCell.style.color = splash.getColor(`success`)
|
|
titleCell.textContent = title
|
|
titleCell.style.fontWeight = `bold`
|
|
titleCell.parentNode.style.backgroundColor = splash.getColor(`foreground`)
|
|
titleCell.parentNode.style.color = splash.getColor(`background`)
|
|
}
|
|
|
|
function failure() {
|
|
statusCell.textContent = `✘`
|
|
statusCell.style.color = splash.getColor(`failure`)
|
|
titleCell.textContent = title
|
|
titleCell.style.fontWeight = `bold`
|
|
titleCell.parentNode.style.backgroundColor = splash.getColor(`foreground`)
|
|
titleCell.parentNode.style.color = splash.getColor(`failure`)
|
|
}
|
|
xhr.send()
|
|
})
|
|
}
|
|
|
|
function keyevent(resolve) {
|
|
return function (e) {
|
|
if ((e.keycode || e.which) == 32) {
|
|
e.preventDefault()
|
|
resolve()
|
|
}
|
|
}
|
|
}
|
|
|
|
const resizeCanvas = function (canvas, scale, resolution) {
|
|
if (scale && resolution) {
|
|
// optimizeSpeed is the standardized value. different
|
|
// browsers support different values they will all ignore
|
|
// values that they don't understand.
|
|
canvas.style.imageRendering = `-moz-crisp-edges`
|
|
canvas.style.imageRendering = `-o-crisp-edges`
|
|
canvas.style.imageRendering = `-webkit-optimize-contrast`
|
|
canvas.style.imageRendering = `optimize-contrast`
|
|
canvas.style.imageRendering = `crisp-edges`
|
|
canvas.style.imageRendering = `pixelated`
|
|
canvas.style.imageRendering = `optimizeSpeed`
|
|
|
|
canvas.style.width = `${resolution.width * scale}px`
|
|
canvas.style.height = `${resolution.height * scale}px`
|
|
canvas.width = resolution.width
|
|
canvas.height = resolution.height
|
|
}
|
|
}
|
|
|
|
function setupSplash(canvas, splash) {
|
|
splash.splashElt = document.getElementById(`doseeSplashScreen`)
|
|
if (!splash.splashElt) {
|
|
splash.splashElt = document.createElement(`div`)
|
|
splash.splashElt.setAttribute(`id`, `doseeSplashScreen`)
|
|
splash.splashElt.style.position = `absolute`
|
|
splash.splashElt.style.top = `${canvas.offsetTop}px`
|
|
splash.splashElt.style.left = `${canvas.offsetLeft}px`
|
|
splash.splashElt.style.width = `${canvas.offsetWidth}px`
|
|
splash.splashElt.style.color = splash.getColor(`foreground`)
|
|
splash.splashElt.style.backgroundColor = splash.getColor(`background`)
|
|
canvas.parentElement.appendChild(splash.splashElt)
|
|
}
|
|
|
|
splash.splashimg.setAttribute(`id`, `doseeSplashImg`)
|
|
splash.splashimg.style.display = `block`
|
|
splash.splashimg.style.marginLeft = `auto`
|
|
splash.splashimg.style.marginRight = `auto`
|
|
splash.splashElt.appendChild(splash.splashimg)
|
|
|
|
splash.titleElt = document.createElement(`span`)
|
|
splash.titleElt.setAttribute(`id`, `doseeSplashTitle`)
|
|
splash.titleElt.style.display = `block`
|
|
splash.titleElt.style.width = `100%`
|
|
splash.titleElt.style.marginTop = `1em`
|
|
splash.titleElt.style.marginBottom = `1em`
|
|
splash.titleElt.style.textAlign = `center`
|
|
splash.titleElt.style.font = `24px sans-serif`
|
|
splash.titleElt.textContent = ``
|
|
splash.splashElt.appendChild(splash.titleElt)
|
|
|
|
let table = document.getElementById(`doseeProgressIndicator`)
|
|
if (!table) {
|
|
table = document.createElement(`table`)
|
|
table.setAttribute(`id`, `doseeProgressIndicator`)
|
|
table.style.width = `50%`
|
|
table.style.color = splash.getColor(`foreground`)
|
|
table.style.backgroundColor = splash.getColor(`background`)
|
|
table.style.marginLeft = `auto`
|
|
table.style.marginRight = `auto`
|
|
table.style.borderCollapse = `separate`
|
|
table.style.borderSpacing = `2px`
|
|
splash.splashElt.appendChild(table)
|
|
}
|
|
splash.table = table
|
|
}
|
|
|
|
splash.setTitle = function (title) {
|
|
splash.titleElt.textContent = title
|
|
}
|
|
|
|
splash.hide = function () {
|
|
splash.splashElt.style.display = `none`
|
|
}
|
|
|
|
splash.getColor = function (name) {
|
|
return name in splash.colors ? splash.colors[name] : defaultSplashColors[name]
|
|
}
|
|
|
|
const addRow = function (table) {
|
|
const row = table.insertRow(-1)
|
|
row.style.textAlign = `center`
|
|
const cell = row.insertCell(-1)
|
|
cell.style.position = `relative`
|
|
const titleCell = document.createElement(`span`)
|
|
titleCell.textContent = `—`
|
|
titleCell.style.verticalAlign = `center`
|
|
titleCell.style.minHeight = `24px`
|
|
cell.appendChild(titleCell)
|
|
const statusCell = document.createElement(`span`)
|
|
statusCell.style.position = `absolute`
|
|
statusCell.style.left = `0`
|
|
statusCell.style.paddingLeft = `0.5em`
|
|
cell.appendChild(statusCell)
|
|
return [titleCell, statusCell]
|
|
}
|
|
|
|
const drawsplash = function () {
|
|
canvas.setAttribute(`moz-opaque`, ``)
|
|
if (!splash.splashimg.src) {
|
|
splash.splashimg.src = `images/floppy_disk_icon-180x180.png`
|
|
}
|
|
}
|
|
|
|
function attach_script(js_url) {
|
|
if (js_url) {
|
|
const head = document.getElementsByTagName(`head`)[0]
|
|
const newScript = document.createElement(`script`)
|
|
newScript.type = `text/javascript`
|
|
newScript.src = js_url
|
|
head.appendChild(newScript)
|
|
}
|
|
}
|
|
|
|
function getpointerlockenabler() {
|
|
return canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock
|
|
}
|
|
|
|
function getfullscreenenabler() {
|
|
return canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen || canvas.requestFullScreen
|
|
}
|
|
|
|
this.isfullscreensupported = function () {
|
|
return !!(getfullscreenenabler())
|
|
}
|
|
|
|
function setupFullScreen() {
|
|
const fullScreenChangeHandler = function () {
|
|
if (!(document.mozFullScreenElement || document.fullScreenElement)) {
|
|
resizeCanvas(canvas, scale, cssResolution, aspectRatio)
|
|
}
|
|
}
|
|
if (`onfullscreenchange` in document) {
|
|
document.addEventListener(`fullscreenchange`, fullScreenChangeHandler)
|
|
} else if (`onmozfullscreenchange` in document) {
|
|
document.addEventListener(`mozfullscreenchange`, fullScreenChangeHandler)
|
|
} else if (`onwebkitfullscreenchange` in document) {
|
|
document.addEventListener(`webkitfullscreenchange`, fullScreenChangeHandler)
|
|
}
|
|
}
|
|
|
|
this.requestFullScreen = function () {
|
|
Module.requestFullScreen(1, 0)
|
|
}
|
|
|
|
/**
|
|
* Prevents page navigation keys such as page up/page down from
|
|
* moving the page while the user is playing.
|
|
*/
|
|
function blockSomeKeys() {
|
|
function keypress(e) {
|
|
if (e.which >= 33 && e.which <= 40) {
|
|
e.preventDefault()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
window.onkeydown = keypress
|
|
}
|
|
|
|
/**
|
|
* Disables the right click menu for the given element.
|
|
*/
|
|
function disableRightClickContextMenu(element) {
|
|
element.addEventListener(`contextmenu`,
|
|
function (e) {
|
|
if (e.button == 2) {
|
|
// Block right-click menu thru preventing default action.
|
|
e.preventDefault()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* misc
|
|
*/
|
|
function BFSOpenZip(loadedData) {
|
|
return new BrowserFS.FileSystem.ZipFS(loadedData)
|
|
}
|
|
|
|
// This is such a hack. We're not calling the BrowserFS api
|
|
// 'correctly', so we have to synthesize these flags ourselves
|
|
const flag_r = {
|
|
isReadable: function () { return true },
|
|
isWriteable: function () { return false },
|
|
isTruncating: function () { return false },
|
|
isAppendable: function () { return false },
|
|
isSynchronous: function () { return false },
|
|
isExclusive: function () { return false },
|
|
pathExistsAction: function () { return 0 },
|
|
pathNotExistsAction: function () { return 1 },
|
|
}
|
|
const flag_w = {
|
|
isReadable: function () { return false },
|
|
isWriteable: function () { return true },
|
|
isTruncating: function () { return false },
|
|
isAppendable: function () { return false },
|
|
isSynchronous: function () { return false },
|
|
isExclusive: function () { return false },
|
|
pathExistsAction: function () { return 0 },
|
|
pathNotExistsAction: function () { return 3 },
|
|
}
|
|
|
|
/**
|
|
* Searches for dosbox.conf, and moves it to '/dosbox.conf'
|
|
so dosbox uses it.
|
|
*/
|
|
function moveConfigToRoot(fs) {
|
|
let dosboxConfPath = null
|
|
// Recursively search for dosbox.conf.
|
|
function searchDirectory(dirPath) {
|
|
fs.readdirSync(dirPath).forEach(function (item) {
|
|
if (dosboxConfPath) return
|
|
// Avoid infinite recursion by ignoring these entries, which exist at
|
|
// the root.
|
|
if (item === `.` || item === `..`) return
|
|
// Append `/` between dirPath and the item's name... unless dirPath
|
|
// already ends in it (which always occurs if dirPath is the root, `/`).
|
|
const itemPath = dirPath + (dirPath[dirPath.length - 1] !== `/` ? `/` : ``) + item, itemStat = fs.statSync(itemPath)
|
|
if (itemStat.isDirectory(itemStat.mode)) {
|
|
searchDirectory(itemPath)
|
|
} else if (item === `dosbox.conf`) {
|
|
dosboxConfPath = itemPath
|
|
}
|
|
})
|
|
}
|
|
|
|
searchDirectory(`/`)
|
|
|
|
if (dosboxConfPath !== null) {
|
|
fs.writeFileSync(`/dosbox.conf`, fs.readFileSync(dosboxConfPath, null, flag_r), null, flag_w, 0x1a4)
|
|
}
|
|
}
|
|
|
|
function extend(a, b) {
|
|
if (a === null) return b
|
|
if (b === null) return a
|
|
const ta = typeof a,
|
|
tb = typeof b
|
|
if (ta !== tb) {
|
|
if (ta === `undefined`) return b
|
|
if (tb === `undefined`) return a
|
|
throw new Error(`Cannot extend an ${ta} with an ${tb}`)
|
|
}
|
|
if (Array.isArray(a)) return a.concat(b)
|
|
if (ta === `object`) {
|
|
Object.keys(b).forEach(function (k) { a[k] = extend(a[k], b[k]) })
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
document.getElementById(`doseeVersion`).innerHTML = ` version ${doseeVersion}`
|
|
window.DoseeLoader = DoseeLoader
|
|
window.Emulator = Emulator
|
|
})(typeof Promise === `undefined` ? ES6Promise.Promise : Promise) |