Adding eslint, doing eslint fixes and slowly implementing save backup from server.

This commit is contained in:
Sergiotarxz 2023-03-28 19:35:23 +02:00
parent 67f64438b3
commit 10f04ce059
11 changed files with 490 additions and 391 deletions

37
.eslintrc.js Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
env: {
browser: true,
es2021: true
},
extends: [
'plugin:react/recommended',
'standard-with-typescript'
],
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: 'tsconfig.json'
},
plugins: [
'react',
'no-relative-import-paths'
],
rules: {
indent: ['error', 4, { SwitchCase: 1 }],
'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: true }],
'@typescript-eslint/indent': ['error', 4],
'react/jsx-indent': ['error', 4],
'react/jsx-indent-props': ['error', 4]
},
settings: {
'import/resolver': {
typescript: {
project: [
'tsconfig.json'
]
}
}
}
}

View File

@ -1,233 +1,241 @@
import * as React from 'react'; import * as React from 'react'
import {HOME_BUTTON_IMAGE} from '/constants'; import { HOME_BUTTON_IMAGE } from '@msgba/constants'
import {sendKeyDown} from '/packet'; import { sendKeyDown } from '@msgba/packet'
export interface OverlayControlsProps { export interface OverlayControlsProps {
firstMenuElement: React.RefObject<HTMLAnchorElement>, firstMenuElement: React.RefObject<HTMLAnchorElement>
setHiddenMenu: (c: boolean) => void, setHiddenMenu: (c: boolean) => void
webSocket: WebSocket | null; webSocket: WebSocket | null
}; };
interface ControlMap { interface ControlValue {
[id: string]: {key: number, ref: React.RefObject<HTMLAnchorElement>, sym: string, classes: string, transformX?: number, transformY?: number} key: number
}; ref: React.RefObject<HTMLAnchorElement>
sym: string
classes: string
transformX?: number
transformY?: number
}
export default function OverlayControls({firstMenuElement, setHiddenMenu, webSocket}: OverlayControlsProps) { type ControlMap = Record<string, ControlValue>
function showOverlayMenu() {
setHiddenMenu(false); export default function OverlayControls ({ firstMenuElement, setHiddenMenu, webSocket }: OverlayControlsProps): JSX.Element {
function showOverlayMenu (): void {
setHiddenMenu(false)
setTimeout(() => { setTimeout(() => {
if (firstMenuElement.current == null) { if (firstMenuElement.current == null) {
return; return
} }
firstMenuElement.current.focus(); firstMenuElement.current.focus()
}, 100); }, 100)
} }
const [onGoingTouches, setOnGoingTouches] = React.useState<{[id: string]: number}>({}); const onGoingTouches = new Map<number, number>()
function mouseDown(e: React.MouseEvent<HTMLAnchorElement> | React.TouchEvent<HTMLAnchorElement>, key: number) {
e.preventDefault(); function mouseDown (e: React.MouseEvent<HTMLAnchorElement>, key: number): void {
e.preventDefault()
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return
} }
sendKeyDown(webSocket, true, key); sendKeyDown(webSocket, true, key)
} }
function mouseUp(e: React.MouseEvent<HTMLAnchorElement> | React.TouchEvent<HTMLAnchorElement>, key: number) { function mouseUp (e: React.MouseEvent<HTMLAnchorElement>, key: number): void {
e.preventDefault(); e.preventDefault()
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return
} }
sendKeyDown(webSocket, false, key); sendKeyDown(webSocket, false, key)
} }
const controls: ControlMap = {}; const controls: ControlMap = {}
controls.a = { controls.a = {
ref: React.useRef<HTMLAnchorElement|null>(null), ref: React.useRef<HTMLAnchorElement | null>(null),
key: 0, key: 0,
sym: 'A', sym: 'A',
classes: 'control-a control-button-a-b control control-button', classes: 'control-a control-button-a-b control control-button'
}; }
controls.b = { controls.b = {
ref: React.useRef<HTMLAnchorElement|null>(null), ref: React.useRef<HTMLAnchorElement | null>(null),
key: 1, key: 1,
sym: 'B', sym: 'B',
transformX: 50, transformX: 50,
classes: 'control-b control-button-a-b control control-button', classes: 'control-b control-button-a-b control control-button'
}; }
controls.l = { controls.l = {
key: 2, key: 2,
sym: 'L', sym: 'L',
classes: 'control-l control-button-l-r control', classes: 'control-l control-button-l-r control',
ref: React.useRef<HTMLAnchorElement|null>(null), ref: React.useRef<HTMLAnchorElement | null>(null)
}; }
controls.r = { controls.r = {
key: 3, key: 3,
sym: 'R', sym: 'R',
classes: 'control-r control-button-l-r control', classes: 'control-r control-button-l-r control',
ref: React.useRef<HTMLAnchorElement|null>(null), ref: React.useRef<HTMLAnchorElement | null>(null)
}; }
controls.start = { controls.start = {
ref: React.useRef<HTMLAnchorElement|null>(null),
key: 4, key: 4,
sym: 'START', sym: 'START',
classes: 'control-start control-button-start-select control', classes: 'control-start control-button-start-select control',
transformX: 25, transformX: 25,
}; ref: React.useRef<HTMLAnchorElement | null>(null)
}
controls.select = { controls.select = {
ref: React.useRef<HTMLAnchorElement|null>(null),
key: 5, key: 5,
sym: 'SEL', sym: 'SEL',
classes: 'control-select control-button-start-select control', classes: 'control-select control-button-start-select control',
transformX: -25, transformX: -25,
}; ref: React.useRef<HTMLAnchorElement | null>(null)
}
controls.up = { controls.up = {
ref: React.useRef<HTMLAnchorElement|null>(null),
key: 6, key: 6,
sym: '^', sym: '^',
transformX: 100, transformX: 100,
classes: 'control-up control control-pad-button', classes: 'control-up control control-pad-button',
ref: React.useRef<HTMLAnchorElement | null>(null)
} }
controls.down = { controls.down = {
ref: React.useRef<HTMLAnchorElement|null>(null),
key: 7, key: 7,
sym: 'v', sym: 'v',
transformX: 100, transformX: 100,
classes: 'control-down control control-pad-button', classes: 'control-down control control-pad-button',
ref: React.useRef<HTMLAnchorElement | null>(null)
} }
controls.left = { controls.left = {
ref: React.useRef<HTMLAnchorElement|null>(null),
key: 8, key: 8,
sym: '<', sym: '<',
classes: 'control-left control control-pad-button', classes: 'control-left control control-pad-button',
ref: React.useRef<HTMLAnchorElement | null>(null)
} }
controls.right = { controls.right = {
ref: React.useRef<HTMLAnchorElement|null>(null),
key: 9, key: 9,
sym: '>', sym: '>',
transformX: 200, transformX: 200,
classes: 'control-right control control-pad-button', classes: 'control-right control control-pad-button',
ref: React.useRef<HTMLAnchorElement | null>(null)
} }
function determineKey(e: React.TouchEvent<HTMLDivElement>, touch: React.Touch): number | null { function determineKey (e: React.TouchEvent<HTMLDivElement>, touch: React.Touch): number | null {
const x = touch.pageX; const x = touch.pageX
const y = touch.pageY; const y = touch.pageY
for (const control of Object.keys(controls)) { for (const control of Object.keys(controls)) {
const ref = controls[control].ref.current; const ref = controls[control].ref.current
if (ref == null) { if (ref == null) {
console.log('No ref found'); console.log('No ref found')
continue; continue
} }
let top = ref.getBoundingClientRect().top + document.documentElement.scrollTop; const top = ref.getBoundingClientRect().top + document.documentElement.scrollTop
const currentControl = controls[control]; const currentControl = controls[control]
const transformX = currentControl.transformX; const transformX = currentControl.transformX
const transformY = currentControl.transformY; const transformY = currentControl.transformY
let offsetLeft = ref.offsetLeft; let offsetLeft = ref.offsetLeft
const offsetWidth = ref.offsetWidth; const offsetWidth = ref.offsetWidth
let offsetTop = top; let offsetTop = top
const offsetHeight = ref.offsetHeight; const offsetHeight = ref.offsetHeight
if (transformX != null) { if (transformX != null) {
offsetLeft += offsetWidth * (transformX / 100); offsetLeft += offsetWidth * (transformX / 100)
} }
if (transformY != null) { if (transformY != null) {
offsetTop += offsetHeight * (transformY / 100); offsetTop += offsetHeight * (transformY / 100)
} }
console.log(x, y, offsetLeft, offsetTop, offsetWidth, offsetHeight);
if (x >= offsetLeft && x <= offsetLeft + offsetWidth && y >= offsetTop && y <= offsetTop + offsetHeight) { if (x >= offsetLeft && x <= offsetLeft + offsetWidth && y >= offsetTop && y <= offsetTop + offsetHeight) {
return controls[control].key; return controls[control].key
} }
} }
return null; return null
} }
function touchStartControls(e: React.TouchEvent<HTMLDivElement>) { function touchStartControls (e: React.TouchEvent<HTMLDivElement>): boolean {
e.preventDefault(); e.preventDefault()
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return false
} }
for (let i = 0; i < e.changedTouches.length; i++) { for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i]; const touch = e.changedTouches[i]
let key: number|null = determineKey(e, touch); const key: number | null = determineKey(e, touch)
if (key == null) { if (key == null) {
continue; continue
} }
const idx = touch.identifier; const idx = touch.identifier
onGoingTouches[idx] = key; onGoingTouches.set(idx, key)
sendKeyDown(webSocket, true, key); sendKeyDown(webSocket, true, key)
} }
return false; return false
} }
function touchMoveControls(e: React.TouchEvent<HTMLDivElement>) { function touchMoveControls (e: React.TouchEvent<HTMLDivElement>): boolean {
e.preventDefault(); e.preventDefault()
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return false
} }
for (let i = 0; i < e.changedTouches.length; i++) { for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i]; const touch = e.changedTouches[i]
let key: number|null = determineKey(e, touch); const key: number | null = determineKey(e, touch)
const idx = touch.identifier; const idx = touch.identifier
if (key == null) { if (key == null) {
continue; continue
} }
if (onGoingTouches[idx] != null) { const oldKey = onGoingTouches.get(idx)
sendKeyDown(webSocket, false, onGoingTouches[idx]); if (oldKey != null) {
delete onGoingTouches[idx]; sendKeyDown(webSocket, false, oldKey)
onGoingTouches.delete(idx)
} }
onGoingTouches[idx] = key; onGoingTouches.set(idx, key)
sendKeyDown(webSocket, true, key); sendKeyDown(webSocket, true, key)
} }
return false; return false
} }
function touchEndControls (e: React.TouchEvent<HTMLDivElement>): boolean {
function touchEndControls(e: React.TouchEvent<HTMLDivElement>) { e.preventDefault()
e.preventDefault();
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return false
} }
for (let i = 0; i < e.changedTouches.length; i++) { for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i]; const touch = e.changedTouches[i]
const idx = touch.identifier; const idx = touch.identifier
if (onGoingTouches[idx] == null) { const oldKey = onGoingTouches.get(idx)
return; if (oldKey == null) {
return false
} }
sendKeyDown(webSocket, false, onGoingTouches[idx]); sendKeyDown(webSocket, false, oldKey)
delete onGoingTouches[idx]; onGoingTouches.delete(idx)
} }
return false; return false
} }
const keyMap: string[] = ["KeyZ", "KeyX", "KeyA", "KeyS", const keyMap: string[] = ['KeyZ', 'KeyX', 'KeyA', 'KeyS',
"Enter", "Space", "ArrowUp", "ArrowDown", 'Enter', 'Space', 'ArrowUp', 'ArrowDown',
"ArrowLeft", "ArrowRight"]; 'ArrowLeft', 'ArrowRight']
function onPressControl(e: React.KeyboardEvent<HTMLDivElement>) { function onPressControl (e: React.KeyboardEvent<HTMLDivElement>): void {
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return
} }
let key = keyMap.findIndex((c: string) => c == e.code); const key = keyMap.findIndex((c: string) => c === e.code)
if (key != -1) { if (key !== -1) {
e.preventDefault(); e.preventDefault()
sendKeyDown(webSocket, true, key); sendKeyDown(webSocket, true, key)
} }
} }
function onUnpressControl(e: React.KeyboardEvent<HTMLDivElement>) { function onUnpressControl (e: React.KeyboardEvent<HTMLDivElement>): void {
if (webSocket == null) { if (webSocket == null) {
console.log('There is not websocket'); console.log('There is not websocket')
return; return
} }
let key = keyMap.findIndex((c: string) => c == e.code); const key = keyMap.findIndex((c: string) => c === e.code)
if (key != -1) { if (key !== -1) {
e.preventDefault(); e.preventDefault()
sendKeyDown(webSocket, false, key); sendKeyDown(webSocket, false, key)
} }
} }
document.onselectstart = () => false; document.onselectstart = () => false
return ( return (
<div tabIndex={-1} className="overlay" onKeyDown={onPressControl} onKeyUp={onUnpressControl}> <div tabIndex={-1} className="overlay" onKeyDown={onPressControl} onKeyUp={onUnpressControl}>
<div className="vertical-padding"> <div className="vertical-padding">
</div> </div>
<div className="controls" onTouchStart={touchStartControls} onTouchMove={touchMoveControls} onTouchEnd={touchEndControls}> <div className="controls" onTouchStart={touchStartControls} onTouchMove={touchMoveControls} onTouchEnd={touchEndControls}>
@ -235,18 +243,18 @@ export default function OverlayControls({firstMenuElement, setHiddenMenu, webSoc
<img src={HOME_BUTTON_IMAGE} alt="Go to menu. (House icon)"/> <img src={HOME_BUTTON_IMAGE} alt="Go to menu. (House icon)"/>
</a> </a>
{ {
Object.keys(controls).map((key) => Object.keys(controls).map((key) =>
<a tabIndex={-1} <a tabIndex={-1}
className={controls[key].classes} className={controls[key].classes}
ref={controls[key].ref} ref={controls[key].ref}
key={key} key={key}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, controls[key].key)} onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => { mouseDown(e, controls[key].key) }}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, controls[key].key)}> onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => { mouseUp(e, controls[key].key) }}>
{controls[key].sym} {controls[key].sym}
</a> </a>
) )
} }
</div> </div>
</div> </div>
); )
} }

View File

@ -1,59 +1,79 @@
import * as React from 'react'; import * as React from 'react'
import CloseButton from '/components/close-button'; import CloseButton from '@msgba/components/close-button'
import {CLOSE_BUTTON_IMAGE} from '/constants'; import { sendSaveRequest } from '@msgba/packet'
export interface OverlayMenuProps { export interface OverlayMenuProps {
hiddenMenu: boolean, hiddenMenu: boolean
setHiddenMenu: (c: boolean) => void, setHiddenMenu: (c: boolean) => void
emulationStarted: boolean, emulationStarted: boolean
setHiddenFormSelectFiles: (c: boolean) => void, setHiddenFormSelectFiles: (c: boolean) => void
screenRef: React.RefObject<HTMLDivElement>; screenRef: React.RefObject<HTMLDivElement>
isFullscreen: boolean; isFullscreen: boolean
setIsFullscreen: (c:boolean) => void; setIsFullscreen: (c: boolean) => void
firstMenuElement: React.RefObject<HTMLAnchorElement>, firstMenuElement: React.RefObject<HTMLAnchorElement>
websocket: WebSocket | null
}; };
interface Style { type Style = Record<string, string>
[id: string]: string;
};
export default function OverlayMenu({hiddenMenu, setHiddenMenu, emulationStarted, export default function OverlayMenu ({
setHiddenFormSelectFiles, screenRef, isFullscreen, setIsFullscreen, hiddenMenu, setHiddenMenu, emulationStarted,
firstMenuElement}: OverlayMenuProps) { setHiddenFormSelectFiles, screenRef, isFullscreen, setIsFullscreen,
function exitMenu() { firstMenuElement, websocket
setHiddenMenu(true); }: OverlayMenuProps): JSX.Element {
function exitMenu (): void {
setHiddenMenu(true)
} }
function openSelectFilesMenu() { function openSelectFilesMenu (): void {
setHiddenFormSelectFiles(false); setHiddenFormSelectFiles(false)
} }
function toggleFullscreen() { function toggleFullscreen (): void {
if (isFullscreen) { if (isFullscreen) {
document.exitFullscreen(); document.exitFullscreen().catch((c: string) => { console.log(c) })
setIsFullscreen(false); setIsFullscreen(false)
return;
} }
if (screenRef.current != null) { if (screenRef.current != null) {
screenRef.current.requestFullscreen().then(() => { screenRef.current.requestFullscreen().then(() => {
setIsFullscreen(true); setIsFullscreen(true)
}); }).catch((error: string) => {
console.log(error)
})
} }
} }
const styleSelectRom: Style = {}; const styleSelectRom: Style = {}
if (emulationStarted) { if (emulationStarted) {
styleSelectRom.display = 'none'; styleSelectRom.display = 'none'
} }
const styleMenu: Style = {}; const styleMenu: Style = {}
if (hiddenMenu) { if (hiddenMenu) {
styleMenu.display = 'none'; styleMenu.display = 'none'
} }
let toggleFullscreenText: string = 'Set fullscreen'; let toggleFullscreenText: string = 'Set fullscreen'
if (isFullscreen) { if (isFullscreen) {
toggleFullscreenText = 'End fullscreen'; toggleFullscreenText = 'End fullscreen'
}
const saveButton = React.useRef<HTMLAnchorElement>(null)
const [saveIdentifier, setSaveidentifier] = React.useState<bigint>(0n)
function onSave (): void {
if (websocket == null) {
console.log('No websocket still')
return
}
const currentSave = saveIdentifier
setSaveidentifier((c: bigint) => c + 1n)
sendSaveRequest(websocket, currentSave)
}
const styleSave: Style = {}
if (!emulationStarted) {
styleSave.display = 'none'
} }
return ( return (
@ -64,10 +84,11 @@ export default function OverlayMenu({hiddenMenu, setHiddenMenu, emulationStarted
<div className="overlay-menu"> <div className="overlay-menu">
<ul> <ul>
<li><a ref={firstMenuElement} style={styleSelectRom} onClick={openSelectFilesMenu} href="#">Select rom</a></li> <li><a ref={firstMenuElement} style={styleSelectRom} onClick={openSelectFilesMenu} href="#">Select rom</a></li>
<li><a ref={saveButton} style={styleSave} onClick={onSave} href="#">Save</a></li>
<li><a onClick={toggleFullscreen} href="#">{toggleFullscreenText}</a></li> <li><a onClick={toggleFullscreen} href="#">{toggleFullscreenText}</a></li>
<li><a href="#" onClick={exitMenu}>Exit</a></li> <li><a href="#" onClick={exitMenu}>Exit</a></li>
</ul> </ul>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,141 +1,147 @@
import * as React from 'react'; import * as React from 'react';
import CenterElement from '/components/center-element'; import CenterElement from '@msgba/components/center-element'
import FormSelectFiles from '/components/form-select-files'; import CanvasGBAEmulator from '@msgba/components/canvas-gba-emulator'
import CanvasGBAEmulator from '/components/canvas-gba-emulator'; import OverlayControls from '@msgba/components/overlay-controls'
import OverlayControls from '/components/overlay-controls'; import OverlayMenu from '@msgba/components/overlay-menu'
import OverlayMenu from '/components/overlay-menu'; import OverlaySelectFiles from '@msgba/components/overlay-select-files'
import OverlaySelectFiles from '/components/overlay-select-files'; import { sendHello, handleSendFrame } from '@msgba/packet'
import CloseButton from '/components/close-button';
import {sendHello, handleSendFrame} from '/packet';
import Endian from '/endian'; import Endian from '@msgba/endian'
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants'; import { MIN_WIDTH, MIN_HEIGHT, PACKET_ID_SEND_FRAME } from '@msgba/constants'
export interface handleClickStartEmulationButtonObjectArgs { export interface handleClickStartEmulationButtonObjectArgs {
e: React.MouseEvent<HTMLInputElement>; e: React.MouseEvent<HTMLInputElement>
inputRom: HTMLInputElement | null; inputRom: HTMLInputElement | null
inputSaveState: HTMLInputElement | null; inputSaveState: HTMLInputElement | null
canvas: React.RefObject<HTMLCanvasElement | null>; canvas: React.RefObject<HTMLCanvasElement | null>
setEmulationStarted: (c: boolean) => void, setEmulationStarted: (c: boolean) => void
setHiddenMenu: (c: boolean) => void; setHiddenMenu: (c: boolean) => void
setHiddenFormSelectFiles: (c: boolean) => void; setHiddenFormSelectFiles: (c: boolean) => void
setWebSocket: (c: WebSocket | null) => void; setWebSocket: (c: WebSocket | null) => void
}; }
function handleClickStartEmulationButton({e, inputRom, inputSaveState, canvas, setEmulationStarted, setHiddenMenu, function handleClickStartEmulationButton ({
setHiddenFormSelectFiles, setWebSocket}: handleClickStartEmulationButtonObjectArgs) { e, inputRom, inputSaveState, canvas,
e.preventDefault(); setEmulationStarted, setHiddenMenu,
setHiddenFormSelectFiles, setWebSocket
}: handleClickStartEmulationButtonObjectArgs): void {
e.preventDefault()
if (canvas.current == null) { if (canvas.current == null) {
alert('Canvas does not exists?'); alert('Canvas does not exists?')
return; return
} }
const ctx = canvas.current.getContext('2d') const ctx = canvas.current.getContext('2d')
if (ctx == null) { if (ctx == null) {
alert('Unable to create canvas context, doing nothing'); alert('Unable to create canvas context, doing nothing')
return; return
} }
if (inputRom == null || inputSaveState == null || inputRom.files == null || inputSaveState.files == null) { if (inputRom == null || inputSaveState == null || inputRom.files == null || inputSaveState.files == null) {
alert('Unable to read the files '); alert('Unable to read the files ')
return; return
} }
if (inputRom.files.length == 0) { if (inputRom.files.length === 0) {
alert('There is no rom still'); alert('There is no rom still')
return; return
} }
if (inputSaveState.files.length == 0) { if (inputSaveState.files.length === 0) {
alert('There is no savestate still'); alert('There is no savestate still')
return; return
} }
const rom_file = inputRom.files[0]; const romFile = inputRom.files[0]
const savestate_file = inputSaveState.files[0]; const savestateFile = inputSaveState.files[0]
rom_file.arrayBuffer().then((rom_buffer) => { romFile.arrayBuffer().then((romBuffer) => {
savestate_file.arrayBuffer().then((savestate_buffer) => { savestateFile.arrayBuffer().then((savestateBuffer) => {
const rom_array = new Uint8Array(rom_buffer); const romArray = new Uint8Array(romBuffer)
const savestate_array = new Uint8Array(savestate_buffer); const savestateArray = new Uint8Array(savestateBuffer)
const webSocket = new WebSocket(`ws://${window.location.host}/ws`); const webSocket = new WebSocket(`ws://${window.location.host}/ws`)
setWebSocket(webSocket); setWebSocket(webSocket)
webSocket.binaryType = 'arraybuffer'; webSocket.binaryType = 'arraybuffer'
webSocket.onclose = (message) => { webSocket.onclose = (message) => {
setEmulationStarted(false); setEmulationStarted(false)
console.log('Closing websocket.'); console.log('Closing websocket.')
setWebSocket(null); setWebSocket(null)
} }
webSocket.onopen = () => { webSocket.onopen = () => {
console.log('Opened websocket.'); console.log('Opened websocket.')
setEmulationStarted(true); setEmulationStarted(true)
sendHello(webSocket, rom_array, savestate_array); sendHello(webSocket, romArray, savestateArray)
setHiddenMenu(true); setHiddenMenu(true)
setHiddenFormSelectFiles(true); setHiddenFormSelectFiles(true)
}; }
webSocket.addEventListener('message', (event) => { webSocket.addEventListener('message', (event) => {
onWebSocketPacket(event, canvas.current, ctx) onWebSocketPacket(event, canvas.current, ctx)
}); })
}); }).catch((c: string) => {
}); console.log('Unable to convert file to array_buffer')
})
}).catch((c: string) => {
console.log('Unable to convert file to array_buffer')
})
} }
function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement | null, ctx: CanvasRenderingContext2D) { function onWebSocketPacket (event: MessageEvent,
const buffer = event.data; canvas: HTMLCanvasElement | null,
let packet_u8: Uint8Array | null = new Uint8Array(buffer); ctx: CanvasRenderingContext2D): void {
const id = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8)); const buffer = event.data
packet_u8 = packet_u8.slice(8, packet_u8.length); let packetU8: Uint8Array | null = new Uint8Array(buffer)
const size = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8)); const id = Endian.byteArrayToU64BigEndian(packetU8.slice(0, 8))
const raw_data = packet_u8.slice(8, packet_u8.length); packetU8 = packetU8.slice(8, packetU8.length)
packet_u8 = null; const size = Endian.byteArrayToU64BigEndian(packetU8.slice(0, 8))
const rawData = packetU8.slice(8, Number(size))
packetU8 = null
switch (id) { switch (id) {
case PACKET_ID_SEND_FRAME: case PACKET_ID_SEND_FRAME:
handleSendFrame(raw_data, canvas, ctx); handleSendFrame(rawData, canvas, ctx)
break; break
default: default:
console.log(`Received unknown packet ${id}`); console.log(`Received unknown packet ${id}`)
} }
} }
export default function Page() { export default function Page (): JSX.Element {
const screenDimensions = useScreenDimensions(); const screenDimensions = useScreenDimensions()
const emulatorDimensions = calculateSizeEmulator(screenDimensions); const emulatorDimensions = calculateSizeEmulator(screenDimensions)
const canvasRef = React.useRef<HTMLCanvasElement>(null); const canvasRef = React.useRef<HTMLCanvasElement>(null)
function resizeCanvas() { function resizeCanvas (): void {
const canvas = canvasRef.current; const canvas = canvasRef.current
if (canvas == null) { if (canvas == null) {
return; return
} }
if (emulatorDimensions.width === undefined || emulatorDimensions.height === undefined) { if (emulatorDimensions.width === undefined || emulatorDimensions.height === undefined) {
return; return
} }
canvas.width = emulatorDimensions.width; canvas.width = emulatorDimensions.width
canvas.height = emulatorDimensions.height; canvas.height = emulatorDimensions.height
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (ctx == null) { if (ctx == null) {
return; return
} }
fillBlack(canvas, ctx); fillBlack(canvas, ctx)
}; }
const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState<boolean>(true); const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState<boolean>(true)
React.useEffect(resizeCanvas, [emulatorDimensions]); React.useEffect(resizeCanvas, [emulatorDimensions])
const refInputRom = React.useRef<HTMLInputElement | null>(null); const refInputRom = React.useRef<HTMLInputElement | null>(null)
const refInputSaveState = React.useRef<HTMLInputElement | null>(null); const refInputSaveState = React.useRef<HTMLInputElement | null>(null)
const [emulationStarted, setEmulationStarted] = React.useState<boolean>(false); const [emulationStarted, setEmulationStarted] = React.useState<boolean>(false)
const [hiddenMenu, setHiddenMenu] = React.useState<boolean>(true); const [hiddenMenu, setHiddenMenu] = React.useState<boolean>(true)
const [webSocket, setWebSocket] = React.useState<WebSocket | null>(null); const [webSocket, setWebSocket] = React.useState<WebSocket | null>(null)
const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>) => { const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>): void => {
handleClickStartEmulationButton({ handleClickStartEmulationButton({
e: e, e,
setEmulationStarted: setEmulationStarted, setEmulationStarted,
inputRom: refInputRom.current, inputRom: refInputRom.current,
inputSaveState: refInputSaveState.current, inputSaveState: refInputSaveState.current,
canvas: canvasRef, canvas: canvasRef,
setHiddenMenu: setHiddenMenu, setHiddenMenu,
setHiddenFormSelectFiles: setHiddenFormSelectFiles, setHiddenFormSelectFiles,
setWebSocket: setWebSocket, setWebSocket
})
}); }
}; const firstMenuElement = React.useRef<HTMLAnchorElement>(null)
const firstMenuElement = React.useRef<HTMLAnchorElement>(null); const screenRef = React.useRef<HTMLDivElement>(null)
const screenRef = React.useRef<HTMLDivElement>(null); const [isFullscreen, setIsFullscreen] = React.useState<boolean>(false)
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(false);
return ( return (
<div ref={screenRef}> <div ref={screenRef}>
<OverlayControls firstMenuElement={firstMenuElement} <OverlayControls firstMenuElement={firstMenuElement}
@ -144,78 +150,78 @@ export default function Page() {
setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted} setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted}
setHiddenFormSelectFiles={setHiddenFormSelectFiles} screenRef={screenRef} setHiddenFormSelectFiles={setHiddenFormSelectFiles} screenRef={screenRef}
isFullscreen={isFullscreen} setIsFullscreen={setIsFullscreen} isFullscreen={isFullscreen} setIsFullscreen={setIsFullscreen}
firstMenuElement={firstMenuElement}/> firstMenuElement={firstMenuElement} websocket={webSocket}/>
<OverlaySelectFiles hiddenFormSelectFiles={hiddenFormSelectFiles} <OverlaySelectFiles hiddenFormSelectFiles={hiddenFormSelectFiles}
setHiddenFormSelectFiles={setHiddenFormSelectFiles} setHiddenFormSelectFiles={setHiddenFormSelectFiles}
refInputRom={refInputRom} refInputSaveState={refInputSaveState} refInputRom={refInputRom} refInputSaveState={refInputSaveState}
onStartEmulation={onStartEmulation}/> onStartEmulation={onStartEmulation}/>
<div> <div>
<CenterElement full={true}> <CenterElement full={true}>
<CanvasGBAEmulator canvasRef={canvasRef}/> <CanvasGBAEmulator canvasRef={canvasRef}/>
</CenterElement> </CenterElement>
</div> </div>
</div> </div>
); )
} }
function getScreenDimensions() { function getScreenDimensions (): EmulatorDimensions {
return { return {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight height: document.body.clientHeight
}; }
} }
function useScreenDimensions() { function useScreenDimensions (): EmulatorDimensions {
const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions()); const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions())
React.useEffect(() => { React.useEffect(() => {
function onResize() { function onResize (): void {
setScreenDimensions(getScreenDimensions()); setScreenDimensions(getScreenDimensions())
} }
window.addEventListener("resize", onResize); window.addEventListener('resize', onResize)
return () => { return () => {
window.removeEventListener("resize", onResize); window.removeEventListener('resize', onResize)
} }
}, []); }, [])
return screenDimensions
return screenDimensions;
} }
function fillBlack(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) { function fillBlack (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void {
ctx.beginPath(); ctx.beginPath()
ctx.rect(0, 0, canvas.width, canvas.height); ctx.rect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#0E0E10'; ctx.fillStyle = '#0E0E10'
ctx.fill(); ctx.fill()
} }
export interface EmulatorDimensions { export interface EmulatorDimensions {
width?: number; width?: number
height?: number; height?: number
}; };
function calculateSizeEmulator(screenDimensions: EmulatorDimensions): EmulatorDimensions {
function calculateSizeEmulator (screenDimensions: EmulatorDimensions): EmulatorDimensions {
if (screenDimensions.width === undefined || screenDimensions.height === undefined) { if (screenDimensions.width === undefined || screenDimensions.height === undefined) {
console.error(screenDimensions, 'screenDimensions has undefined fields'); console.error(screenDimensions, 'screenDimensions has undefined fields')
return {}; return {}
} }
const width = screenDimensions.width; const width = screenDimensions.width
const height = screenDimensions.height; const height = screenDimensions.height
const emulatorDimensions: EmulatorDimensions = {}; const emulatorDimensions: EmulatorDimensions = {}
if (width < MIN_WIDTH || height < MIN_HEIGHT) { if (width < MIN_WIDTH || height < MIN_HEIGHT) {
return { return {
width: MIN_WIDTH, width: MIN_WIDTH,
height: MIN_HEIGHT, height: MIN_HEIGHT
}; }
} }
const ratioWidth = width / MIN_WIDTH; const ratioWidth = width / MIN_WIDTH
const ratioHeight = height / MIN_HEIGHT; const ratioHeight = height / MIN_HEIGHT
if (ratioWidth < ratioHeight) { if (ratioWidth < ratioHeight) {
emulatorDimensions.width = MIN_WIDTH * ratioWidth; emulatorDimensions.width = MIN_WIDTH * ratioWidth
emulatorDimensions.height = MIN_HEIGHT * ratioWidth; emulatorDimensions.height = MIN_HEIGHT * ratioWidth
} else { } else {
emulatorDimensions.height = MIN_HEIGHT * ratioHeight; emulatorDimensions.height = MIN_HEIGHT * ratioHeight
emulatorDimensions.width = MIN_WIDTH * ratioHeight; emulatorDimensions.width = MIN_WIDTH * ratioHeight
} }
return emulatorDimensions; return emulatorDimensions
} }

View File

@ -1,16 +1,9 @@
export const MIN_WIDTH = 240; export const MIN_WIDTH = 240
export const MIN_HEIGHT = 160; export const MIN_HEIGHT = 160
export const PACKET_ID_HELLO = 0n; export const PACKET_ID_HELLO = 0n
export const PACKET_ID_SEND_FRAME = 1n; export const PACKET_ID_SEND_FRAME = 1n
export const PACKET_ID_KEY_DOWN = 2n; export const PACKET_ID_KEY_DOWN = 2n
export const CLOSE_BUTTON_IMAGE: string = "/img/close.png"; export const PACKET_ID_SAVE_REQUEST = 3n
export const HOME_BUTTON_IMAGE: string = "/img/home.png"; export const PACKET_ID_SAVE_RESPONSE = 4n
export const CLOSE_BUTTON_IMAGE: string = '/img/close.png'
export default class Constants { export const HOME_BUTTON_IMAGE: string = '/img/home.png'
public static MIN_WIDTH: number = MIN_WIDTH;
public static MIN_HEIGHT: number = MIN_HEIGHT;
public static PACKET_ID_HELLO: bigint = PACKET_ID_HELLO;
public static PACKET_ID_SEND_FRAME: bigint = PACKET_ID_SEND_FRAME;
public static CLOSE_BUTTON_IMAGE: string = CLOSE_BUTTON_IMAGE;
public static HOME_BUTTON_IMAGE: string = HOME_BUTTON_IMAGE;
};

View File

@ -2,8 +2,8 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOMClient from 'react-dom/client'; import * as ReactDOMClient from 'react-dom/client';
import Endian from '/endian'; import Endian from '@msgba/endian';
import Page from '/components/page'; import Page from '@msgba/components/page';
const body = document.querySelector('body'); const body = document.querySelector('body');
if (body != null) { if (body != null) {

View File

@ -1,70 +1,78 @@
import Endian from '/endian'; import Endian from '@msgba/endian'
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME, PACKET_ID_KEY_DOWN} from '/constants'; import { MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_KEY_DOWN, PACKET_ID_SAVE_REQUEST } from '@msgba/constants'
function concatU8Array(array1: Uint8Array, array2: Uint8Array) { function concatU8Array (array1: Uint8Array, array2: Uint8Array): Uint8Array {
const final_array = new Uint8Array(array1.length + array2.length); const finalArray = new Uint8Array(array1.length + array2.length)
final_array.set(array1); finalArray.set(array1)
final_array.set(array2, array1.length); finalArray.set(array2, array1.length)
return final_array; return finalArray
} }
export function sendHello(websocket: WebSocket, rom_array: Uint8Array, savestate_array: Uint8Array) { export function sendHello (websocket: WebSocket, romArray: Uint8Array, savestateArray: Uint8Array): void {
console.log('Sending hello.'); console.log('Sending hello.')
const length_rom = BigInt(rom_array.length); const lengthRom = BigInt(romArray.length)
const length_savestate = BigInt(savestate_array.length); const lengthSavestate = BigInt(savestateArray.length)
const raw_data = const rawData =
concatU8Array( concatU8Array(
concatU8Array( concatU8Array(
concatU8Array(Endian.u64ToByteArrayBigEndian(length_rom), rom_array), concatU8Array(Endian.u64ToByteArrayBigEndian(lengthRom), romArray),
Endian.u64ToByteArrayBigEndian(length_savestate) Endian.u64ToByteArrayBigEndian(lengthSavestate)
), ),
savestate_array savestateArray
); )
sendPacket(websocket, PACKET_ID_HELLO, raw_data); sendPacket(websocket, PACKET_ID_HELLO, rawData)
} }
export function sendKeyDown(websocket: WebSocket, isDown: boolean, key: number) { export function sendKeyDown (websocket: WebSocket, isDown: boolean, key: number): void {
console.log('Sending keyDown.', isDown); console.log('Sending keyDown.', isDown)
const isDownArray = new Uint8Array(1); const isDownArray = new Uint8Array(1)
isDownArray[0] = isDown ? 1: 0; isDownArray[0] = isDown ? 1 : 0
const rawData = concatU8Array(isDownArray, Endian.u32ToByteArrayBigEndian(key)); const rawData = concatU8Array(isDownArray, Endian.u32ToByteArrayBigEndian(key))
sendPacket(websocket, PACKET_ID_KEY_DOWN, rawData); sendPacket(websocket, PACKET_ID_KEY_DOWN, rawData)
} }
export function sendPacket(websocket: WebSocket, id: bigint, raw_data: Uint8Array) { export function sendSaveRequest (websocket: WebSocket, identifier: bigint): void {
const packet_u8 = concatU8Array( console.log('Sendidng save request', identifier)
concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(raw_data.length))), const rawData = Endian.u64ToByteArrayBigEndian(identifier)
raw_data sendPacket(websocket, PACKET_ID_SAVE_REQUEST, rawData)
);
const packet_buffer = packet_u8.buffer;
console.log('Sending packet');
websocket.send(packet_buffer);
} }
export function handleSendFrame(raw_data: Uint8Array, canvas: HTMLCanvasElement | null, ctx: CanvasRenderingContext2D) { export function sendPacket (websocket: WebSocket, id: bigint, rawData: Uint8Array): void {
console.log('Reachs here'); const packetU8 = concatU8Array(
concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(rawData.length))),
rawData
)
const packetBuffer = packetU8.buffer
console.log('Sending packet')
websocket.send(packetBuffer)
}
export function handleSendFrame (rawData: Uint8Array, canvas: HTMLCanvasElement | null, ctx: CanvasRenderingContext2D): void {
console.log('Reachs here')
if (canvas == null) { if (canvas == null) {
console.log('No canvas'); console.log('No canvas')
return; return
} }
let data: Uint8Array | null = raw_data; let data: Uint8Array | null = rawData
const stride = Endian.byteArrayToU32BigEndian(data.slice(0, 4)); data = data.slice(4, data.length)
data = data.slice(4, data.length); const outputBufferSize = Endian.byteArrayToU64BigEndian(data.slice(0, 8))
const output_buffer_size = Endian.byteArrayToU64BigEndian(data.slice(0, 8)); // TODO: This number conversion is not great. Is there other option?
data = data.slice(8, data.length); data = data.slice(8, Number(outputBufferSize))
const img_data = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT); const imgData = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT)
const img_data_u8 = new Uint8Array(img_data.data.buffer); const imgDataU8 = new Uint8Array(imgData.data.buffer)
for (let i = 0; i<data.length; i++) { for (let i = 0; i < data.length; i++) {
if (i % 4 == 3) { if (i % 4 === 3) {
img_data_u8[i] = 255; imgDataU8[i] = 255
continue; continue
} }
img_data_u8[i] = data[i]; imgDataU8[i] = data[i]
} }
data = null; data = null
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx)); createImageBitmap(imgData).then((bitmap) => { drawBitmap(bitmap, canvas, ctx) }).catch((c: string) => {
console.log(`Unable to print to the canvas the frame because: ${c}`)
})
} }
function drawBitmap(bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) { function drawBitmap (bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void {
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height); ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
} }

View File

@ -13,10 +13,20 @@
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"eslint": "^8.36.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-no-relative-import-paths": "^1.5.2",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"typescript-transform-paths": "^3.4.6",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"

File diff suppressed because one or more lines are too long

View File

@ -10,8 +10,21 @@
"strictNullChecks": true, "strictNullChecks": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"*": ["js-src/*"] "@msgba/*": ["js-src/*"],
} "/*": ["js-src/*"]
},
"plugins": [
{
"transform": "typescript-transform-paths"
},
{
"transform": "typescript-transform-paths",
"afterDeclarations": true
}
]
}, },
"include": ["js-src/*.ts", "js-src/*/*.ts" ] "include": [
"js-src/*.ts", "js-src/*/*.ts",
"js-src/*.tsx", "js-src/*/*.tsx"
]
} }

View File

@ -1,35 +1,38 @@
const path = require('path'); const path = require('path')
module.exports = { module.exports = {
entry: './js-src/index.tsx', entry: './js-src/index.tsx',
mode: 'development', mode: 'development',
output: { output: {
filename: 'bundle.js', filename: 'bundle.js',
path: path.resolve(__dirname, 'public/js/'), path: path.resolve(__dirname, 'public/js/')
}, },
resolve: { resolve: {
extensions: [ '.js', '.jsx','.ts', '.tsx' ], extensions: ['.js', '.jsx', '.ts', '.tsx'],
roots: [ roots: [
path.resolve(__dirname, 'js-src/') path.resolve(__dirname, 'js-src/')
] ],
alias: {
'@msgba': path.resolve(__dirname, 'js-src')
}
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: 'ts-loader', use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/
}, },
{ {
test: /\.jpe?g|png$/, test: /\.jpe?g|png$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ["url-loader", "file-loader"] use: ['url-loader', 'file-loader']
}, },
{ {
test: /\.(js|jsx)$/, test: /\.(js|jsx)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: "babel-loader" loader: 'babel-loader'
} }
] ]
} }
}; }