diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 0000000..30e6bce --- /dev/null +++ b/babel.config.json @@ -0,0 +1,5 @@ +{ + "presets": [ + "@babel/preset-react" + ] +} diff --git a/js-src/components/canvas-gba-emulator.jsx b/js-src/components/canvas-gba-emulator.jsx new file mode 100644 index 0000000..8e03378 --- /dev/null +++ b/js-src/components/canvas-gba-emulator.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import {MIN_WIDTH, MIN_HEIGHT} from '/constants'; + +export default function CanvasGBAEmulator(props) { + return (); +} diff --git a/js-src/components/center-element.jsx b/js-src/components/center-element.jsx new file mode 100644 index 0000000..89c242f --- /dev/null +++ b/js-src/components/center-element.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export default function CenterElement(props) { + let hidden = props.hidden; + if (hidden == null) { + hidden = false; + } + return ( + + ); +} diff --git a/js-src/components/form-select-files.jsx b/js-src/components/form-select-files.jsx new file mode 100644 index 0000000..09b00d0 --- /dev/null +++ b/js-src/components/form-select-files.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +export default function FormSelectFiles(props) { + const inputRom = props.refInputRom ? props.refInputRom : React.useRef(null); + const inputSaveState = props.refInputSaveState ? props.refInputSaveState : React.useRef(null); + const onStartEmulation = props.onStartEmulation; + + return ( +
+ + + +
+ ); +} diff --git a/js-src/components/page.jsx b/js-src/components/page.jsx new file mode 100644 index 0000000..0e82f2e --- /dev/null +++ b/js-src/components/page.jsx @@ -0,0 +1,245 @@ +import React from 'react'; + +import CenterElement from '/components/center-element'; +import FormSelectFiles from '/components/form-select-files'; +import CanvasGBAEmulator from '/components/canvas-gba-emulator'; + +import Endian from '/endian'; + +import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants'; + +function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHiddenFormSelectFiles, canvas}) { + const ctx = canvas.getContext('2d') + e.preventDefault(); + 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://localhost:3000/ws`); + websocket.binaryType = 'arraybuffer'; + websocket.onclose = (message) => console.log('CLOSE', message); + websocket.onopen = () => { + setHiddenFormSelectFiles(c => false); + console.log('Opened websocket.'); + sendHello(websocket, rom_array, savestate_array); + }; + websocket.addEventListener('message', (event) => onWebSocketPacket(event, canvas, ctx)); + }); + }); +} + +function concatU8Array(array1, array2) { + const final_array = new Uint8Array(array1.length + array2.length); + final_array.set(array1); + final_array.set(array2, array1.length); + return final_array; +} + +function u64ToByteArrayBigEndian(input_number) { + const buffer = new ArrayBuffer(8); + const buffer8 = new Uint8Array(buffer); + const buffer64 = new BigUint64Array(buffer); + buffer64[0] = input_number; + if (Endian.isLittleEndian()) { + buffer8.reverse(); + } + return buffer8; +} + +function sendPacket(websocket, id, raw_data) { + const packet_u8 = concatU8Array( + concatU8Array(u64ToByteArrayBigEndian(id), u64ToByteArrayBigEndian(BigInt(raw_data.length))), + raw_data + ); + const packet_buffer = packet_u8.buffer; + console.log('Sending packet'); + websocket.send(packet_buffer); +} + + function sendHello(websocket, rom_array, savestate_array) { + console.log('Sending hello.'); + const length_rom = BigInt(rom_array.length); + const length_savestate = BigInt(savestate_array.length); + const raw_data = + concatU8Array( + concatU8Array( + concatU8Array(u64ToByteArrayBigEndian(length_rom), rom_array), + u64ToByteArrayBigEndian(length_savestate) + ), + savestate_array + ); + sendPacket(websocket, PACKET_ID_HELLO, raw_data); +} + +function onWebSocketPacket(event, canvas, ctx) { + const buffer = event.data; + let packet_u8 = new Uint8Array(buffer); + const id = byteArrayToU64BigEndian(packet_u8.slice(0, 8)); + packet_u8 = packet_u8.slice(8, packet_u8.length); + const size = 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); + break; + default: + console.log(`Received unknown packet ${id}`); + } +} + +let printing_frame = false; +function handleSendFrame(raw_data, canvas, ctx) { + if (printing_frame) { + return; + } + printing_frame = true; + let data = raw_data; + const stride = byteArrayToU32BigEndian(data.slice(0, 4)); + data = data.slice(4, data.length); + const output_buffer_size = byteArrayToU32BigEndian(data.slice(0, 8)); + data = data.slice(8, data.length); + console.log(data.length / 4 / MIN_WIDTH); + 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)); +} + +function byteArrayToU32BigEndian(input_array) { + if (Endian.isLittleEndian()) { + input_array = input_array.reverse(); + } + const buffer = input_array.buffer; + const output_u32_array = new Uint32Array(buffer); + return output_u32_array[0]; +} + +function byteArrayToU64BigEndian(input_array) { + if (Endian.isLittleEndian()) { + input_array = input_array.reverse(); + } + const buffer = input_array.buffer; + const output_u64_array = new BigUint64Array(buffer); + return output_u64_array[0]; +} + +function drawBitmap(bitmap, canvas, ctx) { + ctx.drawImage(bitmap, 0, 0, canvas1.width, canvas1.height); + printing_frame = false; +} + +export default function Page() { + const screenDimensions = useScreenDimensions(); + const emulatorDimensions = calculateSizeEmulator(screenDimensions); + const canvasRef = React.useRef(null); + function resizeCanvas(node) { + const canvas = canvasRef.current; + if (canvas) { + canvas.width = emulatorDimensions.width; + canvas.height = emulatorDimensions.height; + const ctx = canvas.getContext('2d') + fillBlack(canvas, ctx); + } + }; + const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState(false); + React.useEffect(resizeCanvas, [emulatorDimensions]); + const refInputRom = React.useRef(null); + const refInputSaveState = React.useRef(null); + const onStartEmulation = (e) => { + handleClickStartEmulationButton({ + e: e, + setHiddenFormSelectFiles: setHiddenFormSelectFiles, + inputRom: refInputRom.current, + inputSaveState: refInputSaveState.current, + canvas: canvasRef.current, + }); + }; + return ( +
+ +

msGBA Emulator Online for GBA.

+
+ + + + + +
+ ); +} + +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, ctx) { + ctx.beginPath(); + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'black'; + ctx.fill(); +} + +function calculateSizeEmulator(screenDimensions) { + const width = screenDimensions.width; + const height = screenDimensions.height * 0.75; + const emulatorDimensions = {}; + if (width < MIN_WIDTH || height < MIN_HEIGHT) { + return { + width: MIN_WIDTH, + height: MIN_HEIGHT, + }; + } + const ratioWidth = Math.floor(width / MIN_WIDTH); + const ratioHeight = Math.floor(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; +} diff --git a/js-src/constants.js b/js-src/constants.js new file mode 100644 index 0000000..61435fe --- /dev/null +++ b/js-src/constants.js @@ -0,0 +1,10 @@ +export const MIN_WIDTH = 240; +export const MIN_HEIGHT = 160; +export const PACKET_ID_HELLO = 0n; +export const PACKET_ID_SEND_FRAME = 1n; +export default class Constants { +}; +Constants.MIN_WIDTH = MIN_WIDTH; +Constants.MIN_HEIGHT = MIN_HEIGHT; +Constants.PACKET_ID_HELLO = PACKET_ID_HELLO; +Constants.PACKET_ID_SEND_FRAME = PACKET_ID_SEND_FRAME; diff --git a/js-src/endian.js b/js-src/endian.js new file mode 100644 index 0000000..510fd60 --- /dev/null +++ b/js-src/endian.js @@ -0,0 +1,18 @@ +"use strict"; + +let littleEndian = true; +(()=>{ + let buf = new ArrayBuffer(4); + let buf8 = new Uint8ClampedArray(buf); + let data = new Uint32Array(buf); + data[0] = 0xdeadbeef; + if(buf8[0] === 0xde){ + littleEndian = false; + } +})() + +export default class Endian { + static isLittleEndian() { + return littleEndian; + } +} diff --git a/js-src/index.jsx b/js-src/index.jsx new file mode 100644 index 0000000..90f0a33 --- /dev/null +++ b/js-src/index.jsx @@ -0,0 +1,12 @@ +"use strict"; +import React from 'react'; +import ReactDOMClient from 'react-dom/client'; + +import Endian from '/endian'; +import Page from '/components/page'; + +const body = document.querySelector('body'); +const app = document.createElement('div'); +body.appendChild(app); +const root = ReactDOMClient.createRoot(app); +root.render(); diff --git a/lib/MSGBA/Web.pm b/lib/MSGBA/Web.pm index 57bce0f..52b1bc3 100644 --- a/lib/MSGBA/Web.pm +++ b/lib/MSGBA/Web.pm @@ -14,8 +14,12 @@ sub startup ($self) { my $r = $self->routes; # Normal route to controller - $r->get('/')->to('Static#index'); + #$r->get('/')->to('Static#index'); $r->websocket('/ws')->to('WS#proxy'); + $r->get('/', => sub { + my $c = shift; + $c->reply->static('index.html'); + }); } 1; diff --git a/lib/MSGBA/Web/Controller/WS.pm b/lib/MSGBA/Web/Controller/WS.pm index eda782f..078eedf 100644 --- a/lib/MSGBA/Web/Controller/WS.pm +++ b/lib/MSGBA/Web/Controller/WS.pm @@ -58,7 +58,7 @@ sub _build_msgba_connection { if (length $tx_buffer->{$ws->tx} < 16 + $size_num) { return; } - read $fh, my $raw_data, $size_num; + read $fh, my ($raw_data), $size_num; $tx_buffer->{$ws->tx} = substr $tx_buffer->{$ws->tx}, 16 + $size_num, length $tx_buffer->{$ws->tx}; handle_packet($ws, { id => $id, diff --git a/package.json b/package.json new file mode 100644 index 0000000..d005f80 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "MSGBA-Web", + "version": "0.1.1", + "description": "", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@babel/preset-react": "^7.18.6", + "babel-loader": "^9.1.2", + "file-loader": "^6.2.0", + "url-loader": "^4.1.1", + "webpack": "^5.38.1", + "webpack-cli": "^4.7.2" + }, + "dependencies": { + "babel-preset-react": "^6.24.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/public/css/styles.css b/public/css/styles.css new file mode 100644 index 0000000..120cc68 --- /dev/null +++ b/public/css/styles.css @@ -0,0 +1,17 @@ +body { + min-height: 100vh; +} + +body > div, body > div > div { + min-height: 100vh; +} + +body +.center-content { + display: flex; + justify-content: center; +} +form label, form input { + display: block; +} + diff --git a/public/index.html b/public/index.html index e74bb5f..2b02c68 100644 --- a/public/index.html +++ b/public/index.html @@ -1,11 +1,9 @@ - - Welcome to the Mojolicious real-time web framework! - - -

Welcome to the Mojolicious real-time web framework!

- This is the static document "public/index.html", - click here to get back to the start. - + + + + + + diff --git a/public/js/bundle.js b/public/js/bundle.js new file mode 100644 index 0000000..affbffa --- /dev/null +++ b/public/js/bundle.js @@ -0,0 +1,240 @@ +/* + * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). + * This devtool is neither made for production nor for readable output files. + * It uses "eval()" calls to create a separate source file in the browser devtools. + * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) + * or disable the default devtool with "devtool: false". + * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). + */ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ "./js-src/components/canvas-gba-emulator.jsx": +/*!***************************************************!*\ + !*** ./js-src/components/canvas-gba-emulator.jsx ***! + \***************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ CanvasGBAEmulator)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../../constants */ \"./js-src/constants.js\");\n\n\nfunction CanvasGBAEmulator(props) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"canvas\", {\n ref: props.canvasRef,\n width: _constants__WEBPACK_IMPORTED_MODULE_1__.MIN_WIDTH,\n height: _constants__WEBPACK_IMPORTED_MODULE_1__.MIN_HEIGHT\n });\n}\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/components/canvas-gba-emulator.jsx?"); + +/***/ }), + +/***/ "./js-src/components/center-element.jsx": +/*!**********************************************!*\ + !*** ./js-src/components/center-element.jsx ***! + \**********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ CenterElement)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nfunction CenterElement(props) {\n let hidden = props.hidden;\n if (hidden == null) {\n hidden = false;\n }\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: hidden ? 'none' : ''\n },\n className: \"center-content\"\n }, props.children);\n}\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/components/center-element.jsx?"); + +/***/ }), + +/***/ "./js-src/components/form-select-files.jsx": +/*!*************************************************!*\ + !*** ./js-src/components/form-select-files.jsx ***! + \*************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ FormSelectFiles)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nfunction FormSelectFiles(props) {\n const inputRom = props.refInputRom ? props.refInputRom : react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);\n const inputSaveState = props.refInputSaveState ? props.refInputSaveState : react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);\n const onStartEmulation = props.onStartEmulation;\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"form\", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n htmlFor: \"rom\"\n }, \"Rom file\", /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"file\",\n ref: inputRom,\n name: \"rom\"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n htmlFor: \"savestate\"\n }, \"Savestate (A ss file from mgba...)\", /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"file\",\n ref: inputSaveState,\n name: \"savestate\"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"button\",\n value: \"Start emulation\",\n onClick: onStartEmulation,\n ref: props.startEmulationButton\n }));\n}\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/components/form-select-files.jsx?"); + +/***/ }), + +/***/ "./js-src/components/page.jsx": +/*!************************************!*\ + !*** ./js-src/components/page.jsx ***! + \************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Page)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _components_center_element__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../../components/center-element */ \"./js-src/components/center-element.jsx\");\n/* harmony import */ var _components_form_select_files__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../components/form-select-files */ \"./js-src/components/form-select-files.jsx\");\n/* harmony import */ var _components_canvas_gba_emulator__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../../components/canvas-gba-emulator */ \"./js-src/components/canvas-gba-emulator.jsx\");\n/* harmony import */ var _endian__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../../endian */ \"./js-src/endian.js\");\n/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../../constants */ \"./js-src/constants.js\");\n\n\n\n\n\n\nfunction handleClickStartEmulationButton({\n e,\n inputRom,\n inputSaveState,\n setHiddenFormSelectFiles,\n canvas\n}) {\n const ctx = canvas.getContext('2d');\n e.preventDefault();\n if (inputRom.files.length == 0) {\n alert('There is no rom still');\n return;\n }\n if (inputSaveState.files.length == 0) {\n alert('There is no savestate still');\n return;\n }\n const rom_file = inputRom.files[0];\n const savestate_file = inputSaveState.files[0];\n rom_file.arrayBuffer().then(rom_buffer => {\n savestate_file.arrayBuffer().then(savestate_buffer => {\n const rom_array = new Uint8Array(rom_buffer);\n const savestate_array = new Uint8Array(savestate_buffer);\n const websocket = new WebSocket(`ws://localhost:3000/ws`);\n websocket.binaryType = 'arraybuffer';\n websocket.onclose = message => console.log('CLOSE', message);\n websocket.onopen = () => {\n setHiddenFormSelectFiles(c => false);\n console.log('Opened websocket.');\n sendHello(websocket, rom_array, savestate_array);\n };\n websocket.addEventListener('message', event => onWebSocketPacket(event, canvas, ctx));\n });\n });\n}\nfunction concatU8Array(array1, array2) {\n const final_array = new Uint8Array(array1.length + array2.length);\n final_array.set(array1);\n final_array.set(array2, array1.length);\n return final_array;\n}\nfunction u64ToByteArrayBigEndian(input_number) {\n const buffer = new ArrayBuffer(8);\n const buffer8 = new Uint8Array(buffer);\n const buffer64 = new BigUint64Array(buffer);\n buffer64[0] = input_number;\n if (_endian__WEBPACK_IMPORTED_MODULE_4__[\"default\"].isLittleEndian()) {\n buffer8.reverse();\n }\n return buffer8;\n}\nfunction sendPacket(websocket, id, raw_data) {\n const packet_u8 = concatU8Array(concatU8Array(u64ToByteArrayBigEndian(id), u64ToByteArrayBigEndian(BigInt(raw_data.length))), raw_data);\n const packet_buffer = packet_u8.buffer;\n console.log('Sending packet');\n websocket.send(packet_buffer);\n}\nfunction sendHello(websocket, rom_array, savestate_array) {\n console.log('Sending hello.');\n const length_rom = BigInt(rom_array.length);\n const length_savestate = BigInt(savestate_array.length);\n const raw_data = concatU8Array(concatU8Array(concatU8Array(u64ToByteArrayBigEndian(length_rom), rom_array), u64ToByteArrayBigEndian(length_savestate)), savestate_array);\n sendPacket(websocket, _constants__WEBPACK_IMPORTED_MODULE_5__.PACKET_ID_HELLO, raw_data);\n}\nfunction onWebSocketPacket(event, canvas, ctx) {\n const buffer = event.data;\n let packet_u8 = new Uint8Array(buffer);\n const id = byteArrayToU64BigEndian(packet_u8.slice(0, 8));\n packet_u8 = packet_u8.slice(8, packet_u8.length);\n const size = byteArrayToU64BigEndian(packet_u8.slice(0, 8));\n const raw_data = packet_u8.slice(8, packet_u8.length);\n packet_u8 = null;\n switch (id) {\n case _constants__WEBPACK_IMPORTED_MODULE_5__.PACKET_ID_SEND_FRAME:\n handleSendFrame(raw_data, canvas, ctx);\n break;\n default:\n console.log(`Received unknown packet ${id}`);\n }\n}\nlet printing_frame = false;\nfunction handleSendFrame(raw_data, canvas, ctx) {\n if (printing_frame) {\n return;\n }\n printing_frame = true;\n let data = raw_data;\n const stride = byteArrayToU32BigEndian(data.slice(0, 4));\n data = data.slice(4, data.length);\n const output_buffer_size = byteArrayToU32BigEndian(data.slice(0, 8));\n data = data.slice(8, data.length);\n console.log(data.length / 4 / _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH);\n const img_data = ctx.createImageData(_constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH, _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_HEIGHT);\n const img_data_u8 = new Uint8Array(img_data.data.buffer);\n for (let i = 0; i < data.length; i++) {\n if (i % 4 == 3) {\n img_data_u8[i] = 255;\n continue;\n }\n img_data_u8[i] = data[i];\n }\n data = null;\n createImageBitmap(img_data).then(bitmap => drawBitmap(bitmap, canvas, ctx));\n}\nfunction byteArrayToU32BigEndian(input_array) {\n if (_endian__WEBPACK_IMPORTED_MODULE_4__[\"default\"].isLittleEndian()) {\n input_array = input_array.reverse();\n }\n const buffer = input_array.buffer;\n const output_u32_array = new Uint32Array(buffer);\n return output_u32_array[0];\n}\nfunction byteArrayToU64BigEndian(input_array) {\n if (_endian__WEBPACK_IMPORTED_MODULE_4__[\"default\"].isLittleEndian()) {\n input_array = input_array.reverse();\n }\n const buffer = input_array.buffer;\n const output_u64_array = new BigUint64Array(buffer);\n return output_u64_array[0];\n}\nfunction drawBitmap(bitmap, canvas, ctx) {\n ctx.drawImage(bitmap, 0, 0, canvas1.width, canvas1.height);\n printing_frame = false;\n}\nfunction Page() {\n const screenDimensions = useScreenDimensions();\n const emulatorDimensions = calculateSizeEmulator(screenDimensions);\n const canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);\n function resizeCanvas(node) {\n const canvas = canvasRef.current;\n if (canvas) {\n canvas.width = emulatorDimensions.width;\n canvas.height = emulatorDimensions.height;\n const ctx = canvas.getContext('2d');\n fillBlack(canvas, ctx);\n }\n }\n ;\n const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);\n react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(resizeCanvas, [emulatorDimensions]);\n const refInputRom = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);\n const refInputSaveState = react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null);\n const onStartEmulation = e => {\n handleClickStartEmulationButton({\n e: e,\n setHiddenFormSelectFiles: setHiddenFormSelectFiles,\n inputRom: refInputRom.current,\n inputSaveState: refInputSaveState.current,\n canvas: canvasRef.current\n });\n };\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_center_element__WEBPACK_IMPORTED_MODULE_1__[\"default\"], null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h2\", null, \"msGBA Emulator Online for GBA.\")), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_center_element__WEBPACK_IMPORTED_MODULE_1__[\"default\"], null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_canvas_gba_emulator__WEBPACK_IMPORTED_MODULE_3__[\"default\"], {\n canvasRef: canvasRef\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_center_element__WEBPACK_IMPORTED_MODULE_1__[\"default\"], {\n hidden: hiddenFormSelectFiles\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_form_select_files__WEBPACK_IMPORTED_MODULE_2__[\"default\"], {\n refInputRom: refInputRom,\n refInputSaveState: refInputSaveState,\n onStartEmulation: onStartEmulation\n })));\n}\nfunction getScreenDimensions() {\n return {\n width: document.body.clientWidth,\n height: document.body.clientHeight\n };\n}\nfunction useScreenDimensions() {\n const [screenDimensions, setScreenDimensions] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(getScreenDimensions());\n react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {\n function onResize() {\n setScreenDimensions(getScreenDimensions());\n }\n window.addEventListener(\"resize\", onResize);\n return () => {\n window.removeEventListener(\"resize\", onResize);\n };\n }, []);\n return screenDimensions;\n}\nfunction fillBlack(canvas, ctx) {\n ctx.beginPath();\n ctx.rect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = 'black';\n ctx.fill();\n}\nfunction calculateSizeEmulator(screenDimensions) {\n const width = screenDimensions.width;\n const height = screenDimensions.height * 0.75;\n const emulatorDimensions = {};\n if (width < _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH || height < _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_HEIGHT) {\n return {\n width: _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH,\n height: _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_HEIGHT\n };\n }\n const ratioWidth = Math.floor(width / _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH);\n const ratioHeight = Math.floor(height / _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_HEIGHT);\n if (ratioWidth < ratioHeight) {\n emulatorDimensions.width = _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH * ratioWidth;\n emulatorDimensions.height = _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_HEIGHT * ratioWidth;\n } else {\n emulatorDimensions.height = _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_HEIGHT * ratioHeight;\n emulatorDimensions.width = _constants__WEBPACK_IMPORTED_MODULE_5__.MIN_WIDTH * ratioHeight;\n }\n return emulatorDimensions;\n}\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/components/page.jsx?"); + +/***/ }), + +/***/ "./js-src/constants.js": +/*!*****************************!*\ + !*** ./js-src/constants.js ***! + \*****************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"MIN_HEIGHT\": () => (/* binding */ MIN_HEIGHT),\n/* harmony export */ \"MIN_WIDTH\": () => (/* binding */ MIN_WIDTH),\n/* harmony export */ \"PACKET_ID_HELLO\": () => (/* binding */ PACKET_ID_HELLO),\n/* harmony export */ \"PACKET_ID_SEND_FRAME\": () => (/* binding */ PACKET_ID_SEND_FRAME),\n/* harmony export */ \"default\": () => (/* binding */ Constants)\n/* harmony export */ });\nconst MIN_WIDTH = 240;\nconst MIN_HEIGHT = 160;\nconst PACKET_ID_HELLO = 0n;\nconst PACKET_ID_SEND_FRAME = 1n;\nclass Constants {}\n;\nConstants.MIN_WIDTH = MIN_WIDTH;\nConstants.MIN_HEIGHT = MIN_HEIGHT;\nConstants.PACKET_ID_HELLO = PACKET_ID_HELLO;\nConstants.PACKET_ID_SEND_FRAME = PACKET_ID_SEND_FRAME;\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/constants.js?"); + +/***/ }), + +/***/ "./js-src/endian.js": +/*!**************************!*\ + !*** ./js-src/endian.js ***! + \**************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Endian)\n/* harmony export */ });\n\n\nlet littleEndian = true;\n(() => {\n let buf = new ArrayBuffer(4);\n let buf8 = new Uint8ClampedArray(buf);\n let data = new Uint32Array(buf);\n data[0] = 0xdeadbeef;\n if (buf8[0] === 0xde) {\n littleEndian = false;\n }\n})();\nclass Endian {\n static isLittleEndian() {\n return littleEndian;\n }\n}\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/endian.js?"); + +/***/ }), + +/***/ "./js-src/index.jsx": +/*!**************************!*\ + !*** ./js-src/index.jsx ***! + \**************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_dom_client__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-dom/client */ \"./node_modules/react-dom/client.js\");\n/* harmony import */ var _endian__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../endian */ \"./js-src/endian.js\");\n/* harmony import */ var _components_page__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../../components/page */ \"./js-src/components/page.jsx\");\n\n\n\n\n\n\nconst body = document.querySelector('body');\nconst app = document.createElement('div');\nbody.appendChild(app);\nconst root = react_dom_client__WEBPACK_IMPORTED_MODULE_1__.createRoot(app);\nroot.render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_page__WEBPACK_IMPORTED_MODULE_3__[\"default\"], null));\n\n//# sourceURL=webpack://MSGBA-Web/./js-src/index.jsx?"); + +/***/ }), + +/***/ "./node_modules/react-dom/cjs/react-dom.development.js": +/*!*************************************************************!*\ + !*** ./node_modules/react-dom/cjs/react-dom.development.js ***! + \*************************************************************/ +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +eval("/**\n * @license React\n * react-dom.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nif (true) {\n (function() {\n\n 'use strict';\n\n/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */\nif (\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===\n 'function'\n) {\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());\n}\n var React = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\nvar Scheduler = __webpack_require__(/*! scheduler */ \"./node_modules/scheduler/index.js\");\n\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar suppressWarning = false;\nfunction setSuppressWarning(newSuppressWarning) {\n {\n suppressWarning = newSuppressWarning;\n }\n} // In DEV, calls to console.warn and console.error get replaced\n// by calls to these methods by a Babel plugin.\n//\n// In PROD (or in packages without access to React internals),\n// they are left as they are instead.\n\nfunction warn(format) {\n {\n if (!suppressWarning) {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n\n printWarning('warn', format, args);\n }\n }\n}\nfunction error(format) {\n {\n if (!suppressWarning) {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n\n printWarning('error', format, args);\n }\n }\n}\n\nfunction printWarning(level, format, args) {\n // When changing this logic, you might want to also\n // update consoleWithStackDev.www.js as well.\n {\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n var stack = ReactDebugCurrentFrame.getStackAddendum();\n\n if (stack !== '') {\n format += '%s';\n args = args.concat([stack]);\n } // eslint-disable-next-line react-internal/safe-string-coercion\n\n\n var argsWithFormat = args.map(function (item) {\n return String(item);\n }); // Careful: RN currently depends on this prefix\n\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\n // breaks IE9: https://github.com/facebook/react/issues/13610\n // eslint-disable-next-line react-internal/no-production-logging\n\n Function.prototype.apply.call(console[level], console, argsWithFormat);\n }\n}\n\nvar FunctionComponent = 0;\nvar ClassComponent = 1;\nvar IndeterminateComponent = 2; // Before we know whether it is function or class\n\nvar HostRoot = 3; // Root of a host tree. Could be nested inside another node.\n\nvar HostPortal = 4; // A subtree. Could be an entry point to a different renderer.\n\nvar HostComponent = 5;\nvar HostText = 6;\nvar Fragment = 7;\nvar Mode = 8;\nvar ContextConsumer = 9;\nvar ContextProvider = 10;\nvar ForwardRef = 11;\nvar Profiler = 12;\nvar SuspenseComponent = 13;\nvar MemoComponent = 14;\nvar SimpleMemoComponent = 15;\nvar LazyComponent = 16;\nvar IncompleteClassComponent = 17;\nvar DehydratedFragment = 18;\nvar SuspenseListComponent = 19;\nvar ScopeComponent = 21;\nvar OffscreenComponent = 22;\nvar LegacyHiddenComponent = 23;\nvar CacheComponent = 24;\nvar TracingMarkerComponent = 25;\n\n// -----------------------------------------------------------------------------\n\nvar enableClientRenderFallbackOnTextMismatch = true; // TODO: Need to review this code one more time before landing\n// the react-reconciler package.\n\nvar enableNewReconciler = false; // Support legacy Primer support on internal FB www\n\nvar enableLazyContextPropagation = false; // FB-only usage. The new API has different semantics.\n\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\n\nvar enableSuspenseAvoidThisFallback = false; // Enables unstable_avoidThisFallback feature in Fizz\n// React DOM Chopping Block\n//\n// Similar to main Chopping Block but only flags related to React DOM. These are\n// grouped because we will likely batch all of them into a single major release.\n// -----------------------------------------------------------------------------\n// Disable support for comment nodes as React DOM containers. Already disabled\n// in open source, but www codebase still relies on it. Need to remove.\n\nvar disableCommentsAsDOMContainers = true; // Disable javascript: URL strings in href for XSS protection.\n// and client rendering, mostly to allow JSX attributes to apply to the custom\n// element's object properties instead of only HTML attributes.\n// https://github.com/facebook/react/issues/11347\n\nvar enableCustomElementPropertySupport = false; // Disables children for