├── .gitignore ├── README.md ├── demo.gif ├── misc.d.ts ├── package-lock.json ├── package.json ├── room.blend ├── src ├── DosWorkerWrapper.ts ├── VRDos.ts ├── Xhr.ts ├── boot.gif ├── favicon.ico ├── index.html ├── index.ts ├── styles.css └── utils.ts ├── static ├── dos-hdd.zip ├── room.glb ├── wdosbox-emterp.wasm └── wdosbox-emterp.worker.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist 4 | .DS_Store 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MS-DOS Virtual Reality 2 | 3 | http://sonictruth.github.io/vr-dos/ 4 | 5 | This is an experimental emulator for a "PC running DOS" within a VR environment. 6 | The VR environment was constructed using Three.js, and the emulator was rendered within this world using CanvasTexture. 7 | The main challenge was achieving good FPS, as the main thread required a significant amount of CPU for VR rendering. 8 | Therefore, I modified JS-Dos to enable its compilation as a Web Worker. 9 | 10 | ![](demo.gif) 11 | 12 | ## Keys 13 | 14 | You can use your keyboard to control the emulator on your PC, and your mouse to look around. 15 | You can use the virtual keys at the top right on mobile. 16 | These are the default mappings in VR (tested with Oculus Quest): 17 | 18 | 0: [Key.Enter], // Trigger 19 | 20 | 1: [Key.Shift], // Squeeze 21 | 22 | 3: [Key.Ctrl], // Joystick press 23 | 24 | 4: [Key.Space, Key.Shift, Key.Ctrl], // A 25 | 26 | 5: [Key.Ctrl, Key.Q, Key.Escape] // B 27 | 28 | ## TODO 29 | - Add sound support 30 | - Add mouse support 31 | - Add Joystick support 32 | - Optimize canvas drawing using OffscreenCanvas 33 | - Optimize rendering loops, gamepad handling 34 | 35 | ## Credits 36 | 3D Model 37 | https://sketchfab.com/railek 38 | js-dos 39 | https://js-dos.com/ 40 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/demo.gif -------------------------------------------------------------------------------- /misc.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "webxr-polyfill"; 7 | 8 | type XRSessionMode = 9 | | "inline" 10 | | "immersive-vr" 11 | | "immersive-ar"; 12 | 13 | type XRReferenceSpaceType = 14 | | "viewer" 15 | | "local" 16 | | "local-floor" 17 | | "bounded-floor" 18 | | "unbounded"; 19 | 20 | type XREnvironmentBlendMode = 21 | | "opaque" 22 | | "additive" 23 | | "alpha-blend"; 24 | 25 | type XRVisibilityState = 26 | | "visible" 27 | | "visible-blurred" 28 | | "hidden"; 29 | 30 | type XRHandedness = 31 | | "none" 32 | | "left" 33 | | "right"; 34 | 35 | type XRTargetRayMode = 36 | | "gaze" 37 | | "tracked-pointer" 38 | | "screen"; 39 | 40 | type XREye = 41 | | "none" 42 | | "left" 43 | | "right"; 44 | 45 | type XREventType = 46 | | "devicechange" 47 | | "visibilitychange" 48 | | "end" 49 | | "inputsourceschange" 50 | | "select" 51 | | "selectstart" 52 | | "selectend" 53 | | "squeeze" 54 | | "squeezestart" 55 | | "squeezeend" 56 | | "reset"; 57 | 58 | interface XRSpace extends EventTarget { 59 | 60 | } 61 | 62 | interface XRRenderState { 63 | depthNear?: number; 64 | depthFar?: number; 65 | inlineVerticalFieldOfView?: number; 66 | baseLayer?: XRWebGLLayer; 67 | } 68 | 69 | interface XRInputSource { 70 | handedness: XRHandedness; 71 | targetRayMode: XRTargetRayMode; 72 | targetRaySpace: XRSpace; 73 | gripSpace: XRSpace | undefined; 74 | gamepad: Gamepad | undefined; 75 | profiles: Array; 76 | } 77 | 78 | interface XRSessionInit { 79 | optionalFeatures?: XRReferenceSpaceType[]; 80 | requiredFeatures?: XRReferenceSpaceType[]; 81 | } 82 | 83 | interface XRSession extends XRAnchorCreator { 84 | addEventListener: Function; 85 | removeEventListener: Function; 86 | requestReferenceSpace(type: XRReferenceSpaceType): Promise; 87 | updateRenderState(XRRenderStateInit: XRRenderState): Promise; 88 | requestAnimationFrame: Function; 89 | end(): Promise; 90 | renderState: XRRenderState; 91 | inputSources: Array; 92 | 93 | // hit test 94 | requestHitTestSource(options: XRHitTestOptionsInit): Promise; 95 | requestHitTestSourceForTransientInput(options: XRTransientInputHitTestOptionsInit): Promise; 96 | 97 | // legacy AR hit test 98 | requestHitTest(ray: XRRay, referenceSpace: XRReferenceSpace): Promise; 99 | 100 | // legacy plane detection 101 | updateWorldTrackingState(options: { 102 | planeDetectionState?: { enabled: boolean; } 103 | }): void; 104 | } 105 | 106 | interface XRReferenceSpace extends XRSpace { 107 | getOffsetReferenceSpace(originOffset: XRRigidTransform): XRReferenceSpace; 108 | onreset: any; 109 | } 110 | 111 | type XRPlaneSet = Set; 112 | type XRAnchorSet = Set; 113 | 114 | interface XRFrame { 115 | session: XRSession; 116 | getViewerPose(referenceSpace: XRReferenceSpace): XRViewerPose | undefined; 117 | getPose(space: XRSpace, baseSpace: XRSpace): XRPose | undefined; 118 | 119 | // AR 120 | getHitTestResults(hitTestSource: XRHitTestSource): Array ; 121 | getHitTestResultsForTransientInput(hitTestSource: XRTransientInputHitTestSource): Array; 122 | // Anchors 123 | trackedAnchors?: XRAnchorSet; 124 | // Planes 125 | worldInformation: { 126 | detectedPlanes?: XRPlaneSet; 127 | }; 128 | } 129 | 130 | interface XRViewerPose extends XRPose { 131 | views: Array; 132 | } 133 | 134 | interface XRPose { 135 | transform: XRRigidTransform; 136 | emulatedPosition: boolean; 137 | } 138 | 139 | interface XRWebGLLayerOptions { 140 | antialias?: boolean; 141 | depth?: boolean; 142 | stencil?: boolean; 143 | alpha?: boolean; 144 | multiview?: boolean; 145 | framebufferScaleFactor?: number; 146 | } 147 | 148 | declare var XRWebGLLayer: { 149 | prototype: XRWebGLLayer; 150 | new(session: XRSession, context: WebGLRenderingContext | undefined, options?: XRWebGLLayerOptions): XRWebGLLayer; 151 | }; 152 | interface XRWebGLLayer { 153 | framebuffer: WebGLFramebuffer; 154 | framebufferWidth: number; 155 | framebufferHeight: number; 156 | getViewport: Function; 157 | } 158 | 159 | declare class XRRigidTransform { 160 | constructor(matrix: Float32Array | DOMPointInit, direction?: DOMPointInit); 161 | position: DOMPointReadOnly; 162 | orientation: DOMPointReadOnly; 163 | matrix: Float32Array; 164 | inverse: XRRigidTransform; 165 | } 166 | 167 | interface XRView { 168 | eye: XREye; 169 | projectionMatrix: Float32Array; 170 | transform: XRRigidTransform; 171 | } 172 | 173 | interface XRInputSourceChangeEvent { 174 | session: XRSession; 175 | removed: Array; 176 | added: Array; 177 | } 178 | 179 | interface XRInputSourceEvent extends Event { 180 | readonly frame: XRFrame; 181 | readonly inputSource: XRInputSource; 182 | } 183 | 184 | // Experimental(er) features 185 | declare class XRRay { 186 | constructor(transformOrOrigin: XRRigidTransform | DOMPointInit, direction?: DOMPointInit); 187 | origin: DOMPointReadOnly; 188 | direction: DOMPointReadOnly; 189 | matrix: Float32Array; 190 | } 191 | 192 | declare enum XRHitTestTrackableType { 193 | "point", 194 | "plane" 195 | } 196 | 197 | interface XRHitResult { 198 | hitMatrix: Float32Array; 199 | } 200 | 201 | interface XRTransientInputHitTestResult { 202 | readonly inputSource: XRInputSource; 203 | readonly results: Array; 204 | } 205 | 206 | interface XRHitTestResult { 207 | getPose(baseSpace: XRSpace): XRPose | undefined; 208 | } 209 | 210 | interface XRHitTestSource { 211 | cancel(): void; 212 | } 213 | 214 | interface XRTransientInputHitTestSource { 215 | cancel(): void; 216 | } 217 | 218 | interface XRHitTestOptionsInit { 219 | space: XRSpace; 220 | entityTypes?: Array; 221 | offsetRay?: XRRay; 222 | } 223 | 224 | interface XRTransientInputHitTestOptionsInit { 225 | profile: string; 226 | entityTypes?: Array; 227 | offsetRay?: XRRay; 228 | } 229 | 230 | interface XRAnchor { 231 | // remove? 232 | id?: string; 233 | anchorSpace: XRSpace; 234 | lastChangedTime: number; 235 | detach(): void; 236 | } 237 | 238 | interface XRPlane extends XRAnchorCreator { 239 | orientation: "Horizontal" | "Vertical"; 240 | planeSpace: XRSpace; 241 | polygon: Array; 242 | lastChangedTime: number; 243 | } 244 | 245 | interface XRAnchorCreator { 246 | // AR Anchors 247 | createAnchor(pose: XRPose | XRRigidTransform, referenceSpace: XRReferenceSpace): Promise; 248 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vr-dos", 3 | "version": "1.0.0", 4 | "description": "MS-DOS in Virtual Reality", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel serve src/index.html --no-source-maps --https --open ", 8 | "build": "parcel build src/index.html --no-source-maps --public-url ./", 9 | "deploy": "npm run build; gh-pages --dist dist;" 10 | }, 11 | "author": "dextor@gmail.com", 12 | "license": "ISC", 13 | "homepage": "https://sonictruth.github.io/vr-dos/", 14 | "devDependencies": { 15 | "@types/node": "^13.13.4", 16 | "@types/stats.js": "^0.17.0", 17 | "gh-pages": "^2.2.0", 18 | "parcel": "^1.12.4", 19 | "parcel-plugin-static-files-copy": "^2.3.1", 20 | "shx": "^0.3.2", 21 | "typescript": "^3.8.3" 22 | }, 23 | "dependencies": { 24 | "@webxr-input-profiles/motion-controllers": "^1.0.0", 25 | "stats.js": "^0.17.0", 26 | "three": "^0.115.0", 27 | "ts-keycode-enum": "^1.0.6", 28 | "webxr-polyfill": "^2.0.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /room.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/room.blend -------------------------------------------------------------------------------- /src/DosWorkerWrapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cloneObject, 3 | shouldPreventDefault, 4 | dosboxConf, 5 | } from './utils'; 6 | 7 | import { Xhr } from './Xhr' 8 | import { CanvasTexture } from 'three'; 9 | 10 | class DosWorkerWrapper { 11 | private zipUrl: string = ''; 12 | private workerUrl = 'wdosbox-emterp.worker.js'; 13 | private home = '/home/web_user/'; 14 | private canvas: HTMLCanvasElement; 15 | private renderFrameData: ArrayLike | null = null; 16 | private ctx: CanvasRenderingContext2D | null = null; 17 | private imageData: ImageData | null = null; 18 | private frameId = 0; 19 | private worker: Worker | null = null; 20 | private rpcPromises = {}; 21 | private commands: string[] = []; 22 | private startedPromise = () => { }; 23 | private texture: CanvasTexture; 24 | 25 | 26 | constructor(canvas: HTMLCanvasElement, texture: CanvasTexture) { 27 | this.canvas = canvas; 28 | this.texture = texture; 29 | this.handleWorkerMessage = this.handleWorkerMessage.bind(this); 30 | this.attachEvents(); 31 | } 32 | 33 | async run( 34 | zipUrl: string = '', 35 | commands: string[] = [], 36 | ) { 37 | return new Promise((resolve) => { 38 | this.terminate(); 39 | this.renderFrameData = null; 40 | this.imageData = null; 41 | this.zipUrl = zipUrl; 42 | this.commands = commands; 43 | this.frameId = 0; 44 | this.worker = new Worker(this.workerUrl); 45 | this.worker.addEventListener('message', this.handleWorkerMessage); 46 | this.startedPromise = resolve; 47 | }); 48 | } 49 | 50 | terminate() { 51 | this.worker?.removeEventListener('message', this.handleWorkerMessage); 52 | this.worker?.terminate(); 53 | } 54 | 55 | private handleWorkerMessage(event: MessageEvent) { 56 | 57 | const data = event.data; 58 | switch (data.target) { 59 | case 'loaded': { 60 | this.initWorker(); 61 | break; 62 | } 63 | case 'ready': { 64 | this.start(); 65 | break; 66 | } 67 | case 'custom': { 68 | if (this.rpcPromises[data.id]) { 69 | if (data.error) { 70 | this.rpcPromises[data.id][1](data.result); 71 | } else { 72 | this.rpcPromises[data.id][0](data.result); 73 | } 74 | delete this.rpcPromises[data.id]; 75 | } 76 | break; 77 | } 78 | case 'stdout': { 79 | this.print('DOSWorker: ' + data.content); 80 | break; 81 | } 82 | case 'stderr': { 83 | this.printErr('DOSWorker: ' + data.content); 84 | break; 85 | } 86 | case 'window': { 87 | (window)[data.method](); 88 | break; 89 | } 90 | case 'canvas': { 91 | switch (data.op) { 92 | case 'getContext': { 93 | this.ctx = this.canvas.getContext(data.type, data.attributes); 94 | break; 95 | } 96 | case 'resize': { 97 | this.canvas.width = data.width; 98 | this.canvas.height = data.height; 99 | if (this.ctx && this.ctx.getImageData) { 100 | this.imageData = this.ctx.getImageData(0, 0, data.width, data.height); 101 | } 102 | 103 | this.worker?.postMessage( 104 | { 105 | target: 'canvas', 106 | boundingClientRect: cloneObject(this.canvas.getBoundingClientRect()) 107 | } 108 | ); 109 | break; 110 | } 111 | case 'render': { 112 | if (this.renderFrameData) { 113 | // previous image was not rendered yet, just update image 114 | this.renderFrameData = data.image.data; 115 | } else { 116 | // previous image was rendered so update image and request another frame 117 | this.renderFrameData = data.image.data; 118 | 119 | setTimeout(this.renderFrame.bind(this)); 120 | // this.renderFrame(); 121 | } 122 | break; 123 | } 124 | case 'setObjectProperty': { 125 | (this.canvas)[data.object][data.property] = data.value; 126 | break; 127 | } 128 | default: 'eh?'; 129 | } 130 | break; 131 | } 132 | case 'tick': { 133 | this.frameId = data.id; 134 | this.worker?.postMessage({ target: 'tock', id: this.frameId }); 135 | break; 136 | } 137 | case 'setimmediate': { 138 | this.worker?.postMessage({ target: 'setimmediate' }); 139 | break; 140 | } 141 | 142 | } 143 | 144 | } 145 | 146 | private async initWorker() { 147 | this.worker?.postMessage( 148 | { target: 'gl', op: 'setPrefetched', preMain: true } 149 | ); 150 | this.worker?.postMessage({ 151 | target: 'worker-init', 152 | width: this.canvas.width, 153 | height: this.canvas.height, 154 | boundingClientRect: cloneObject(this.canvas.getBoundingClientRect()), 155 | URL: document.URL, 156 | currentScriptUrl: this.workerUrl, 157 | preMain: true, 158 | }); 159 | } 160 | 161 | private async start() { 162 | 163 | await this.callWorker( 164 | 'Module.FS.chdir', 165 | this.home 166 | ); 167 | 168 | await this.callWorker( 169 | 'Module.FS.createDataFile', 170 | this.home, 171 | 'dosbox.conf', dosboxConf, 172 | true, 173 | true, 174 | true 175 | ); 176 | 177 | if (this.zipUrl !== '') { 178 | const bytes = await this.getArchive(this.zipUrl); 179 | await this.callWorker( 180 | 'unzip', 181 | bytes 182 | ); 183 | } 184 | 185 | const args = ['run', '-c', 'mount c .', '-c', 'c:', ...this.commands]; 186 | await this.callWorker.apply(this, args); 187 | this.startedPromise(); 188 | } 189 | 190 | private async getArchive(url: string): Promise { 191 | return new Promise((resolve, reject) => { 192 | new Xhr( 193 | url, 194 | { 195 | responseType: "arraybuffer", 196 | fail: (msg) => reject(msg), 197 | success: (data: ArrayBuffer) => { 198 | const bytes = new Uint8Array(data); 199 | resolve(bytes); 200 | } 201 | }) 202 | }); 203 | } 204 | 205 | private async callWorker(...args: any[]) { 206 | 207 | const cmd = args.shift(); 208 | 209 | const id = Math.random().toString(36).substr(2, 9); 210 | return new Promise((resolve, reject) => { 211 | this.worker?.postMessage({ 212 | id, 213 | target: 'custom', 214 | cmd: cmd, 215 | args, 216 | }); 217 | this.rpcPromises[id] = [resolve, reject]; 218 | setTimeout(() => { 219 | delete this.rpcPromises[id]; 220 | reject(cmd + ' timeout'); 221 | }, 20000) 222 | }); 223 | }; 224 | 225 | private print(message: string) { 226 | console.log(message); 227 | } 228 | private printErr(message: string) { 229 | console.error(message); 230 | } 231 | private renderFrame() { 232 | 233 | const dst = this.imageData?.data; 234 | 235 | if (this.renderFrameData) { 236 | if (dst?.set) { 237 | this.texture.needsUpdate = true; 238 | dst.set(this.renderFrameData); 239 | } else { 240 | for (var i = 0; i < this.renderFrameData.length; i++) { 241 | // @ts-ignore 242 | dst[i] = this.renderFrameData[i]; 243 | } 244 | } 245 | } 246 | 247 | if (this.imageData) { 248 | this.ctx?.putImageData(this.imageData, 0, 0); 249 | } 250 | 251 | this.renderFrameData = null; 252 | } 253 | 254 | public sendKey(keyCode: number, type = 'keyup') { 255 | this.worker?.postMessage({ 256 | target: 'document', event: { 257 | keyCode, 258 | type 259 | } 260 | }); 261 | } 262 | 263 | private attachEvents() { 264 | // TODO: Move listeners to canvas 265 | ['keydown', 'keyup', 'keypress', 'blur', 'visibilitychange'] 266 | .forEach((eventName) => { 267 | document.addEventListener(eventName, (event: Event) => { 268 | const clonedEvent = cloneObject(event); 269 | this.worker?.postMessage({ target: 'document', event: clonedEvent }); 270 | 271 | if (shouldPreventDefault(event)) { 272 | event.preventDefault(); 273 | } 274 | }); 275 | }); 276 | 277 | ['unload'] 278 | .forEach((eventName) => { 279 | window.addEventListener(eventName, (event) => { 280 | this.worker?.postMessage({ target: 'window', event: cloneObject(event) }); 281 | }); 282 | }); 283 | /* 284 | ['mousedown', 'mouseup', 'mousemove', 'DOMMouseScroll', 'mousewheel', 'mouseout'] 285 | .forEach((eventName) => { 286 | this.canvas.addEventListener(eventName, (event: Event) => { 287 | this.worker?.postMessage({ target: 'canvas', event: cloneObject(event) }); 288 | event.preventDefault(); 289 | }, true); 290 | }); 291 | */ 292 | } 293 | 294 | 295 | } 296 | 297 | 298 | export default DosWorkerWrapper; 299 | -------------------------------------------------------------------------------- /src/VRDos.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Color, 4 | PerspectiveCamera, 5 | WebGLRenderer, 6 | sRGBEncoding, 7 | Camera, 8 | Vector3, 9 | Mesh, 10 | CanvasTexture, 11 | PlaneBufferGeometry, 12 | MeshPhongMaterial, 13 | DirectionalLight, 14 | AmbientLight, 15 | Light, 16 | FrontSide, 17 | MathUtils, 18 | Vector2, 19 | AnimationMixer, 20 | Clock, 21 | LoopOnce, 22 | AnimationClip, 23 | Group, 24 | LinearFilter, 25 | NearestFilter 26 | } from 'three'; 27 | 28 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 29 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 30 | import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'; 31 | import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js'; 32 | 33 | import Stats from 'stats.js'; 34 | 35 | import { Key } from 'ts-keycode-enum'; 36 | 37 | import DosWorkerWrapper from './DosWorkerWrapper'; 38 | 39 | enum GamePadAxis { 40 | x = 2, 41 | y = 3, 42 | } 43 | 44 | class VRDos { 45 | private scene: Scene | null = null; 46 | private camera: PerspectiveCamera | null = null; 47 | private renderer: WebGLRenderer | null = null; 48 | 49 | private initialized = false; 50 | private screenMeshName = 'SM_Monitor_Screen_0'; 51 | private gamepads: Gamepad[] = []; 52 | private pressThreshold = .5; 53 | private stats = new Stats(); 54 | private isDev = document.location.port === '1234'; 55 | 56 | private dosCanvas = document.createElement('canvas'); 57 | 58 | private dosTexture = new CanvasTexture(this.dosCanvas); 59 | 60 | private dos = new DosWorkerWrapper(this.dosCanvas, this.dosTexture); 61 | 62 | private animationMixer: AnimationMixer | null = null; 63 | private animationClips: AnimationClip[] = []; 64 | private clock = new Clock(); 65 | private isLoading = true; 66 | 67 | private userHeight = 0; 68 | private keyStatus: { [code: number]: string; } = {}; 69 | 70 | private gamePadButtonMap: { [vrButtonIndex: number]: number[]; } = { 71 | 0: [Key.Enter], // Trigger 72 | 1: [Key.Shift], // Sqeeze 73 | 3: [Key.Ctrl], // Joy fire 74 | 4: [Key.Space, Key.Shift, Key.Ctrl], // A 75 | 5: [Key.Ctrl, Key.Q, Key.Escape] // B 76 | } 77 | vrUser: Group = new Group(); 78 | 79 | get devicePixelRatio(): number { 80 | return window.devicePixelRatio; 81 | } 82 | 83 | get width(): number { 84 | return window.innerWidth; 85 | } 86 | 87 | get height(): number { 88 | return window.innerHeight; 89 | } 90 | 91 | get aspectRatio(): number { 92 | return (this.width / this.height) 93 | } 94 | 95 | private setLoading( 96 | loading: boolean, 97 | text: string = 'Loading...' 98 | ) { 99 | const ctx = this.dosCanvas.getContext('2d'); 100 | if (loading) { 101 | if (ctx && this.dosTexture) { 102 | this.dosCanvas.width = 512; 103 | this.dosCanvas.height = 512; 104 | ctx.font = 'bold 15px Verdana'; 105 | ctx.fillStyle = 'green'; 106 | ctx.fillText(text, 40, 40); 107 | } 108 | this.dosTexture.needsUpdate = true; 109 | this.isLoading = loading; 110 | } else { 111 | this.isLoading = loading; 112 | }; 113 | 114 | } 115 | 116 | public emulateKeyEvent(code: number, type: 'keydown' | 'keyup') { 117 | if (!this.keyStatus[code] && type === 'keydown') { 118 | this.dos.sendKey(code, type); 119 | this.keyStatus[code] = type; 120 | } else if (this.keyStatus[code] === 'keydown' && type === 'keyup') { 121 | this.dos.sendKey(code, type); 122 | delete this.keyStatus[code]; 123 | } 124 | } 125 | 126 | 127 | private processGamepadsInputs() { 128 | if (this.gamepads.length === 0) { 129 | return; 130 | } 131 | const gamepad = this.gamepads[this.gamepads.length - 1]; 132 | 133 | // Joystick 134 | for (let ai = 0; ai < gamepad.axes.length; ai++) { 135 | const value = gamepad.axes[ai]; 136 | if (ai === GamePadAxis.x) { 137 | if (value > this.pressThreshold) { 138 | this.emulateKeyEvent(Key.RightArrow, 'keydown'); 139 | } else if (value < -this.pressThreshold) { 140 | this.emulateKeyEvent(Key.LeftArrow, 'keydown'); 141 | } else { 142 | this.emulateKeyEvent(Key.RightArrow, 'keyup'); 143 | this.emulateKeyEvent(Key.LeftArrow, 'keyup'); 144 | } 145 | } 146 | if (ai === GamePadAxis.y) { 147 | if (value > this.pressThreshold) { 148 | this.emulateKeyEvent(Key.DownArrow, 'keydown'); 149 | } else if (value < -this.pressThreshold) { 150 | this.emulateKeyEvent(Key.UpArrow, 'keydown'); 151 | } else { 152 | this.emulateKeyEvent(Key.UpArrow, 'keyup'); 153 | this.emulateKeyEvent(Key.DownArrow, 'keyup'); 154 | } 155 | } 156 | } 157 | // Buttons 158 | for (let bi = 0; bi < gamepad.buttons.length; bi++) { 159 | const button = gamepad.buttons[bi]; 160 | const map = this.gamePadButtonMap[bi]; 161 | if (map) { 162 | map.forEach(key => { 163 | const action = button.pressed ? 'keydown' : 'keyup'; 164 | this.emulateKeyEvent(key, action); 165 | }); 166 | } 167 | }; 168 | } 169 | 170 | render(time: number, xrFrame: XRFrame) { 171 | 172 | 173 | if (this.animationMixer) { 174 | const deltaTime = this.clock.getDelta(); 175 | this.animationMixer.update(deltaTime); 176 | } 177 | 178 | if (this.userHeight === 0 && xrFrame) { 179 | const xrPose = xrFrame.getViewerPose(this.renderer?.xr.getReferenceSpace()); 180 | if (xrPose) { 181 | this.userHeight = xrPose?.transform.position.y; 182 | } 183 | } 184 | 185 | if (!this.isLoading) { 186 | this.fixTextureSize(this.dosCanvas, this.dosTexture); 187 | this.processGamepadsInputs(); 188 | } 189 | 190 | this.renderer?.render(this.scene, this.camera); 191 | 192 | if (this.isDev) { 193 | this.stats.update(); 194 | } 195 | } 196 | 197 | private createScene( 198 | color: Color = new Color('white') 199 | ): Scene { 200 | const scene = new Scene(); 201 | scene.castShadow = true; 202 | scene.background = color; 203 | return scene; 204 | } 205 | 206 | private createCamera( 207 | fov: number, 208 | ratio: number, 209 | initialPosition: Vector3, 210 | near = 0.1, 211 | far = 30 212 | ): PerspectiveCamera { 213 | const camera = new PerspectiveCamera(fov, ratio, near, far); 214 | camera.position.copy(initialPosition); 215 | return camera; 216 | } 217 | 218 | private createRenderer(): WebGLRenderer { 219 | // To enable WebGL2: 220 | // const canvas = document.createElement('canvas'); 221 | // const context = canvas.getContext('webgl2', { alpha: false }); 222 | const renderer = new WebGLRenderer({ 223 | antialias: true, 224 | alpha: false, 225 | }); 226 | renderer.xr.enabled = true; 227 | renderer.setPixelRatio(devicePixelRatio); 228 | renderer.setSize(window.innerWidth, window.innerHeight); 229 | renderer.outputEncoding = sRGBEncoding; 230 | return renderer; 231 | } 232 | 233 | private createOrbitControls( 234 | camera: Camera, 235 | container: HTMLElement, 236 | target: Vector3, 237 | ): OrbitControls { 238 | const controls = new OrbitControls(camera, container); 239 | controls.target.copy(target); 240 | controls.update(); 241 | return controls; 242 | } 243 | 244 | private createLights(): Light[] { 245 | var light = new DirectionalLight(0xFFFFFF); 246 | light.position.set(3, 5, 3) 247 | const alight = new AmbientLight(0xFFFFFF, 0.2) 248 | return [light, alight]; 249 | } 250 | 251 | private handleResize() { 252 | if (this.camera && this.renderer) { 253 | this.camera.aspect = this.aspectRatio; 254 | this.camera.updateProjectionMatrix(); 255 | this.renderer.setSize(this.width, this.height); 256 | } 257 | } 258 | 259 | private setupVRControllers() { 260 | 261 | /* 262 | const buildController = (xrInputSource: XRInputSource): Object3D | undefined => { 263 | let geometry, material; 264 | switch (xrInputSource.targetRayMode) { 265 | case 'tracked-pointer': 266 | geometry = new BufferGeometry(); 267 | geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 0, 0, - 1], 3)); 268 | geometry.setAttribute('color', new Float32BufferAttribute([0.5, 0.5, 0.5, 0, 0, 0], 3)); 269 | material = new LineBasicMaterial({ vertexColors: true, blending: AdditiveBlending }); 270 | return new Line(geometry, material); 271 | 272 | case 'gaze': 273 | geometry = new RingBufferGeometry(0.02, 0.04, 32).translate(0, 0, - 1); 274 | material = new MeshBasicMaterial({ opacity: 0.5, transparent: true }); 275 | return new Mesh(geometry, material); 276 | } 277 | } 278 | */ 279 | 280 | const xrManager = this.renderer?.xr; 281 | const scene = this.scene; 282 | 283 | if (xrManager && scene) { 284 | 285 | const controllers = [ 286 | xrManager.getController(0), 287 | xrManager.getController(1) 288 | ]; 289 | 290 | controllers.forEach(controller => { 291 | controller.addEventListener('connected', event => { 292 | 293 | const controller = event.data; 294 | if (controller.gamepad) { 295 | this.gamepads.push(controller.gamepad); 296 | } 297 | // controller.add(buildController(event.data)); 298 | }); 299 | controller.addEventListener('disconnected', event => { 300 | const controller = event.data; 301 | this.gamepads = this.gamepads.filter( 302 | gamepad => controller.gamepad?.id !== gamepad.id 303 | ); 304 | }); 305 | 306 | // this.vrUser.add(controller); 307 | 308 | }); 309 | 310 | 311 | const controllerModelFactory = new XRControllerModelFactory(); 312 | const controllerGrips = [ 313 | xrManager.getControllerGrip(0), 314 | xrManager.getControllerGrip(1) 315 | ] 316 | controllerGrips.forEach(grip => { 317 | grip.add(controllerModelFactory.createControllerModel(grip)); 318 | this.vrUser.add(grip); 319 | }); 320 | 321 | } 322 | 323 | } 324 | 325 | async init(domElement: HTMLElement | null) { 326 | if (this.initialized) { 327 | return; 328 | } 329 | if (this.isDev) { 330 | this.stats.showPanel(0); 331 | document.body.appendChild(this.stats.dom); 332 | } 333 | this.renderer = this.createRenderer(); 334 | 335 | const computerMonitorHeight = .7; 336 | 337 | this.scene = this.createScene(); 338 | 339 | const cameraPosition = new Vector3(0, computerMonitorHeight, 0); 340 | const fov = 65; 341 | this.camera = this.createCamera(fov, this.aspectRatio, cameraPosition); 342 | 343 | 344 | this.vrUser.position.set(0, 0, 0); 345 | this.vrUser.add(this.camera); 346 | this.scene.add(this.vrUser); 347 | 348 | 349 | this.createOrbitControls( 350 | this.camera, 351 | this.renderer.domElement, 352 | new Vector3(0, computerMonitorHeight, 0) 353 | ); 354 | 355 | 356 | 357 | // Adjust VR view height 358 | // @ts-ignore 359 | this.renderer.xr.addEventListener( 360 | 'sessionstart', 361 | () => { 362 | setTimeout(() => { 363 | const diff = computerMonitorHeight - this.userHeight; 364 | this.vrUser.position.set(0, diff, 0); 365 | }, 100); // FIXME 366 | } 367 | ); 368 | 369 | // @ts-ignore 370 | this.renderer.xr.addEventListener( 371 | 'sessionend', 372 | () => { 373 | this.userHeight = 0; 374 | this.vrUser.position.set(0, 0, 0); 375 | } 376 | ); 377 | 378 | this.createLights().forEach(light => this.scene?.add(light)); 379 | 380 | this.setupVRControllers(); 381 | 382 | if (domElement) { 383 | domElement.appendChild(this.renderer.domElement); 384 | domElement.appendChild( 385 | VRButton.createButton( 386 | this.renderer, 387 | { referenceSpaceType: 'local-floor' } 388 | ) 389 | ); 390 | } else { 391 | throw Error('Missing container dom element'); 392 | } 393 | window.addEventListener('resize', this.handleResize.bind(this)); 394 | 395 | const roomGLTF = await this.loadGLTF('room.glb'); 396 | const roomMesh = roomGLTF.scene.children[0]; 397 | this.scene.add(roomMesh); 398 | 399 | this.animationClips = roomGLTF.animations; 400 | 401 | const roomScreenMesh = this.scene.getObjectByName(this.screenMeshName); 402 | 403 | if (roomScreenMesh) { 404 | await this.attachDosScreen(roomScreenMesh); 405 | } else { 406 | throw new Error('Screen mesh not found' + this.screenMeshName); 407 | } 408 | // this.dosTexture.minFilter = LinearFilter; 409 | // this.dosTexture.magFilter = NearestFilter; 410 | // this.dosTexture.anisotropy = 8; 411 | 412 | this.renderer.setAnimationLoop(this.render.bind(this)); 413 | this.initialized = true; 414 | } 415 | 416 | public async run(archiveUrl: string, commands: string[] = []) { 417 | this.setLoading(true, `Please wait...`); 418 | await this.playIntro(); 419 | this.setLoading(true, `Booting ${archiveUrl}...`); 420 | await this.dos.run(archiveUrl, commands); 421 | this.setLoading(false); 422 | } 423 | 424 | private async playIntro() { 425 | return new Promise((resolve) => { 426 | if (this.scene && this.dosCanvas) { 427 | 428 | const mixer = this.animationMixer = new AnimationMixer(this.scene); 429 | const introClip = this.animationClips[0]; 430 | const action = mixer.clipAction(introClip); 431 | action.clampWhenFinished = true; 432 | action.loop = LoopOnce; 433 | mixer.addEventListener('finished', () => { 434 | this.animationMixer = null; 435 | resolve(); 436 | }) 437 | action.play(); 438 | } else { 439 | resolve(); 440 | } 441 | }) 442 | } 443 | 444 | private loadGLTF(path: string): Promise { 445 | const promise = new Promise((resolve, reject) => { 446 | const gltfLoader = new GLTFLoader(); 447 | gltfLoader.load( 448 | path, 449 | (gltf) => resolve(gltf), 450 | () => { }, 451 | (msg) => reject(msg) 452 | ); 453 | }); 454 | return >promise; 455 | } 456 | 457 | 458 | private fixTextureSize( 459 | canvas: HTMLCanvasElement, 460 | texture: CanvasTexture 461 | ) { 462 | // Other possible fixes for 'not power of 2' textures: 463 | // this.dosTexture.minFilter = LinearFilter; // looks ugly 464 | // or use WebGL 2 // Not supported by Safari 465 | if (!MathUtils.isPowerOfTwo(canvas.width)) { 466 | canvas.width = MathUtils.ceilPowerOfTwo(canvas.width); 467 | canvas.height = MathUtils.ceilPowerOfTwo(canvas.height); 468 | texture.offset = new Vector2(0, 0.2); 469 | texture.repeat.set(0.63, 0.8); 470 | } 471 | } 472 | 473 | private attachDosScreen(dosScreen: Mesh) { 474 | const material = new MeshPhongMaterial({ 475 | side: FrontSide, 476 | shininess: 40, 477 | specular: new Color(0xffffff), 478 | map: this.dosTexture, 479 | }); 480 | 481 | // TODO: Resize and position realScreen 482 | // automatically relative to dosScreen 483 | const geo = new PlaneBufferGeometry(.245, .23); 484 | const realScreen = new Mesh(geo, material); 485 | realScreen.position.set(-.25, .2, .1223); 486 | realScreen.rotateY(-Math.PI / 2); 487 | dosScreen.add(realScreen); 488 | 489 | } 490 | 491 | } 492 | 493 | export default VRDos; 494 | -------------------------------------------------------------------------------- /src/Xhr.ts: -------------------------------------------------------------------------------- 1 | interface XhrOptions { 2 | method?: string; 3 | success?: (response: any) => void; 4 | progress?: (total: number, loaded: number) => void; 5 | fail?: (url: string, status: number, message: string) => void; 6 | data?: string; 7 | responseType?: XMLHttpRequestResponseType; 8 | } 9 | 10 | export class Xhr { 11 | 12 | private resource: string; 13 | private options: XhrOptions; 14 | private xhr: XMLHttpRequest | null = null; 15 | private total: number = 0; 16 | private loaded: number = 0; 17 | 18 | constructor(url: string, options: XhrOptions) { 19 | this.resource = url; 20 | this.options = options; 21 | this.options.method = options.method || "GET"; 22 | 23 | if (this.options.method === "GET") { 24 | this.makeHttpRequest(); 25 | } 26 | } 27 | 28 | private makeHttpRequest() { 29 | this.xhr = new XMLHttpRequest(); 30 | this.xhr.open(this.options.method || "GET", this.resource, true); 31 | if (this.options.method === "POST") { 32 | this.xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 33 | } 34 | this.xhr.overrideMimeType("text/plain; charset=x-user-defined"); 35 | 36 | let progressListner; 37 | if (typeof (progressListner = this.xhr).addEventListener === "function") { 38 | progressListner.addEventListener("progress", (evt) => { 39 | this.total = evt.total; 40 | this.loaded = evt.loaded; 41 | if (this.options.progress) { 42 | return this.options.progress(evt.total, evt.loaded); 43 | } 44 | }); 45 | } 46 | 47 | let errorListener; 48 | if (typeof (errorListener = this.xhr).addEventListener === "function") { 49 | errorListener.addEventListener("error", (evt) => { 50 | if (this.options.fail) { 51 | this.options.fail(this.resource, (this.xhr as XMLHttpRequest).status, "connection problem"); 52 | return delete this.options.fail; 53 | } 54 | }); 55 | } 56 | this.xhr.onreadystatechange = () => { 57 | return this.onReadyStateChange(); 58 | }; 59 | if (this.options.responseType) { 60 | this.xhr.responseType = this.options.responseType; 61 | } 62 | this.xhr.send(this.options.data); 63 | } 64 | 65 | private onReadyStateChange() { 66 | const xhr = (this.xhr as XMLHttpRequest); 67 | if (xhr.readyState === 4) { 68 | if (xhr.status === 200) { 69 | if (this.options.success) { 70 | const total = Math.max(this.total, this.loaded); 71 | if (this.options.progress !== undefined) { 72 | this.options.progress(total, total); 73 | } 74 | 75 | return this.options.success(xhr.response); 76 | } 77 | } else if (this.options.fail) { 78 | this.options.fail(this.resource, xhr.status, "connection problem"); 79 | return delete this.options.fail; 80 | } 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/boot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/src/boot.gif -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VR-DOS 5 | 6 | 8 | 10 | 13 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
VR-DOS
22 | 23 |
24 |
25 | 27 |
28 | 29 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import VRDos from './VRDos'; 2 | import WebXRPolyfill from 'webxr-polyfill'; 3 | import { Key } from 'ts-keycode-enum'; 4 | 5 | (async () => { 6 | const container = document.getElementById('main'); 7 | (new WebXRPolyfill()); 8 | const vrDos = await new VRDos(); 9 | await vrDos.init(container); 10 | document.body.classList.remove('loading'); 11 | await vrDos.run('dos-hdd.zip', ['-c', 'c:\\doszip\\dz.exe']); 12 | document.body.classList.add('ready'); 13 | const controls = document.getElementById('controls'); 14 | const virtualButtons: { [buttonName: string]: number[]; } = { 15 | 16 | '←': [Key.LeftArrow], 17 | '↑': [Key.UpArrow], 18 | '↓': [Key.DownArrow], 19 | '→': [Key.RightArrow], 20 | 'Ent': [Key.Enter], 21 | 'Spc': [Key.Space], 22 | 'Ctr': [Key.Ctrl], 23 | 'Sft': [Key.Shift], 24 | 'Esc': [Key.Ctrl, Key.Q, Key.X, Key.Escape], 25 | } 26 | Object.keys(virtualButtons).forEach(keyName => { 27 | const button = document.createElement('button'); 28 | const keys = virtualButtons[keyName]; 29 | button.innerHTML = keyName; 30 | button.addEventListener('mousedown', 31 | () => 32 | keys.forEach(key => vrDos.emulateKeyEvent(key, 'keydown') 33 | ) 34 | ); 35 | button.addEventListener('mouseup', 36 | () => 37 | keys.forEach(key => vrDos.emulateKeyEvent(key, 'keyup') 38 | ) 39 | ); 40 | controls?.appendChild(button); 41 | }) 42 | 43 | })(); 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); 2 | 3 | html, 4 | body, 5 | #main { 6 | font-family: 'VT323', monospace; 7 | font-size: 20px; 8 | padding: 0; 9 | margin: 0; 10 | height: 100%; 11 | background-color: #202020; 12 | color: green; 13 | } 14 | #controls { 15 | display: none; 16 | } 17 | #controls button { 18 | color: gray; 19 | width: 35px; 20 | height: 35px; 21 | 22 | } 23 | .ready #controls { 24 | user-select: none; 25 | text-align: center; 26 | border-radius: 0; 27 | background-color: black; 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | padding: 10px; 32 | display: block; 33 | } 34 | 35 | #spinner{ 36 | position: absolute; 37 | top: 0; 38 | left: 0; 39 | right: 0; 40 | bottom: 0; 41 | background-color: black; 42 | } 43 | #main { 44 | display: block; 45 | cursor: grab; 46 | } 47 | 48 | #spinner { 49 | display: none; 50 | } 51 | #spinner div{ 52 | padding-left: 20px; 53 | } 54 | 55 | .loading #main, .loading #help { 56 | display: none; 57 | } 58 | 59 | .loading #spinner { 60 | display: block; 61 | } 62 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function cloneObject(event: any) { 2 | var ret = {}; 3 | for (var x in event) { 4 | if (x == x.toUpperCase()) continue; 5 | var prop = event[x]; 6 | if (typeof prop === 'number' || typeof prop === 'string') { 7 | ret[x] = prop; 8 | } 9 | } 10 | return ret; 11 | }; 12 | 13 | export function shouldPreventDefault(event: KeyboardEvent) { 14 | if ( 15 | event.type === 'keydown' && 16 | event.keyCode !== 8 && 17 | event.keyCode !== 9 18 | ) { 19 | return false; 20 | } else { 21 | return true; 22 | } 23 | }; 24 | 25 | export const dosboxConf = ` 26 | # This is the configurationfile for DOSBox 0.74. (Please use the latest version of DOSBox) 27 | # Lines starting with a # are commentlines and are ignored by DOSBox. 28 | # They are used to (briefly) document the effect of each option. 29 | 30 | [sdl] 31 | # fullscreen: Start dosbox directly in fullscreen. (Press ALT-Enter to go back) 32 | # fulldouble: Use double buffering in fullscreen. It can reduce screen flickering, but it can also result in a slow DOSBox. 33 | # fullresolution: What resolution to use for fullscreen: original or fixed size (e.g. 1024x768). 34 | # Using your monitor's native resolution with aspect=true might give the best results. 35 | # If you end up with small window on a large screen, try an output different from surface. 36 | # windowresolution: Scale the window to this size IF the output device supports hardware scaling. 37 | # (output=surface does not!) 38 | # output: What video system to use for output. 39 | # Possible values: surface, overlay, opengl, openglnb. 40 | # autolock: Mouse will automatically lock, if you click on the screen. (Press CTRL-F10 to unlock) 41 | # sensitivity: Mouse sensitivity. 42 | # waitonerror: Wait before closing the console if dosbox has an error. 43 | # priority: Priority levels for dosbox. Second entry behind the comma is for when dosbox is not focused/minimized. 44 | # pause is only valid for the second entry. 45 | # Possible values: lowest, lower, normal, higher, highest, pause. 46 | # mapperfile: File used to load/save the key/event mappings from. Resetmapper only works with the defaul value. 47 | # usescancodes: Avoid usage of symkeys, might not work on all operating systems. 48 | 49 | fullscreen=false 50 | fulldouble=false 51 | fullresolution=original 52 | windowresolution=original 53 | output=surface 54 | autolock=false 55 | sensitivity=100 56 | waitonerror=true 57 | priority=higher,normal 58 | mapperfile=mapper-jsdos.map 59 | usescancodes=true 60 | vsync=false 61 | 62 | [dosbox] 63 | # language: Select another language file. 64 | # machine: The type of machine tries to emulate. 65 | # Possible values: hercules, cga, tandy, pcjr, ega, vgaonly, svga_s3, svga_et3000, svga_et4000, svga_paradise, vesa_nolfb, vesa_oldvbe. 66 | # captures: Directory where things like wave, midi, screenshot get captured. 67 | # memsize: Amount of memory DOSBox has in megabytes. 68 | # This value is best left at its default to avoid problems with some games, 69 | # though few games might require a higher value. 70 | # There is generally no speed advantage when raising this value. 71 | 72 | language= 73 | machine=svga_s3 74 | captures=capture 75 | memsize=16 76 | 77 | [render] 78 | # frameskip: How many frames DOSBox skips before drawing one. 79 | # aspect: Do aspect correction, if your output method doesn't support scaling this can slow things down!. 80 | # scaler: Scaler used to enlarge/enhance low resolution modes. 81 | # If 'forced' is appended, then the scaler will be used even if the result might not be desired. 82 | # Possible values: none, normal2x, normal3x, advmame2x, advmame3x, advinterp2x, advinterp3x, hq2x, hq3x, 2xsai, super2xsai, supereagle, tv2x, tv3x, rgb2x, rgb3x, scan2x, scan3x. 83 | 84 | frameskip=0 85 | aspect=false 86 | scaler=none 87 | 88 | [cpu] 89 | # core: CPU Core used in emulation. auto will switch to dynamic if available and appropriate. 90 | # Possible values: auto, dynamic, normal, simple. 91 | # cputype: CPU Type used in emulation. auto is the fastest choice. 92 | # Possible values: auto, 386, 386_slow, 486_slow, pentium_slow, 386_prefetch. 93 | # cycles: Amount of instructions DOSBox tries to emulate each millisecond. 94 | # Setting this value too high results in sound dropouts and lags. 95 | # Cycles can be set in 3 ways: 96 | # 'auto' tries to guess what a game needs. 97 | # It usually works, but can fail for certain games. 98 | # 'fixed #number' will set a fixed amount of cycles. This is what you usually need if 'auto' fails. 99 | # (Example: fixed 4000). 100 | # 'max' will allocate as much cycles as your computer is able to handle. 101 | # 102 | # Possible values: auto, fixed, max. 103 | # cycleup: Amount of cycles to decrease/increase with keycombo.(CTRL-F11/CTRL-F12) 104 | # cycledown: Setting it lower than 100 will be a percentage. 105 | 106 | core=auto 107 | cputype=auto 108 | cycles=auto 109 | cycleup=10 110 | cycledown=20 111 | 112 | [mixer] 113 | # nosound: Enable silent mode, sound is still emulated though. 114 | # rate: Mixer sample rate, setting any device's rate higher than this will probably lower their sound quality. 115 | # Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716. 116 | # blocksize: Mixer block size, larger blocks might help sound stuttering but sound will also be more lagged. 117 | # Possible values: 1024, 2048, 4096, 8192, 512, 256. 118 | # prebuffer: How many milliseconds of data to keep on top of the blocksize. 119 | 120 | nosound=false 121 | rate=44100 122 | blocksize=1024 123 | prebuffer=20 124 | 125 | [midi] 126 | # mpu401: Type of MPU-401 to emulate. 127 | # Possible values: intelligent, uart, none. 128 | # mididevice: Device that will receive the MIDI data from MPU-401. 129 | # Possible values: default, win32, alsa, oss, coreaudio, coremidi, none. 130 | # midiconfig: Special configuration options for the device driver. This is usually the id of the device you want to use. 131 | # See the README/Manual for more details. 132 | 133 | mpu401=intelligent 134 | mididevice=default 135 | midiconfig= 136 | 137 | [sblaster] 138 | # sbtype: Type of Soundblaster to emulate. gb is Gameblaster. 139 | # Possible values: sb1, sb2, sbpro1, sbpro2, sb16, gb, none. 140 | # sbbase: The IO address of the soundblaster. 141 | # Possible values: 220, 240, 260, 280, 2a0, 2c0, 2e0, 300. 142 | # irq: The IRQ number of the soundblaster. 143 | # Possible values: 7, 5, 3, 9, 10, 11, 12. 144 | # dma: The DMA number of the soundblaster. 145 | # Possible values: 1, 5, 0, 3, 6, 7. 146 | # hdma: The High DMA number of the soundblaster. 147 | # Possible values: 1, 5, 0, 3, 6, 7. 148 | # sbmixer: Allow the soundblaster mixer to modify the DOSBox mixer. 149 | # oplmode: Type of OPL emulation. On 'auto' the mode is determined by sblaster type. All OPL modes are Adlib-compatible, except for 'cms'. 150 | # Possible values: auto, cms, opl2, dualopl2, opl3, none. 151 | # oplemu: Provider for the OPL emulation. compat might provide better quality (see oplrate as well). 152 | # Possible values: default, compat, fast. 153 | # oplrate: Sample rate of OPL music emulation. Use 49716 for highest quality (set the mixer rate accordingly). 154 | # Possible values: 44100, 49716, 48000, 32000, 22050, 16000, 11025, 8000. 155 | 156 | sbtype=sb16 157 | sbbase=220 158 | irq=7 159 | dma=1 160 | hdma=5 161 | sbmixer=true 162 | oplmode=auto 163 | oplemu=default 164 | oplrate=44100 165 | 166 | [gus] 167 | # gus: Enable the Gravis Ultrasound emulation. 168 | # gusrate: Sample rate of Ultrasound emulation. 169 | # Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716. 170 | # gusbase: The IO base address of the Gravis Ultrasound. 171 | # Possible values: 240, 220, 260, 280, 2a0, 2c0, 2e0, 300. 172 | # gusirq: The IRQ number of the Gravis Ultrasound. 173 | # Possible values: 5, 3, 7, 9, 10, 11, 12. 174 | # gusdma: The DMA channel of the Gravis Ultrasound. 175 | # Possible values: 3, 0, 1, 5, 6, 7. 176 | # ultradir: Path to Ultrasound directory. In this directory 177 | # there should be a MIDI directory that contains 178 | # the patch files for GUS playback. Patch sets used 179 | # with Timidity should work fine. 180 | 181 | gus=false 182 | gusrate=44100 183 | gusbase=240 184 | gusirq=5 185 | gusdma=3 186 | ultradir=C:\ULTRASND 187 | 188 | [speaker] 189 | # pcspeaker: Enable PC-Speaker emulation. 190 | # pcrate: Sample rate of the PC-Speaker sound generation. 191 | # Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716. 192 | # tandy: Enable Tandy Sound System emulation. For 'auto', emulation is present only if machine is set to 'tandy'. 193 | # Possible values: auto, on, off. 194 | # tandyrate: Sample rate of the Tandy 3-Voice generation. 195 | # Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716. 196 | # disney: Enable Disney Sound Source emulation. (Covox Voice Master and Speech Thing compatible). 197 | 198 | pcspeaker=true 199 | pcrate=44100 200 | tandy=auto 201 | tandyrate=44100 202 | disney=true 203 | 204 | [joystick] 205 | # joysticktype: Type of joystick to emulate: auto (default), none, 206 | # 2axis (supports two joysticks), 207 | # 4axis (supports one joystick, first joystick used), 208 | # 4axis_2 (supports one joystick, second joystick used), 209 | # fcs (Thrustmaster), ch (CH Flightstick). 210 | # none disables joystick emulation. 211 | # auto chooses emulation depending on real joystick(s). 212 | # (Remember to reset dosbox's mapperfile if you saved it earlier) 213 | # Possible values: auto, 2axis, 4axis, 4axis_2, fcs, ch, none. 214 | # timed: enable timed intervals for axis. Experiment with this option, if your joystick drifts (away). 215 | # autofire: continuously fires as long as you keep the button pressed. 216 | # swap34: swap the 3rd and the 4th axis. can be useful for certain joysticks. 217 | # buttonwrap: enable button wrapping at the number of emulated buttons. 218 | 219 | joysticktype=auto 220 | timed=true 221 | autofire=false 222 | swap34=false 223 | buttonwrap=false 224 | 225 | [serial] 226 | # serial1: set type of device connected to com port. 227 | # Can be disabled, dummy, modem, nullmodem, directserial. 228 | # Additional parameters must be in the same line in the form of 229 | # parameter:value. Parameter for all types is irq (optional). 230 | # for directserial: realport (required), rxdelay (optional). 231 | # (realport:COM1 realport:ttyS0). 232 | # for modem: listenport (optional). 233 | # for nullmodem: server, rxdelay, txdelay, telnet, usedtr, 234 | # transparent, port, inhsocket (all optional). 235 | # Example: serial1=modem listenport:5000 236 | # Possible values: dummy, disabled, modem, nullmodem, directserial. 237 | # serial2: see serial1 238 | # Possible values: dummy, disabled, modem, nullmodem, directserial. 239 | # serial3: see serial1 240 | # Possible values: dummy, disabled, modem, nullmodem, directserial. 241 | # serial4: see serial1 242 | # Possible values: dummy, disabled, modem, nullmodem, directserial. 243 | 244 | serial1=dummy 245 | serial2=dummy 246 | serial3=disabled 247 | serial4=disabled 248 | 249 | [dos] 250 | # xms: Enable XMS support. 251 | # ems: Enable EMS support. 252 | # umb: Enable UMB support. 253 | # keyboardlayout: Language code of the keyboard layout (or none). 254 | 255 | xms=true 256 | ems=true 257 | umb=true 258 | keyboardlayout=auto 259 | 260 | [ipx] 261 | # ipx: Enable ipx over UDP/IP emulation. 262 | 263 | ipx=false 264 | 265 | [autoexec] 266 | # Lines in this section will be run at startup. 267 | # You can put your MOUNT lines here. 268 | @echo off 269 | 270 | 271 | `; 272 | -------------------------------------------------------------------------------- /static/dos-hdd.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/static/dos-hdd.zip -------------------------------------------------------------------------------- /static/room.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/static/room.glb -------------------------------------------------------------------------------- /static/wdosbox-emterp.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonictruth/vr-dos/890e483a6cfbb6a31dde600cf2ae775fb82a55ed/static/wdosbox-emterp.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "strict": true, 5 | "module": "commonjs", 6 | "jsx": "preserve", 7 | "esModuleInterop": true, 8 | "sourceMap": false, 9 | "allowJs": false, 10 | "lib": [ 11 | "es2017", 12 | "dom" 13 | ], 14 | "rootDir": "src", 15 | "moduleResolution": "node" 16 | } 17 | } --------------------------------------------------------------------------------