mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-14 18:31:59 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c261079b67 | ||
|
|
6f337ac986 | ||
|
|
3189f3a8a2 | ||
|
|
5c1af3ae86 | ||
|
|
1ae2e5d546 | ||
|
|
7f3b23c5bf | ||
|
|
aa62c10700 | ||
|
|
86674b6090 | ||
|
|
90182076e6 | ||
|
|
e03e6148db | ||
|
|
59ee32806e | ||
|
|
9b32bf58d9 | ||
|
|
5bceb2e448 | ||
|
|
6918c425d6 | ||
|
|
8c3f608621 | ||
|
|
8b3117689e | ||
|
|
334a52b238 | ||
|
|
c11ae3caea | ||
|
|
1fbfca70b2 | ||
|
|
9c1ba25119 | ||
|
|
69d8b0d2be | ||
|
|
4f34053a84 | ||
|
|
5be27d7bab | ||
|
|
5ffbf4a106 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
out
|
||||
src/renderer/images
|
||||
src/images
|
||||
.DS_Store
|
||||
|
||||
34
README.md
34
README.md
@@ -1,11 +1,43 @@
|
||||
# Windows95
|
||||
# windows95
|
||||
|
||||
This is Windows 95, running in an Electron app. Yes, it's the full thing. I'm sorry.
|
||||
|
||||
## 💿⏬ [Download it here](https://github.com/felixrieseberg/windows95/releases).
|
||||
|
||||

|
||||
|
||||
## Does it work?
|
||||
Yes! Quite well, actually.
|
||||
|
||||
## Should this have been a native app?
|
||||
Absolutely.
|
||||
|
||||
## Does it run Doom (or my other favorite game)?
|
||||
You'll likely be better off with an actual virtualization app, but the short answer is yes. [Thanks to
|
||||
@DisplacedGamers](https://youtu.be/xDXqmdFxofM) I can recommend that you switch to a resolution of
|
||||
640x480 @ 256 colors before starting DOS games - just like in the good ol' days.
|
||||
|
||||
## How's the code?
|
||||
This only works well by accident and was mostly a joke. The code quality is accordingly.
|
||||
|
||||
## Credits
|
||||
|
||||
99.999% of the work was done over at [v86](https://github.com/copy/v86/) by Copy.
|
||||
|
||||
## Contributing
|
||||
|
||||
Before you can run this from source, you'll need the disk and state images. They're not part of the repo,
|
||||
but [you can download them here](https://mega.nz/#!euxygQBT!i03vtE4kYTgrZ1rjZa1gT2F8hvhcwIAgGBsY4ECjs0w).
|
||||
|
||||
Unpack the `images` folder into the `src/renderer` folder, creating this layout:
|
||||
|
||||
```
|
||||
./src/images/default-state.bin
|
||||
./src/images/windows95.img
|
||||
```
|
||||
|
||||
Once you've done so, run `npm install` and `npm start` to run your local build.
|
||||
|
||||
## License
|
||||
|
||||
This project is provided for educational purposes only. It is not affiliated with and has
|
||||
|
||||
@@ -3,7 +3,9 @@ const package = require('./package.json');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
asar: {
|
||||
unpack: '**/images/*.img'
|
||||
},
|
||||
icon: path.resolve(__dirname, 'assets', 'icon'),
|
||||
appBundleId: 'com.felixrieseberg.windows95',
|
||||
appCategoryType: 'public.app-category.developer-tools',
|
||||
@@ -20,7 +22,7 @@ module.exports = {
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
config: {
|
||||
name: 'windows98',
|
||||
name: 'windows95',
|
||||
authors: 'Felix Rieseberg',
|
||||
exe: 'windows95.exe',
|
||||
noMsi: true,
|
||||
|
||||
7
issue_template.md
Normal file
7
issue_template.md
Normal file
@@ -0,0 +1,7 @@
|
||||
⚠️ Thank you for reporting an issue!
|
||||
|
||||
Before we go any further, understand that I probably won't be able to fullfil feature requests.
|
||||
Feel free to report what feature you'd love to see, just don't get angry when I don't have
|
||||
time to implement it 🙇♂️
|
||||
|
||||
I will however _gladly_ help you make a pull request if you're willing to play with Javascript!
|
||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windows95",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1531,6 +1531,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"electron-is-dev": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz",
|
||||
"integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4="
|
||||
},
|
||||
"electron-osx-sign": {
|
||||
"version": "0.4.10",
|
||||
"resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz",
|
||||
@@ -2545,6 +2550,14 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"github-url-to-object": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/github-url-to-object/-/github-url-to-object-4.0.4.tgz",
|
||||
"integrity": "sha512-1Ri1pR8XTfzLpbtPz5MlW/amGNdNReuExPsbF9rxLsBfO1GH9RtDBamhJikd0knMWq3RTTQDbTtw0GGvvEAJEA==",
|
||||
"requires": {
|
||||
"is-url": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
@@ -2901,6 +2914,11 @@
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
|
||||
"dev": true
|
||||
},
|
||||
"is-url": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
|
||||
},
|
||||
"is-utf8": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
|
||||
@@ -4650,6 +4668,24 @@
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||
},
|
||||
"update-electron-app": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/update-electron-app/-/update-electron-app-1.3.0.tgz",
|
||||
"integrity": "sha512-OXfcmeenpjMyzXmadZ6NqxnrpPpiLji0sLUpXkexfX97XM8Gnk4iLovk4TlK4N8dzlETWdm9klgMmo9HpRbK7Q==",
|
||||
"requires": {
|
||||
"electron-is-dev": "^0.3.0",
|
||||
"github-url-to-object": "^4.0.4",
|
||||
"is-url": "^1.2.4",
|
||||
"ms": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"username": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/username/-/username-3.0.0.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windows95",
|
||||
"productName": "windows95",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Windows 95, in an app. I'm sorry.",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"appState",
|
||||
"V86Starter",
|
||||
"windows95"
|
||||
],
|
||||
@@ -29,7 +30,8 @@
|
||||
"dependencies": {
|
||||
"electron-default-menu": "^1.0.1",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"fs-extra": "^7.0.0"
|
||||
"fs-extra": "^7.0.0",
|
||||
"update-electron-app": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.22",
|
||||
|
||||
29
src/es6.js
Normal file
29
src/es6.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { protocol } = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
|
||||
const ES6_PATH = path.join(__dirname, 'renderer')
|
||||
|
||||
protocol.registerStandardSchemes(['es6'])
|
||||
|
||||
async function setupProtocol () {
|
||||
protocol.registerBufferProtocol('es6', async (req, cb) => {
|
||||
console.log(req)
|
||||
|
||||
try {
|
||||
const filePath = path.join(ES6_PATH, req.url.replace('es6://', ''))
|
||||
.replace('.js/', '.js')
|
||||
.replace('.js\\', '.js')
|
||||
|
||||
const fileContent = await fs.readFile(filePath)
|
||||
|
||||
cb({ mimeType: 'text/javascript', data: fileContent }) // eslint-disable-line
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupProtocol
|
||||
}
|
||||
14
src/index.js
14
src/index.js
@@ -3,11 +3,19 @@ const path = require('path')
|
||||
|
||||
const { clearCaches } = require('./cache')
|
||||
const { createMenu } = require('./menu')
|
||||
const { setupProtocol } = require('./es6')
|
||||
|
||||
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
|
||||
app.quit()
|
||||
}
|
||||
|
||||
if (app.isPackaged) {
|
||||
require('update-electron-app')({
|
||||
repo: 'felixrieseberg/windows95',
|
||||
updateInterval: '1 hour'
|
||||
})
|
||||
}
|
||||
|
||||
let mainWindow
|
||||
|
||||
const createWindow = () => {
|
||||
@@ -16,13 +24,13 @@ const createWindow = () => {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
useContentSize: true,
|
||||
nodeIntegration: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadURL(`file://${__dirname}/renderer/index.html?system=win98`)
|
||||
mainWindow.loadURL(`file://${__dirname}/renderer/index.html`)
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
@@ -30,8 +38,10 @@ const createWindow = () => {
|
||||
}
|
||||
|
||||
app.on('ready', async () => {
|
||||
await setupProtocol()
|
||||
await createMenu()
|
||||
await clearCaches()
|
||||
|
||||
createWindow()
|
||||
})
|
||||
|
||||
|
||||
43
src/menu.js
43
src/menu.js
@@ -1,8 +1,49 @@
|
||||
const { app, shell, Menu } = require('electron')
|
||||
const { app, shell, Menu, BrowserWindow } = require('electron')
|
||||
const defaultMenu = require('electron-default-menu')
|
||||
|
||||
function send (cmd) {
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
|
||||
if (windows[0]) {
|
||||
windows[0].webContents.send(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
async function createMenu () {
|
||||
const menu = defaultMenu(app, shell)
|
||||
.map((item) => {
|
||||
if (item.label === 'View') {
|
||||
item.submenu = item.submenu.filter((subItem) => {
|
||||
return subItem.label !== 'Reload'
|
||||
})
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
.filter((item) => {
|
||||
return item.label !== 'Edit'
|
||||
})
|
||||
|
||||
menu.splice(1, 0, {
|
||||
label: 'Machine',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Send Ctrl+Alt+Del',
|
||||
click: () => send('ctrlaltdel')
|
||||
},
|
||||
{
|
||||
label: 'Restart',
|
||||
click: () => send('restart')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Go to Disk Image',
|
||||
click: () => send('disk-image')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
|
||||
}
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
const { remote } = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
const { remote, shell, ipcRenderer } = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
const { STATE_PATH, getState, resetState } = require('./state')
|
||||
const { STATE_PATH, resetState, restoreState, saveState } = require('./state')
|
||||
|
||||
window.windows95 = {
|
||||
STATE_PATH,
|
||||
|
||||
restoreState,
|
||||
resetState,
|
||||
saveState,
|
||||
|
||||
async saveState () {
|
||||
return new Promise((resolve) => {
|
||||
if (!window.emulator || !window.emulator.save_state) {
|
||||
return resolve()
|
||||
}
|
||||
showDiskImage () {
|
||||
const imagePath = path.join(__dirname, 'images/windows95.img')
|
||||
.replace('app.asar', 'app.asar.unpacked')
|
||||
|
||||
window.emulator.save_state(async (error, newState) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
return
|
||||
}
|
||||
|
||||
await fs.outputFile(STATE_PATH, Buffer.from(newState))
|
||||
|
||||
console.log(`Saved state to ${STATE_PATH}`)
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
shell.showItemInFolder(imagePath)
|
||||
},
|
||||
|
||||
async restoreState () {
|
||||
try {
|
||||
window.emulator.restore_state(getState())
|
||||
} catch (error) {
|
||||
console.log(`Could not read state file. Maybe none exists?`, error)
|
||||
}
|
||||
},
|
||||
|
||||
quit () {
|
||||
remote.app.quit()
|
||||
}
|
||||
quit: () => remote.app.quit()
|
||||
}
|
||||
|
||||
ipcRenderer.on('ctrlaltdel', () => {
|
||||
if (!window.emulator || !window.emulator.is_running) return
|
||||
|
||||
window.emulator.keyboard_send_scancodes([
|
||||
0x1D, // ctrl
|
||||
0x38, // alt
|
||||
0x53, // delete
|
||||
|
||||
// break codes
|
||||
0x1D | 0x80,
|
||||
0x38 | 0x80,
|
||||
0x53 | 0x80
|
||||
])
|
||||
})
|
||||
|
||||
ipcRenderer.on('restart', () => {
|
||||
if (!window.emulator || !window.emulator.is_running) return
|
||||
|
||||
window.emulator.restart()
|
||||
})
|
||||
|
||||
ipcRenderer.on('disk-image', () => {
|
||||
windows95.showDiskImage()
|
||||
})
|
||||
|
||||
8
src/renderer/app-state.js
Normal file
8
src/renderer/app-state.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export function setupState () {
|
||||
window.appState = {
|
||||
cursorCaptured: false,
|
||||
floppyFile: null,
|
||||
bootFresh: false,
|
||||
infoInterval: null
|
||||
}
|
||||
}
|
||||
44
src/renderer/buttons.js
Normal file
44
src/renderer/buttons.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const $ = document.querySelector.bind(document)
|
||||
|
||||
export function setupButtons (start) {
|
||||
// Start
|
||||
$('.btn-start').addEventListener('click', () => start())
|
||||
|
||||
// Disk Image
|
||||
$('#show-disk-image').addEventListener('click', () => windows95.showDiskImage())
|
||||
|
||||
// Reset
|
||||
$('#reset').addEventListener('click', () => windows95.resetState())
|
||||
|
||||
$('#discard-state').addEventListener('click', () => {
|
||||
window.appState.bootFresh = true
|
||||
|
||||
start('win95')
|
||||
})
|
||||
|
||||
// Floppy
|
||||
$('#floppy').addEventListener('click', () => {
|
||||
$('#file-input').click()
|
||||
})
|
||||
|
||||
// Floppy (Hidden Input)
|
||||
$('#file-input').addEventListener('change', (event) => {
|
||||
window.appState.floppyFile = event.target.files && event.target.files.length > 0
|
||||
? event.target.files[0]
|
||||
: null
|
||||
|
||||
if (window.appState.floppyFile) {
|
||||
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${window.appState.floppyFile.path}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function toggleButtons (forceTo) {
|
||||
const buttonElements = $('#buttons')
|
||||
|
||||
if (buttonElements.style.display !== 'none' || forceTo === false) {
|
||||
buttonElements.style.display = 'none'
|
||||
} else {
|
||||
buttonElements.style.display = undefined
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@
|
||||
</head>
|
||||
|
||||
<body class="paused">
|
||||
<div id="status">
|
||||
Disk: <span id="disk-status">Idle</span>
|
||||
| CPU Speed: <span id="cpu-status">0</span>
|
||||
| <a onclick="document.querySelector('#status').style.display='none'">Hide</a>
|
||||
</div>
|
||||
<div id="buttons">
|
||||
<div id="start-buttons">
|
||||
<!-- <div class="btn" id="win98">Windows 98</div> -->
|
||||
@@ -26,14 +31,15 @@
|
||||
<div class="btn" id="reset">Reset Machine & Delete State</div>
|
||||
<div class="btn" id="floppy">Insert Floppy Disk</div>
|
||||
<div class="btn" id="discard-state">Discard State & Boot From Scratch</div>
|
||||
<div class="btn" id="show-disk-image">Show Disk Image</div>
|
||||
<input id="file-input" type='file'>
|
||||
</div>
|
||||
<div id="information">
|
||||
<p id="floppy-path"></p>
|
||||
<p>You can insert a floppy disk image with the ".img" format.</p>
|
||||
<p>
|
||||
Boot the machine from scratch if you've inserted a new floppy disk to
|
||||
make sure that Windows can load it.
|
||||
Boot the machine from scratch if you've inserted a new floppy disk
|
||||
or if you've changed the disk image.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,8 +47,8 @@
|
||||
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
|
||||
<canvas style="display: none"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
require('./renderer.js')
|
||||
<script type="module">
|
||||
import("es6://renderer.js")
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
36
src/renderer/info.js
Normal file
36
src/renderer/info.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const $ = document.querySelector.bind(document)
|
||||
|
||||
export function setupInfo () {
|
||||
const diskStatus = $('#disk-status')
|
||||
const cpuStatus = $('#cpu-status')
|
||||
let lastCounter = 0
|
||||
let lastTick = 0
|
||||
|
||||
window.emulator.add_listener('ide-read-start', () => {
|
||||
diskStatus.innerHTML = 'Read'
|
||||
})
|
||||
|
||||
window.emulator.add_listener('ide-read-end', () => {
|
||||
diskStatus.innerHTML = 'Idle'
|
||||
})
|
||||
|
||||
window.emulator.add_listener('ide-write-end', () => {
|
||||
diskStatus.innerHTML = 'Idle'
|
||||
})
|
||||
|
||||
window.emulator.add_listener('screen-set-size-graphical', (...args) => {
|
||||
console.log(...args)
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
const instructionCounter = window.emulator.get_instruction_counter()
|
||||
const ips = instructionCounter - lastCounter
|
||||
const deltaTime = now - lastTick
|
||||
|
||||
lastTick = now
|
||||
lastCounter = instructionCounter
|
||||
|
||||
cpuStatus.innerHTML = Math.round(ips / deltaTime)
|
||||
}, 500)
|
||||
}
|
||||
41
src/renderer/listeners.js
Normal file
41
src/renderer/listeners.js
Normal file
@@ -0,0 +1,41 @@
|
||||
export function setupCloseListener () {
|
||||
window.appState.isQuitting = false
|
||||
|
||||
const handleClose = async () => {
|
||||
await windows95.saveState()
|
||||
window.appState.isQuitting = true
|
||||
windows95.quit()
|
||||
}
|
||||
|
||||
window.onbeforeunload = (event) => {
|
||||
if (window.appState.isQuitting) return
|
||||
|
||||
handleClose()
|
||||
event.preventDefault()
|
||||
event.returnValue = false
|
||||
}
|
||||
}
|
||||
|
||||
export function setupEscListener () {
|
||||
document.onkeydown = function (evt) {
|
||||
evt = evt || window.event
|
||||
if (evt.keyCode === 27) {
|
||||
if (window.appState.cursorCaptured) {
|
||||
window.appState.cursorCaptured = false
|
||||
document.exitPointerLock()
|
||||
} else {
|
||||
window.appState.cursorCaptured = true
|
||||
window.emulator.lock_mouse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setupClickListener () {
|
||||
document.addEventListener('click', () => {
|
||||
if (!window.appState.cursorCaptured) {
|
||||
window.appState.cursorCaptured = true
|
||||
window.emulator.lock_mouse()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,24 +1,16 @@
|
||||
const $ = document.querySelector.bind(document)
|
||||
const $$ = document.querySelectorAll.bind(document)
|
||||
import { setupState } from 'es6://app-state.js'
|
||||
import { setupClickListener, setupEscListener, setupCloseListener } from 'es6://listeners.js'
|
||||
import { toggleButtons, setupButtons } from 'es6://buttons.js'
|
||||
import { setupInfo } from 'es6://info.js'
|
||||
|
||||
const BUTTONS = $('#buttons')
|
||||
setupState()
|
||||
|
||||
let cursorCaptured = false
|
||||
let floppyFile = null
|
||||
let bootFresh = false
|
||||
|
||||
const OPTIONS = {
|
||||
win95: {
|
||||
hda: {
|
||||
url: './images/windows95.img',
|
||||
async: true,
|
||||
size: 242049024
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main (id) {
|
||||
const opts = Object.assign({
|
||||
/**
|
||||
* The main method executing the VM.
|
||||
*/
|
||||
async function main () {
|
||||
// New v86 instance
|
||||
window.emulator = new V86Starter({
|
||||
memory_size: 64 * 1024 * 1024,
|
||||
screen_container: document.getElementById('emulator'),
|
||||
bios: {
|
||||
@@ -27,110 +19,48 @@ async function main (id) {
|
||||
vga_bios: {
|
||||
url: './bios/vgabios.bin'
|
||||
},
|
||||
hda: {
|
||||
url: '../images/windows95.img',
|
||||
async: true,
|
||||
size: 242049024
|
||||
},
|
||||
fda: {
|
||||
buffer: floppyFile || undefined
|
||||
buffer: window.appState.floppyFile || undefined
|
||||
},
|
||||
boot_order: 0x132
|
||||
}, OPTIONS[id])
|
||||
})
|
||||
|
||||
// New v86 instance
|
||||
window.emulator = new V86Starter(opts)
|
||||
// High DPI support
|
||||
if (navigator.userAgent.includes('Windows')) {
|
||||
const scale = window.devicePixelRatio
|
||||
|
||||
// Restore state. We can't do this right away.
|
||||
window.emulator.screen_adapter.set_scale(scale, scale)
|
||||
}
|
||||
|
||||
// Restore state. We can't do this right away
|
||||
// and randomly chose 500ms as the appropriate
|
||||
// wait time (lol)
|
||||
setTimeout(async () => {
|
||||
if (!bootFresh) {
|
||||
await windows95.restoreState()
|
||||
if (!window.appState.bootFresh) {
|
||||
windows95.restoreState()
|
||||
}
|
||||
|
||||
cursorCaptured = true
|
||||
setupInfo()
|
||||
|
||||
window.appState.cursorCaptured = true
|
||||
window.emulator.lock_mouse()
|
||||
window.emulator.run()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function start (id) {
|
||||
BUTTONS.remove()
|
||||
function start () {
|
||||
document.body.className = ''
|
||||
main(id)
|
||||
}
|
||||
|
||||
function setupButtons () {
|
||||
// Start
|
||||
$$('.btn-start').forEach((btn) => {
|
||||
btn.addEventListener('click', () => start(btn.id))
|
||||
})
|
||||
|
||||
// Reset
|
||||
$('#reset').addEventListener('click', () => {
|
||||
if (window.emulator.stop) {
|
||||
window.emulator.stop()
|
||||
}
|
||||
|
||||
windows95.resetState()
|
||||
|
||||
if (window.emulator.run) {
|
||||
window.emulator.run()
|
||||
}
|
||||
|
||||
$('#reset').disabled = true
|
||||
})
|
||||
|
||||
$('#discard-state').addEventListener('click', () => {
|
||||
bootFresh = true
|
||||
|
||||
start('win95')
|
||||
})
|
||||
|
||||
// Floppy
|
||||
$('#floppy').addEventListener('click', () => {
|
||||
$('#file-input').click()
|
||||
})
|
||||
|
||||
// Floppy (Hidden Input)
|
||||
$('#file-input').addEventListener('change', (event) => {
|
||||
floppyFile = event.target.files && event.target.files.length > 0
|
||||
? event.target.files[0]
|
||||
: null
|
||||
|
||||
if (floppyFile) {
|
||||
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${floppyFile.path}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setupEscListener () {
|
||||
document.onkeydown = function (evt) {
|
||||
evt = evt || window.event
|
||||
if (evt.keyCode === 27) {
|
||||
if (cursorCaptured) {
|
||||
cursorCaptured = false
|
||||
document.exitPointerLock()
|
||||
} else {
|
||||
cursorCaptured = true
|
||||
window.emulator.lock_mouse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupCloseListener () {
|
||||
let isQuitting = false
|
||||
|
||||
const handleClose = async () => {
|
||||
await windows95.saveState()
|
||||
isQuitting = true
|
||||
windows95.quit()
|
||||
}
|
||||
|
||||
window.onbeforeunload = (event) => {
|
||||
if (isQuitting) return
|
||||
|
||||
handleClose()
|
||||
event.preventDefault()
|
||||
event.returnValue = false
|
||||
}
|
||||
toggleButtons(false)
|
||||
setupClickListener()
|
||||
main()
|
||||
}
|
||||
|
||||
setupEscListener()
|
||||
setupCloseListener()
|
||||
setupButtons()
|
||||
setupButtons(start)
|
||||
|
||||
@@ -16,6 +16,28 @@ body.paused {
|
||||
font-family: Courier;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#status {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
left: calc(50vw - 110px);
|
||||
background: white;
|
||||
text-align: center;
|
||||
font-family: Courier;
|
||||
font-size: 10px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
#floppy-path {
|
||||
background: beige;
|
||||
padding: 5px;
|
||||
|
||||
58
src/state.js
58
src/state.js
@@ -2,9 +2,15 @@ const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { remote } = require('electron')
|
||||
|
||||
const DEFAULT_PATH = path.join(__dirname, 'renderer/images/default-state.bin')
|
||||
const DEFAULT_PATH = path.join(__dirname, 'images/default-state.bin')
|
||||
const STATE_PATH = path.join(remote.app.getPath('userData'), 'state.bin')
|
||||
|
||||
/**
|
||||
* Returns the current machine's state - either what
|
||||
* we have saved or alternatively the default state.
|
||||
*
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
function getState () {
|
||||
const statePath = fs.existsSync(STATE_PATH)
|
||||
? STATE_PATH
|
||||
@@ -13,12 +19,58 @@ function getState () {
|
||||
return fs.readFileSync(statePath).buffer
|
||||
}
|
||||
|
||||
function resetState () {
|
||||
fs.removeSync(STATE_PATH)
|
||||
/**
|
||||
* Resets a saved state by simply deleting it.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function resetState () {
|
||||
if (fs.existsSync(STATE_PATH)) {
|
||||
return fs.remove(STATE_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current VM's state.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function saveState () {
|
||||
return new Promise((resolve) => {
|
||||
if (!window.emulator || !window.emulator.save_state) {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
window.emulator.save_state(async (error, newState) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
return
|
||||
}
|
||||
|
||||
await fs.outputFile(STATE_PATH, Buffer.from(newState))
|
||||
|
||||
console.log(`Saved state to ${STATE_PATH}`)
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the VM's state.
|
||||
*/
|
||||
function restoreState () {
|
||||
try {
|
||||
window.emulator.restore_state(getState())
|
||||
} catch (error) {
|
||||
console.log(`Could not read state file. Maybe none exists?`, error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
STATE_PATH,
|
||||
saveState,
|
||||
restoreState,
|
||||
resetState,
|
||||
getState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user