Migrating the project to typescript.

This commit is contained in:
Sergiotarxz 2023-03-22 13:53:16 +01:00
parent d7b9d9d664
commit 13a948b21a
15 changed files with 292 additions and 201 deletions

View File

@ -1,7 +0,0 @@
import React from 'react';
import {MIN_WIDTH, MIN_HEIGHT} from '/constants';
export default function CanvasGBAEmulator(props) {
return (<canvas ref={props.canvasRef} width={MIN_WIDTH} height={MIN_HEIGHT}></canvas>);
}

View File

@ -0,0 +1,11 @@
import * as React from 'react';
import {MIN_WIDTH, MIN_HEIGHT} from '/constants';
export interface CanvasGBAEmulatorProps {
canvasRef: React.RefObject<HTMLCanvasElement>
}
export default function CanvasGBAEmulator(props: CanvasGBAEmulatorProps) {
return (<canvas ref={props.canvasRef} width={MIN_WIDTH} height={MIN_HEIGHT}></canvas>);
}

View File

@ -1,13 +0,0 @@
import React from 'react';
export default function CenterElement(props) {
let hidden = props.hidden;
if (hidden == null) {
hidden = false;
}
return (
<div style={{
display: hidden ? 'none':''
}} className="center-content">{props.children}</div>
);
}

View File

@ -0,0 +1,22 @@
import * as React from 'react';
export interface CenterElementProps {
hidden?: boolean | undefined,
children?: React.ReactNode,
}
type IHash = {
[id: string]: string;
}
export default function CenterElement(props: CenterElementProps) {
const styles: IHash = {};
let hidden = props.hidden;
if (hidden == null) {
hidden = false;
}
styles["display"] = hidden ? 'none' : '';
return (
<div style={styles} className="center-content">{props.children}</div>
);
}

View File

@ -1,6 +1,12 @@
import React from 'react'; import * as React from 'react';
export default function FormSelectFiles(props) { export interface FormSelectFilesProps {
refInputRom: React.RefObject<HTMLInputElement>;
refInputSaveState: React.RefObject<HTMLInputElement>;
onStartEmulation: React.MouseEventHandler<HTMLInputElement>;
}
export default function FormSelectFiles(props: FormSelectFilesProps) {
const inputRom = props.refInputRom ? props.refInputRom : React.useRef(null); const inputRom = props.refInputRom ? props.refInputRom : React.useRef(null);
const inputSaveState = props.refInputSaveState ? props.refInputSaveState : React.useRef(null); const inputSaveState = props.refInputSaveState ? props.refInputSaveState : React.useRef(null);
const onStartEmulation = props.onStartEmulation; const onStartEmulation = props.onStartEmulation;
@ -15,7 +21,7 @@ export default function FormSelectFiles(props) {
Savestate (A ss file from mgba...) Savestate (A ss file from mgba...)
<input type="file" ref={inputSaveState} name="savestate"/> <input type="file" ref={inputSaveState} name="savestate"/>
</label> </label>
<input type="button" value="Start emulation" onClick={onStartEmulation} ref={props.startEmulationButton}/> <input type="button" value="Start emulation" onClick={onStartEmulation}/>
</form> </form>
); );
} }

View File

@ -1,4 +1,4 @@
import React from 'react'; import * as React from 'react';
import CenterElement from '/components/center-element'; import CenterElement from '/components/center-element';
import FormSelectFiles from '/components/form-select-files'; import FormSelectFiles from '/components/form-select-files';
@ -8,9 +8,32 @@ import Endian from '/endian';
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants'; import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants';
function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHiddenFormSelectFiles, canvas}) { type setBooleanCallback = (c: boolean) => boolean;
export interface handleClickStartEmulationButtonObjectArgs {
e: React.MouseEvent<HTMLInputElement>;
inputRom: HTMLInputElement | null;
inputSaveState: HTMLInputElement | null;
setHiddenFormSelectFiles: (c: setBooleanCallback) => void;
canvas: HTMLCanvasElement | null;
printingFrame: boolean;
setPrintingFrame: (c: setBooleanCallback) => void;
};
function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHiddenFormSelectFiles, canvas, printingFrame, setPrintingFrame}: handleClickStartEmulationButtonObjectArgs) {
if (canvas == null) {
alert('Canvas does not exists?');
return;
}
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
e.preventDefault(); 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) { if (inputRom.files.length == 0) {
alert('There is no rom still'); alert('There is no rom still');
return; return;
@ -23,42 +46,37 @@ function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHidden
const savestate_file = inputSaveState.files[0]; const savestate_file = inputSaveState.files[0];
rom_file.arrayBuffer().then((rom_buffer) => { rom_file.arrayBuffer().then((rom_buffer) => {
savestate_file.arrayBuffer().then((savestate_buffer) => { savestate_file.arrayBuffer().then((savestate_buffer) => {
setHiddenFormSelectFiles((c: boolean) => true);
const rom_array = new Uint8Array(rom_buffer); const rom_array = new Uint8Array(rom_buffer);
const savestate_array = new Uint8Array(savestate_buffer); const savestate_array = new Uint8Array(savestate_buffer);
const websocket = new WebSocket(`ws://localhost:3000/ws`); const websocket = new WebSocket(`ws://localhost:3000/ws`);
websocket.binaryType = 'arraybuffer'; websocket.binaryType = 'arraybuffer';
websocket.onclose = (message) => console.log('CLOSE', message); websocket.onclose = (message) => {
websocket.onopen = () => {
setHiddenFormSelectFiles(c => false); setHiddenFormSelectFiles(c => false);
console.log('Closing websocket.');
}
websocket.onopen = () => {
console.log('Opened websocket.'); console.log('Opened websocket.');
sendHello(websocket, rom_array, savestate_array); sendHello(websocket, rom_array, savestate_array);
}; };
websocket.addEventListener('message', (event) => onWebSocketPacket(event, canvas, ctx)); setPrintingFrame(c => false);
websocket.addEventListener('message', (event) => {
onWebSocketPacket(event, canvas, ctx, printingFrame, setPrintingFrame)
});
}); });
}); });
} }
function concatU8Array(array1, array2) { function concatU8Array(array1: Uint8Array, array2: Uint8Array) {
const final_array = new Uint8Array(array1.length + array2.length); const final_array = new Uint8Array(array1.length + array2.length);
final_array.set(array1); final_array.set(array1);
final_array.set(array2, array1.length); final_array.set(array2, array1.length);
return final_array; return final_array;
} }
function u64ToByteArrayBigEndian(input_number) { function sendPacket(websocket: WebSocket, id: bigint, raw_data: Uint8Array) {
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( const packet_u8 = concatU8Array(
concatU8Array(u64ToByteArrayBigEndian(id), u64ToByteArrayBigEndian(BigInt(raw_data.length))), concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(raw_data.length))),
raw_data raw_data
); );
const packet_buffer = packet_u8.buffer; const packet_buffer = packet_u8.buffer;
@ -66,50 +84,48 @@ function sendPacket(websocket, id, raw_data) {
websocket.send(packet_buffer); websocket.send(packet_buffer);
} }
function sendHello(websocket, rom_array, savestate_array) { function sendHello(websocket: WebSocket, rom_array: Uint8Array, savestate_array: Uint8Array) {
console.log('Sending hello.'); console.log('Sending hello.');
const length_rom = BigInt(rom_array.length); const length_rom = BigInt(rom_array.length);
const length_savestate = BigInt(savestate_array.length); const length_savestate = BigInt(savestate_array.length);
const raw_data = const raw_data =
concatU8Array( concatU8Array(
concatU8Array( concatU8Array(
concatU8Array(u64ToByteArrayBigEndian(length_rom), rom_array), concatU8Array(Endian.u64ToByteArrayBigEndian(length_rom), rom_array),
u64ToByteArrayBigEndian(length_savestate) Endian.u64ToByteArrayBigEndian(length_savestate)
), ),
savestate_array savestate_array
); );
sendPacket(websocket, PACKET_ID_HELLO, raw_data); sendPacket(websocket, PACKET_ID_HELLO, raw_data);
} }
function onWebSocketPacket(event, canvas, ctx) { function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) {
const buffer = event.data; const buffer = event.data;
let packet_u8 = new Uint8Array(buffer); let packet_u8: Uint8Array | null = new Uint8Array(buffer);
const id = byteArrayToU64BigEndian(packet_u8.slice(0, 8)); const id = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8));
packet_u8 = packet_u8.slice(8, packet_u8.length); packet_u8 = packet_u8.slice(8, packet_u8.length);
const size = byteArrayToU64BigEndian(packet_u8.slice(0, 8)); const size = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8));
const raw_data = packet_u8.slice(8, packet_u8.length); const raw_data = packet_u8.slice(8, packet_u8.length);
packet_u8 = null; packet_u8 = null;
switch (id) { switch (id) {
case PACKET_ID_SEND_FRAME: case PACKET_ID_SEND_FRAME:
handleSendFrame(raw_data, canvas, ctx); handleSendFrame(raw_data, canvas, ctx, printingFrame, setPrintingFrame);
break; break;
default: default:
console.log(`Received unknown packet ${id}`); console.log(`Received unknown packet ${id}`);
} }
} }
let printing_frame = false; function handleSendFrame(raw_data: Uint8Array, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) {
function handleSendFrame(raw_data, canvas, ctx) { if (printingFrame) {
if (printing_frame) {
return; return;
} }
printing_frame = true; setPrintingFrame(c => true);
let data = raw_data; let data: Uint8Array | null = raw_data;
const stride = byteArrayToU32BigEndian(data.slice(0, 4)); const stride = Endian.byteArrayToU32BigEndian(data.slice(0, 4));
data = data.slice(4, data.length); data = data.slice(4, data.length);
const output_buffer_size = byteArrayToU32BigEndian(data.slice(0, 8)); const output_buffer_size = Endian.byteArrayToU64BigEndian(data.slice(0, 8));
data = data.slice(8, data.length); 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 = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT);
const img_data_u8 = new Uint8Array(img_data.data.buffer); const img_data_u8 = new Uint8Array(img_data.data.buffer);
for (let i = 0; i<data.length; i++) { for (let i = 0; i<data.length; i++) {
@ -120,56 +136,48 @@ function handleSendFrame(raw_data, canvas, ctx) {
img_data_u8[i] = data[i]; img_data_u8[i] = data[i];
} }
data = null; data = null;
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx)); createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx, printingFrame));
} }
function byteArrayToU32BigEndian(input_array) { function drawBitmap(bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean) {
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, canvas.width, canvas.height); ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
printing_frame = false; printingFrame = false;
} }
export default function Page() { export default function Page() {
const screenDimensions = useScreenDimensions(); const screenDimensions = useScreenDimensions();
const emulatorDimensions = calculateSizeEmulator(screenDimensions); const emulatorDimensions = calculateSizeEmulator(screenDimensions);
const canvasRef = React.useRef(null); const canvasRef = React.useRef<HTMLCanvasElement>(null);
function resizeCanvas(node) { function resizeCanvas() {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (canvas) { if (canvas == null) {
canvas.width = emulatorDimensions.width; return;
canvas.height = emulatorDimensions.height;
const ctx = canvas.getContext('2d')
fillBlack(canvas, ctx);
} }
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(false); const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState<boolean>(false);
const [printingFrame, setPrintingFrame] = React.useState<boolean>(false);
React.useEffect(resizeCanvas, [emulatorDimensions]); React.useEffect(resizeCanvas, [emulatorDimensions]);
const refInputRom = React.useRef(null); const refInputRom = React.useRef<HTMLInputElement | null>(null);
const refInputSaveState = React.useRef(null); const refInputSaveState = React.useRef<HTMLInputElement | null>(null);
const onStartEmulation = (e) => { const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>) => {
handleClickStartEmulationButton({ handleClickStartEmulationButton({
e: e, e: e,
setHiddenFormSelectFiles: setHiddenFormSelectFiles, setHiddenFormSelectFiles: setHiddenFormSelectFiles,
inputRom: refInputRom.current, inputRom: refInputRom.current,
inputSaveState: refInputSaveState.current, inputSaveState: refInputSaveState.current,
canvas: canvasRef.current, canvas: canvasRef.current,
setPrintingFrame: setPrintingFrame,
printingFrame: printingFrame,
}); });
}; };
return ( return (
@ -198,7 +206,7 @@ function getScreenDimensions() {
} }
function useScreenDimensions() { function useScreenDimensions() {
const [screenDimensions, setScreenDimensions] = React.useState(getScreenDimensions()); const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions());
React.useEffect(() => { React.useEffect(() => {
function onResize() { function onResize() {
@ -215,17 +223,25 @@ function useScreenDimensions() {
return screenDimensions; return screenDimensions;
} }
function fillBlack(canvas, ctx) { function fillBlack(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
ctx.beginPath(); ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height); ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
ctx.fill(); ctx.fill();
} }
function calculateSizeEmulator(screenDimensions) { 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 width = screenDimensions.width;
const height = screenDimensions.height * 0.75; const height = screenDimensions.height * 0.75;
const emulatorDimensions = {}; const emulatorDimensions: EmulatorDimensions = {};
if (width < MIN_WIDTH || height < MIN_HEIGHT) { if (width < MIN_WIDTH || height < MIN_HEIGHT) {
return { return {
width: MIN_WIDTH, width: MIN_WIDTH,

View File

@ -3,6 +3,11 @@ export const MIN_HEIGHT = 160;
export const PACKET_ID_HELLO = 0n; export const PACKET_ID_HELLO = 0n;
export const PACKET_ID_SEND_FRAME = 1n; export const PACKET_ID_SEND_FRAME = 1n;
export default class Constants { export default class Constants {
public static MIN_WIDTH: number = MIN_WIDTH;
public static MIN_HEIGHT: number = MIN_HEIGHT;
public static PACKET_ID_HELLO: bigint = PACKET_ID_HELLO;
public static PACKET_ID_SEND_FRAME: bigint = PACKET_ID_SEND_FRAME;
}; };
Constants.MIN_WIDTH = MIN_WIDTH; Constants.MIN_WIDTH = MIN_WIDTH;
Constants.MIN_HEIGHT = MIN_HEIGHT; Constants.MIN_HEIGHT = MIN_HEIGHT;

View File

@ -1,18 +0,0 @@
"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;
}
}

40
js-src/endian.ts Normal file
View File

@ -0,0 +1,40 @@
"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;
}
static byteArrayToU32BigEndian(inputArray: Uint8Array) {
if (Endian.isLittleEndian()) {
inputArray = inputArray.reverse();
}
const buffer = inputArray.buffer;
const outputU32Array = new Uint32Array(buffer);
return outputU32Array[0];
}
static byteArrayToU64BigEndian(inputArray: Uint8Array) {
if (Endian.isLittleEndian()) {
inputArray = inputArray.reverse();
}
const buffer = inputArray.buffer;
const outputU64Array = new BigUint64Array(buffer);
return outputU64Array[0];
}
static u64ToByteArrayBigEndian(inputNumber: bigint) {
const buffer = new ArrayBuffer(8);
const buffer8 = new Uint8Array(buffer);
const buffer64 = new BigUint64Array(buffer);
buffer64[0] = inputNumber;
if (Endian.isLittleEndian()) {
buffer8.reverse();
}
return buffer8;
}
}

View File

@ -1,12 +0,0 @@
"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(<Page/>);

18
js-src/index.tsx Normal file
View File

@ -0,0 +1,18 @@
"use strict";
import * as React from 'react';
import * as ReactDOMClient from 'react-dom/client';
import Endian from '/endian';
import Page from '/components/page';
const body = document.querySelector('body');
if (body != null) {
fillBody(body);
}
function fillBody(body: HTMLElement) {
const app = document.createElement('div');
body.appendChild(app);
const root = ReactDOMClient.createRoot(app);
root.render(<Page/>);
}

View File

@ -11,8 +11,12 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.2",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"

File diff suppressed because one or more lines are too long

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"outDir": "./public/js/",
"noImplicitAny": true,
"module": "es2020",
"target": "es2020",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"*": ["js-src/*"]
}
},
"include": ["js-src/*.ts", "js-src/*/*.ts" ]
}

View File

@ -1,23 +1,25 @@
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
entry: './js-src/index.jsx', entry: './js-src/index.tsx',
mode: 'development', mode: 'development',
output: { output: {
filename: 'bundle.js', filename: 'bundle.js',
path: path.resolve(__dirname, 'public/js/'), path: path.resolve(__dirname, 'public/js/'),
}, },
resolve: { resolve: {
extensions: [ extensions: [ '.js', '.jsx','.ts', '.tsx' ],
'.js',
'.jsx',
],
roots: [ roots: [
path.resolve(__dirname, 'js-src/') path.resolve(__dirname, 'js-src/')
] ]
}, },
module: { module: {
rules: [ rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{ {
test: /\.jpe?g|png$/, test: /\.jpe?g|png$/,
exclude: /node_modules/, exclude: /node_modules/,