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 inputRom: HTMLInputElement | null inputSaveState: HTMLInputElement | null canvas: React.RefObject 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 onSaveResponseLambdas: Map void> } function onWebSocketPacket (event: MessageEvent, canvas: HTMLCanvasElement | null, ctx: CanvasRenderingContext2D, isGBC: boolean, setIsGBC: (c: boolean) => void, onSaveResponseLambdas: Map 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 void>): Promise { 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(false) const screenDimensions = useScreenDimensions(isGBC) const emulatorDimensions = calculateSizeEmulator(screenDimensions, isGBC) const canvasRef = React.useRef(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(true) React.useEffect(resizeCanvas, [emulatorDimensions]) const refInputRom = React.useRef(null) const refInputSaveState = React.useRef(null) const [emulationStarted, setEmulationStarted] = React.useState(false) const [hiddenMenu, setHiddenMenu] = React.useState(true) const [webSocket, setWebSocket] = React.useState(null) const [onSaveResponseLambdas] = React.useState(new Map void>()) const controlsRef = React.useRef(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): 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): void => { handleClickStartEmulationButton(e) } const overlayMenu = React.useRef(null) const screenRef = React.useRef(null) const [isFullscreen, setIsFullscreen] = React.useState(false) return (
) } function getScreenDimensions (): EmulatorDimensions { return { width: document.body.clientWidth, height: document.body.clientHeight } } function useScreenDimensions (isGBC: boolean): EmulatorDimensions { const [screenDimensions, setScreenDimensions] = React.useState(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 }