26 Commits

Author SHA1 Message Date
Felix Rieseberg
c261079b67 🔧 Read file on Windows 2018-08-25 01:06:09 -07:00
Felix Rieseberg
6f337ac986 🔧 Hide status 2018-08-24 23:21:12 -07:00
Felix Rieseberg
3189f3a8a2 🔧 Add some features 2018-08-24 23:01:33 -07:00
Felix Rieseberg
5c1af3ae86 🔧 Successfully show disk image when packaged 2018-08-24 21:33:51 -07:00
Felix Rieseberg
1ae2e5d546 📝 Update readme 2018-08-24 21:17:26 -07:00
Felix Rieseberg
7f3b23c5bf 📦 Update version number to 1.2 2018-08-24 21:16:56 -07:00
Felix Rieseberg
aa62c10700 🔧 Fancy things up a bit 2018-08-24 20:55:18 -07:00
Felix Rieseberg
86674b6090 🔧 Move to ES6 modules 2018-08-24 20:55:03 -07:00
Felix Rieseberg
90182076e6 📝 Update forge config 2018-08-24 20:03:32 -07:00
Felix Rieseberg
e03e6148db 📝 Make download text more obvious, I guess 2018-08-24 09:49:52 -07:00
Felix Rieseberg
59ee32806e 📝 Issue template 2018-08-23 23:11:48 -07:00
Felix Rieseberg
9b32bf58d9 📝 Fix typo 2018-08-23 21:55:42 -07:00
Felix Rieseberg
5bceb2e448 📝 Document Doom 2018-08-23 21:55:16 -07:00
Felix Rieseberg
6918c425d6 📝 Fix typo 2018-08-23 21:24:01 -07:00
Felix Rieseberg
8c3f608621 📝 Update readme 2018-08-23 16:23:08 -07:00
Felix Rieseberg
8b3117689e 🔧 Don't set scale on non-Windows 2018-08-23 16:16:40 -07:00
Felix Rieseberg
334a52b238 📝 Add issue template 2018-08-23 16:16:23 -07:00
Felix Rieseberg
c11ae3caea Merge pull request #16 from benwiley4000/click-listener
Clicking in the window re-captures the mouse.
2018-08-23 16:00:51 -07:00
Felix Rieseberg
1fbfca70b2 Merge branch 'master' into click-listener 2018-08-23 15:56:49 -07:00
Felix Rieseberg
9c1ba25119 Merge pull request #17 from shanselman/master
high-dpi support
2018-08-23 15:56:00 -07:00
Felix Rieseberg
0c22b05a8d 🔧 Add Floppy Support 2018-08-23 15:35:16 -07:00
Felix Rieseberg
f0449ed7fe 📦 Bump version 2018-08-23 15:11:07 -07:00
Scott Hanselman
69d8b0d2be high-dpi support 2018-08-23 15:06:54 -07:00
Ben Wiley
4f34053a84 Clicking in the window re-captures the mouse. 2018-08-23 17:59:13 -04:00
Felix Rieseberg
5be27d7bab 📝 Another readme update 2018-08-23 07:50:32 -07:00
Felix Rieseberg
5ffbf4a106 📝 Update readme 2018-08-23 07:47:46 -07:00
18 changed files with 502 additions and 127 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
node_modules
out
src/renderer/images
src/images
.DS_Store

View File

@@ -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).
![Screenshot](https://user-images.githubusercontent.com/1426799/44532591-4ceb3680-a6a8-11e8-8c2c-bc29f3bfdef7.png)
## 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

View File

@@ -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
View 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
View File

@@ -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",

View File

@@ -1,7 +1,7 @@
{
"name": "windows95",
"productName": "windows95",
"version": "1.0.0",
"version": "1.2.0",
"description": "Windows 95, in an app. I'm sorry.",
"main": "src/index.js",
"scripts": {
@@ -18,7 +18,11 @@
"forge": "./forge.config.js"
},
"standard": {
"globals": [ "V86Starter", "windows95" ],
"globals": [
"appState",
"V86Starter",
"windows95"
],
"ignore": [
"/src/renderer/lib/*.js"
]
@@ -26,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
View 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
}

View File

@@ -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()
})

View File

@@ -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))
}

View File

@@ -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()
})

View 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
View 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
}
}

View File

@@ -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> -->
@@ -24,14 +29,26 @@
</div>
<div id="other-buttons">
<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
or if you've changed the disk image.
</p>
</div>
</div>
<div id="emulator" style="height: 100vh; width: 100vw">
<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
View 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
View 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()
}
})
}

View File

@@ -1,19 +1,16 @@
const BUTTONS = document.querySelector('#buttons')
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'
let cursorCaptured = false
setupState()
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: {
@@ -21,79 +18,49 @@ async function main (id) {
},
vga_bios: {
url: './bios/vgabios.bin'
}
}, OPTIONS[id])
},
hda: {
url: '../images/windows95.img',
async: true,
size: 242049024
},
fda: {
buffer: window.appState.floppyFile || undefined
},
boot_order: 0x132
})
// 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 () => {
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 setupButtons () {
document.querySelectorAll('.btn-start').forEach((btn) => {
btn.addEventListener('click', () => {
BUTTONS.remove()
document.body.className = ''
main(btn.id)
})
})
function start () {
document.body.className = ''
document.querySelector('#reset').addEventListener('click', () => {
if (window.emulator.stop) {
window.emulator.stop()
}
windows95.resetState()
if (window.emulator.run) {
window.emulator.run()
}
document.querySelector('#reset').disabled = true
})
}
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)

View File

@@ -1,8 +1,54 @@
html, body {
margin: 0;
padding: 0;
}
body {
background: #000;
/* cursor: none; */
}
body.paused > #emulator {
display: none;
}
body.paused {
background: #008080;
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;
}
#information {
text-align: center;
position: absolute;
width: 100vw;
bottom: 50px;
font-size: 18px;
}
#emulator {
@@ -13,7 +59,7 @@ html, body {
margin: auto;
}
body.paused > #emulator {
#file-input {
display: none;
}

View File

@@ -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
}