msgba-web/js-src/components/page.tsx

277 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 (overlayMenu.current == null) {
return
}
const allAnchors = overlayMenu.current.querySelectorAll('a')
for (const anchor of allAnchors) {
if (anchor.style.display === 'none') {
continue
}
anchor.focus()
break
}
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 overlayMenu = React.useRef<HTMLDivElement>(null)
const screenRef = React.useRef<HTMLDivElement>(null)
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(false)
return (
<div ref={screenRef}>
<OverlayControls controlsRef={controlsRef}
setHiddenMenu={setHiddenMenu} webSocket={webSocket}/>
<OverlayMenu hiddenMenu={hiddenMenu}
setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted}
setHiddenFormSelectFiles={setHiddenFormSelectFiles} screenRef={screenRef}
isFullscreen={isFullscreen} setIsFullscreen={setIsFullscreen}
overlayMenu={overlayMenu} 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
}