269 lines
11 KiB
TypeScript
269 lines
11 KiB
TypeScript
import * as React from 'react';
|
|
|
|
import CenterElement from '@msgba/components/center-element'
|
|
import CanvasGBAEmulator from '@msgba/components/canvas-gba-emulator'
|
|
import OverlayControls from '@msgba/components/overlay-controls'
|
|
import OverlayMenu from '@msgba/components/overlay-menu'
|
|
import OverlaySelectFiles from '@msgba/components/overlay-select-files'
|
|
import { sendHello, handleSendFrame, handleSaveResponse } from '@msgba/packet'
|
|
|
|
import Endian from '@msgba/endian'
|
|
|
|
import { MIN_WIDTH_GBA, MIN_HEIGHT_GBA, MIN_WIDTH_GBC, MIN_HEIGHT_GBC, PACKET_ID_SEND_FRAME, PACKET_ID_SAVE_RESPONSE } from '@msgba/constants'
|
|
|
|
export interface handleClickStartEmulationButtonObjectArgs {
|
|
e: React.MouseEvent<HTMLInputElement>
|
|
inputRom: HTMLInputElement | null
|
|
inputSaveState: HTMLInputElement | null
|
|
canvas: React.RefObject<HTMLCanvasElement | null>
|
|
setEmulationStarted: (c: boolean) => void
|
|
setHiddenMenu: (c: boolean) => void
|
|
setHiddenFormSelectFiles: (c: boolean) => void
|
|
setWebSocket: (c: WebSocket | null) => void
|
|
isGBC: boolean
|
|
setIsGBC: (c: boolean) => void
|
|
controlsRef: React.RefObject<HTMLDivElement>
|
|
onSaveResponseLambdas: Map<bigint, (saveFile: Uint8Array) => void>
|
|
}
|
|
|
|
function onWebSocketPacket (event: MessageEvent,
|
|
canvas: HTMLCanvasElement | null,
|
|
ctx: CanvasRenderingContext2D,
|
|
isGBC: boolean, setIsGBC: (c: boolean) => void,
|
|
onSaveResponseLambdas: Map<bigint, (saveFile: Uint8Array) => void>): void {
|
|
const buffer = event.data
|
|
let packetU8: Uint8Array | null = new Uint8Array(buffer)
|
|
const id = Endian.byteArrayToU64BigEndian(packetU8.slice(0, 8))
|
|
packetU8 = packetU8.slice(8, packetU8.length)
|
|
const size = Endian.byteArrayToU64BigEndian(packetU8.slice(0, 8))
|
|
console.log(size)
|
|
const rawData = packetU8.slice(8, Number(size) + 8)
|
|
console.log(rawData.length)
|
|
packetU8 = null
|
|
handlePacket(id, rawData, canvas, setIsGBC, onSaveResponseLambdas).catch((error: string) => {
|
|
console.log('Error handling packet', error)
|
|
})
|
|
}
|
|
|
|
async function handlePacket (id: bigint, rawData: Uint8Array,
|
|
canvas: HTMLCanvasElement | null,
|
|
setIsGBC: (c: boolean) => void,
|
|
onSaveResponseLambdas: Map<bigint, (saveFile: Uint8Array) => void>): Promise<void> {
|
|
switch (id) {
|
|
case PACKET_ID_SEND_FRAME:
|
|
handleSendFrame(rawData, canvas, setIsGBC)
|
|
break
|
|
case PACKET_ID_SAVE_RESPONSE:
|
|
handleSaveResponse(rawData, canvas, onSaveResponseLambdas)
|
|
break
|
|
default:
|
|
throw new Error(`Received unknown packet ${id}`)
|
|
}
|
|
}
|
|
|
|
export default function Page (): JSX.Element {
|
|
const [isGBC, setIsGBC] = React.useState<boolean>(false)
|
|
const screenDimensions = useScreenDimensions(isGBC)
|
|
const emulatorDimensions = calculateSizeEmulator(screenDimensions, isGBC)
|
|
const canvasRef = React.useRef<HTMLCanvasElement>(null)
|
|
function resizeCanvas (): void {
|
|
const canvas = canvasRef.current
|
|
if (canvas == null) {
|
|
return
|
|
}
|
|
if (emulatorDimensions.width === undefined || emulatorDimensions.height === undefined) {
|
|
return
|
|
}
|
|
canvas.width = emulatorDimensions.width
|
|
canvas.height = emulatorDimensions.height
|
|
const ctx = canvas.getContext('2d')
|
|
if (ctx == null) {
|
|
return
|
|
}
|
|
fillBlack(canvas, ctx)
|
|
}
|
|
const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState<boolean>(true)
|
|
React.useEffect(resizeCanvas, [emulatorDimensions])
|
|
const refInputRom = React.useRef<HTMLInputElement | null>(null)
|
|
const refInputSaveState = React.useRef<HTMLInputElement | null>(null)
|
|
const [emulationStarted, setEmulationStarted] = React.useState<boolean>(false)
|
|
const [hiddenMenu, setHiddenMenu] = React.useState<boolean>(true)
|
|
const [webSocket, setWebSocket] = React.useState<WebSocket | null>(null)
|
|
const [onSaveResponseLambdas] = React.useState(new Map<bigint, (saveResponseFile: Uint8Array) => void>())
|
|
|
|
const controlsRef = React.useRef<HTMLDivElement>(null)
|
|
React.useEffect(() => {
|
|
console.log('Focusing the main screen')
|
|
setTimeout(() => {
|
|
if (!hiddenFormSelectFiles) {
|
|
if (refInputRom.current != null) {
|
|
refInputRom.current.focus()
|
|
}
|
|
return
|
|
}
|
|
if (!hiddenMenu) {
|
|
if (firstMenuElement.current != null) {
|
|
firstMenuElement.current.focus()
|
|
}
|
|
return
|
|
}
|
|
|
|
if (controlsRef.current != null && hiddenMenu) {
|
|
controlsRef.current.focus()
|
|
}
|
|
}, 100)
|
|
}, [hiddenMenu, hiddenFormSelectFiles])
|
|
function handleClickStartEmulationButton (e: React.MouseEvent<HTMLInputElement>): void {
|
|
e.preventDefault()
|
|
if (canvasRef.current == null) {
|
|
alert('Canvas does not exists?')
|
|
return
|
|
}
|
|
const ctx = canvasRef.current.getContext('2d')
|
|
if (ctx == null) {
|
|
alert('Unable to create canvas context, doing nothing')
|
|
return
|
|
}
|
|
const inputRom = refInputRom.current
|
|
const inputSaveState = refInputSaveState.current
|
|
if (inputRom == null || inputSaveState == null || inputRom.files == null || inputSaveState.files == null) {
|
|
alert('Unable to read the files ')
|
|
return
|
|
}
|
|
if (inputRom.files.length === 0) {
|
|
alert('There is no rom still')
|
|
return
|
|
}
|
|
if (inputSaveState.files.length === 0) {
|
|
alert('There is no savestate still')
|
|
return
|
|
}
|
|
const romFile = inputRom.files[0]
|
|
const savestateFile = inputSaveState.files[0]
|
|
romFile.arrayBuffer().then((romBuffer) => {
|
|
savestateFile.arrayBuffer().then((savestateBuffer) => {
|
|
const romArray = new Uint8Array(romBuffer)
|
|
const savestateArray = new Uint8Array(savestateBuffer)
|
|
const locationProtocol = window.location.protocol
|
|
if (locationProtocol == null) {
|
|
return
|
|
}
|
|
const protocol = locationProtocol.match(/https:/) != null ? 'wss' : 'ws'
|
|
const webSocket = new WebSocket(`${protocol}://${window.location.host}/ws`)
|
|
setWebSocket(webSocket)
|
|
webSocket.binaryType = 'arraybuffer'
|
|
webSocket.onclose = (message) => {
|
|
setEmulationStarted(false)
|
|
console.log('Closing websocket.')
|
|
setWebSocket(null)
|
|
}
|
|
webSocket.onopen = () => {
|
|
console.log('Opened websocket.')
|
|
setEmulationStarted(true)
|
|
sendHello(webSocket, romArray, savestateArray)
|
|
setHiddenMenu(true)
|
|
setHiddenFormSelectFiles(true)
|
|
}
|
|
webSocket.addEventListener('message', (event) => {
|
|
onWebSocketPacket(event, canvasRef.current, ctx, isGBC, setIsGBC, onSaveResponseLambdas)
|
|
})
|
|
}).catch((c: string) => {
|
|
console.log('Unable to convert file to array_buffer')
|
|
})
|
|
}).catch((c: string) => {
|
|
console.log('Unable to convert file to array_buffer')
|
|
})
|
|
}
|
|
const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>): void => {
|
|
handleClickStartEmulationButton(e)
|
|
}
|
|
const firstMenuElement = React.useRef<HTMLAnchorElement>(null)
|
|
const screenRef = React.useRef<HTMLDivElement>(null)
|
|
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(false)
|
|
return (
|
|
<div ref={screenRef}>
|
|
<OverlayControls controlsRef={controlsRef} firstMenuElement={firstMenuElement}
|
|
setHiddenMenu={setHiddenMenu} webSocket={webSocket}/>
|
|
<OverlayMenu hiddenMenu={hiddenMenu}
|
|
setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted}
|
|
setHiddenFormSelectFiles={setHiddenFormSelectFiles} screenRef={screenRef}
|
|
isFullscreen={isFullscreen} setIsFullscreen={setIsFullscreen}
|
|
firstMenuElement={firstMenuElement} websocket={webSocket}
|
|
onSaveResponseLambdas={onSaveResponseLambdas}/>
|
|
<OverlaySelectFiles hiddenFormSelectFiles={hiddenFormSelectFiles}
|
|
setHiddenFormSelectFiles={setHiddenFormSelectFiles}
|
|
refInputRom={refInputRom} refInputSaveState={refInputSaveState}
|
|
onStartEmulation={onStartEmulation}/>
|
|
<div>
|
|
<CenterElement full={true}>
|
|
<CanvasGBAEmulator canvasRef={canvasRef}/>
|
|
</CenterElement>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function getScreenDimensions (): EmulatorDimensions {
|
|
return {
|
|
width: document.body.clientWidth,
|
|
height: document.body.clientHeight
|
|
}
|
|
}
|
|
|
|
function useScreenDimensions (isGBC: boolean): EmulatorDimensions {
|
|
const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions())
|
|
|
|
React.useEffect(() => {
|
|
function onResize (): void {
|
|
setScreenDimensions(getScreenDimensions())
|
|
}
|
|
|
|
window.addEventListener('resize', onResize)
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', onResize)
|
|
}
|
|
}, [isGBC])
|
|
return screenDimensions
|
|
}
|
|
|
|
function fillBlack (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void {
|
|
ctx.beginPath()
|
|
ctx.rect(0, 0, canvas.width, canvas.height)
|
|
ctx.fillStyle = '#0E0E10'
|
|
ctx.fill()
|
|
}
|
|
|
|
export interface EmulatorDimensions {
|
|
width?: number
|
|
height?: number
|
|
};
|
|
|
|
function calculateSizeEmulator (screenDimensions: EmulatorDimensions, isGBC: boolean): EmulatorDimensions {
|
|
if (screenDimensions.width === undefined || screenDimensions.height === undefined) {
|
|
console.error(screenDimensions, 'screenDimensions has undefined fields')
|
|
return {}
|
|
}
|
|
const width = screenDimensions.width
|
|
const height = screenDimensions.height
|
|
const emulatorDimensions: EmulatorDimensions = {}
|
|
const minWidth = !isGBC ? MIN_WIDTH_GBA : MIN_WIDTH_GBC
|
|
const minHeight = !isGBC ? MIN_HEIGHT_GBA : MIN_HEIGHT_GBC
|
|
if (width < minWidth || height < minHeight) {
|
|
return {
|
|
width: minWidth,
|
|
height: minHeight
|
|
}
|
|
}
|
|
const ratioWidth = width / minWidth
|
|
const ratioHeight = height / minHeight
|
|
if (ratioWidth < ratioHeight) {
|
|
emulatorDimensions.width = minWidth * ratioWidth
|
|
emulatorDimensions.height = minHeight * ratioWidth
|
|
} else {
|
|
emulatorDimensions.height = minHeight * ratioHeight
|
|
emulatorDimensions.width = minWidth * ratioHeight
|
|
}
|
|
return emulatorDimensions
|
|
}
|