import * as React from 'react'; import CenterElement from '/components/center-element'; import FormSelectFiles from '/components/form-select-files'; import CanvasGBAEmulator from '/components/canvas-gba-emulator'; import OverlayControls from '/components/overlay-controls'; import OverlayMenu from '/components/overlay-menu'; import OverlaySelectFiles from '/components/overlay-select-files'; import CloseButton from '/components/close-button'; import Endian from '/endian'; import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants'; type setBooleanCallback = (c: boolean) => boolean; export interface handleClickStartEmulationButtonObjectArgs { e: React.MouseEvent; inputRom: HTMLInputElement | null; inputSaveState: HTMLInputElement | null; canvas: HTMLCanvasElement | null; printingFrame: boolean; setPrintingFrame: (c: setBooleanCallback) => void; setEmulationStarted: (c: boolean) => void, setHiddenMenu: (c: boolean) => void; setHiddenFormSelectFiles: (c: boolean) => void; }; function handleClickStartEmulationButton({e, inputRom, inputSaveState, canvas, printingFrame, setPrintingFrame, setEmulationStarted, setHiddenMenu, setHiddenFormSelectFiles}: handleClickStartEmulationButtonObjectArgs) { e.preventDefault(); if (canvas == null) { alert('Canvas does not exists?'); return; } const ctx = canvas.getContext('2d') if (ctx == null) { alert('Unable to create canvas context, doing nothing'); return; } 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 rom_file = inputRom.files[0]; const savestate_file = inputSaveState.files[0]; rom_file.arrayBuffer().then((rom_buffer) => { savestate_file.arrayBuffer().then((savestate_buffer) => { const rom_array = new Uint8Array(rom_buffer); const savestate_array = new Uint8Array(savestate_buffer); const websocket = new WebSocket(`ws://${window.location.host}/ws`); websocket.binaryType = 'arraybuffer'; websocket.onclose = (message) => { setEmulationStarted(false); console.log('Closing websocket.'); } websocket.onopen = () => { console.log('Opened websocket.'); setEmulationStarted(true); sendHello(websocket, rom_array, savestate_array); setHiddenMenu(true); setHiddenFormSelectFiles(true); }; setPrintingFrame(c => false); websocket.addEventListener('message', (event) => { onWebSocketPacket(event, canvas, ctx, printingFrame, setPrintingFrame) }); }); }); } function concatU8Array(array1: Uint8Array, array2: Uint8Array) { const final_array = new Uint8Array(array1.length + array2.length); final_array.set(array1); final_array.set(array2, array1.length); return final_array; } function sendPacket(websocket: WebSocket, id: bigint, raw_data: Uint8Array) { const packet_u8 = concatU8Array( concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(raw_data.length))), raw_data ); const packet_buffer = packet_u8.buffer; console.log('Sending packet'); websocket.send(packet_buffer); } function sendHello(websocket: WebSocket, rom_array: Uint8Array, savestate_array: Uint8Array) { console.log('Sending hello.'); const length_rom = BigInt(rom_array.length); const length_savestate = BigInt(savestate_array.length); const raw_data = concatU8Array( concatU8Array( concatU8Array(Endian.u64ToByteArrayBigEndian(length_rom), rom_array), Endian.u64ToByteArrayBigEndian(length_savestate) ), savestate_array ); sendPacket(websocket, PACKET_ID_HELLO, raw_data); } function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) { const buffer = event.data; let packet_u8: Uint8Array | null = new Uint8Array(buffer); const id = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8)); packet_u8 = packet_u8.slice(8, packet_u8.length); const size = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8)); const raw_data = packet_u8.slice(8, packet_u8.length); packet_u8 = null; switch (id) { case PACKET_ID_SEND_FRAME: handleSendFrame(raw_data, canvas, ctx, printingFrame, setPrintingFrame); break; default: console.log(`Received unknown packet ${id}`); } } function handleSendFrame(raw_data: Uint8Array, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) { if (printingFrame) { return; } setPrintingFrame(c => true); let data: Uint8Array | null = raw_data; const stride = Endian.byteArrayToU32BigEndian(data.slice(0, 4)); data = data.slice(4, data.length); const output_buffer_size = Endian.byteArrayToU64BigEndian(data.slice(0, 8)); data = data.slice(8, data.length); const img_data = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT); const img_data_u8 = new Uint8Array(img_data.data.buffer); for (let i = 0; i drawBitmap(bitmap, canvas, ctx, printingFrame)); } function drawBitmap(bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean) { ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height); printingFrame = false; } export default function Page() { const screenDimensions = useScreenDimensions(); const emulatorDimensions = calculateSizeEmulator(screenDimensions); const canvasRef = React.useRef(null); function resizeCanvas() { 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); const [printingFrame, setPrintingFrame] = React.useState(false); 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 onStartEmulation = (e: React.MouseEvent) => { handleClickStartEmulationButton({ e: e, setEmulationStarted: setEmulationStarted, inputRom: refInputRom.current, inputSaveState: refInputSaveState.current, canvas: canvasRef.current, setPrintingFrame: setPrintingFrame, printingFrame: printingFrame, setHiddenMenu: setHiddenMenu, setHiddenFormSelectFiles: setHiddenFormSelectFiles, }); }; const firstMenuElement = React.useRef(null); const screenRef = React.useRef(null); const [isFullscreen, setIsFullscreen] = React.useState(false); return (
); } function getScreenDimensions() { return { width: document.body.clientWidth, height: document.body.clientHeight }; } function useScreenDimensions() { const [screenDimensions, setScreenDimensions] = React.useState(getScreenDimensions()); React.useEffect(() => { function onResize() { setScreenDimensions(getScreenDimensions()); } window.addEventListener("resize", onResize); return () => { window.removeEventListener("resize", onResize); } }, []); return screenDimensions; } function fillBlack(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) { 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): 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 = {}; if (width < MIN_WIDTH || height < MIN_HEIGHT) { return { width: MIN_WIDTH, height: MIN_HEIGHT, }; } const ratioWidth = width / MIN_WIDTH; const ratioHeight = height / MIN_HEIGHT; if (ratioWidth < ratioHeight) { emulatorDimensions.width = MIN_WIDTH * ratioWidth; emulatorDimensions.height = MIN_HEIGHT * ratioWidth; } else { emulatorDimensions.height = MIN_HEIGHT * ratioHeight; emulatorDimensions.width = MIN_WIDTH * ratioHeight; } return emulatorDimensions; }