diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index c20f4cb..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,95 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const prettierConfig = require('./.prettierrc') - -module.exports = { - env: { - browser: true, - es2021: true, - node: true - }, - extends: ['plugin:react/recommended', 'airbnb', 'prettier'], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaFeatures: { - jsx: true - }, - sourceType: 'module' - }, - plugins: [ - '@typescript-eslint', - 'react', - 'react-hooks', - 'import', - 'simple-import-sort', - 'prettier' - ], - settings: { - 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], - 'import/resolver': { - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] - }, - alias: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] - } - }, - react: { - version: 'detect' - } - }, - globals: { - USE_HASH_ROUTER: 'readonly' - }, - rules: { - // Import plugin - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - tsx: 'never', - ts: 'never' - } - ], // don't require extension for these - 'import/prefer-default-export': 'off', // named exports are "better" - 'import/no-relative-packages': 'off', // there are no workspaces - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: true, - peerDependencies: true - } - ], - - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - - 'react/destructuring-assignment': 'off', // too strict - 'react/react-in-jsx-scope': 'off', // not needed anymore - 'react/jsx-filename-extension': ['error', { extensions: ['.tsx', '.jsx'] }], - 'react/function-component-definition': 'off', // seems to be broken with TS - 'react/require-default-props': 'off', // not needed with TS - - // TypeScript - '@typescript-eslint/no-unused-vars': 'error', - '@typescript-eslint/consistent-type-definitions': ['error', 'type'], // use "type" instead of "interface" - '@typescript-eslint/array-type': ['error', { default: 'array', readOnly: 'array' }], // use T[] instead of Array - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': ['error'], - 'no-shadow': 'off', - '@typescript-eslint/no-shadow': ['error'], - - // Prettier - 'prettier/prettier': ['error', prettierConfig] - }, - overrides: [ - { - files: ['*.ts', '*.tsx'], - rules: { - 'no-undef': 'off' - } - } - ] -} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index df02a48..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - bracketSameLine: true, - printWidth: 100, - semi: false, - singleQuote: true, - trailingComma: 'none' -} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..9450a2c --- /dev/null +++ b/biome.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "include": ["browser/**/*"], + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentWidth": 2, + "indentStyle": "space" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noForEach": "off" + } + } + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "quoteStyle": "single" + } + } +} diff --git a/browser/assets/styles/main.css b/browser/assets/styles/main.css index c0550dc..6e0ac29 100644 --- a/browser/assets/styles/main.css +++ b/browser/assets/styles/main.css @@ -3,47 +3,50 @@ @tailwind utilities; @font-face { - font-family: 'GillSans'; - src: url('../fonts/GillSansMediumItalic.otf'); + font-family: "GillSans"; + src: url("../fonts/GillSansMediumItalic.otf"); font-style: italic; font-weight: 500; font-display: swap; } @font-face { - font-family: 'GillSans'; - src: url('../fonts/GillSansBoldItalic.otf'); + font-family: "GillSans"; + src: url("../fonts/GillSansBoldItalic.otf"); font-style: italic; font-weight: 700; font-display: swap; } @font-face { - font-family: 'Pretendo'; - src: url('../fonts/Pretendo.ttf'); + font-family: "Pretendo"; + src: url("../fonts/Pretendo.ttf"); font-style: normal; font-weight: 400; font-display: swap; } @font-face { - font-family: 'NESController'; - src: url('../fonts/NES_Controller.ttf'); + font-family: "NESController"; + src: url("../fonts/NES_Controller.ttf"); font-style: normal; font-weight: 400; font-display: swap; } .font-nes { - font-family: 'NESController'; + /* biome-ignore lint/a11y/useGenericFontNames: */ + font-family: "NESController"; } .font-pretendo { - font-family: 'Pretendo'; + /* biome-ignore lint/a11y/useGenericFontNames: */ + font-family: "Pretendo"; } .font-gills-sans { - font-family: 'GillSans'; + /* biome-ignore lint/a11y/useGenericFontNames: */ + font-family: "GillSans"; } html, diff --git a/browser/components/gameboy/Display.tsx b/browser/components/gameboy/Display.tsx index 1fc6c89..82f7605 100644 --- a/browser/components/gameboy/Display.tsx +++ b/browser/components/gameboy/Display.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import type { ReactNode } from 'react' import { Styled } from './GameBoy.styled' @@ -12,13 +12,15 @@ export function Display(props: Props) { - DOT MATRIX WITH STEREO SOUND + + DOT MATRIX WITH STEREO SOUND + - + BATTERY diff --git a/browser/components/gameboy/GameBoy.styled.ts b/browser/components/gameboy/GameBoy.styled.ts index 89ce8fe..1916bea 100644 --- a/browser/components/gameboy/GameBoy.styled.ts +++ b/browser/components/gameboy/GameBoy.styled.ts @@ -1,6 +1,4 @@ -import styled, { css, ThemedStyledProps } from 'styled-components' - -import { Theme } from '../../types' +import styled, { css, type ExecutionContext } from 'styled-components' const SCREEN_PADDING_LEFT = 45 const SCREEN_PADDING_TOP = 23 @@ -16,8 +14,8 @@ const ARROW_SIZE = 26 const TEXT_COLOR = '#393C81' const AB_COLOR = '#8A205E' -function zoom(value: number) { - return (props: ThemedStyledProps) => `${props.theme.zoom * value}px` +function zoom(value: number) { + return (props: ExecutionContext) => `${props.theme.zoom * value}px` } function activeButtonEffect() { @@ -93,8 +91,8 @@ const ScreenOverlay = styled.div` bottom: 0; ` -const BatteryIndicator = styled.div<{ enabled?: boolean }>` - background-color: ${(props) => (props.enabled ? `#f00` : '#000')}; +const BatteryIndicator = styled.div<{ $enabled?: boolean }>` + background-color: ${(props) => (props.$enabled ? '#f00' : '#000')}; box-shadow: 0 0 3px 1px #ef5350; height: ${zoom(INDICATOR_SIZE)}; width: ${zoom(INDICATOR_SIZE)}; @@ -210,8 +208,8 @@ const ButtonsStartSelect = styled.div` const Arrows = styled.div`` export enum ArrowOrientation { - HORIZONTAL, - VERTICAL + HORIZONTAL = 0, + VERTICAL = 1, } const ArrowsLine = styled.div` @@ -347,5 +345,5 @@ export const Styled = { ArrowCenter, ArrowStripe, Speakers, - Speaker + Speaker, } diff --git a/browser/components/gameboy/GameBoy.tsx b/browser/components/gameboy/GameBoy.tsx index e43fea5..362fd9d 100644 --- a/browser/components/gameboy/GameBoy.tsx +++ b/browser/components/gameboy/GameBoy.tsx @@ -1,4 +1,4 @@ -import { ForwardedRef, forwardRef } from 'react' +import { type ForwardedRef, forwardRef } from 'react' import { JsKeys } from '../../../gb-web/pkg' import { useInput } from '../../hooks/useInput' @@ -11,7 +11,7 @@ export const DISPLAY_HEIGHT = 144 function GameBoyComponent( { running }: { running: boolean }, - ref: ForwardedRef + ref: ForwardedRef, ) { const { zoom } = useTheme() const { input, onKeyDown, onKeyUp } = useInput() @@ -35,7 +35,7 @@ function GameBoyComponent( style={{ display: 'block', // prevents wierd margin imageRendering: 'pixelated', - zoom + zoom, }} height={DISPLAY_HEIGHT} width={DISPLAY_WIDTH} @@ -43,7 +43,9 @@ function GameBoyComponent(
- Nintendo + + Nintendo + GAME BOY @@ -58,7 +60,8 @@ function GameBoyComponent( onPointerDown={() => onKeyDown(JsKeys.ArrowUp)} onPointerUp={() => onKeyUp(JsKeys.ArrowUp)} $orientation={ArrowOrientation.HORIZONTAL} - $pressed={pressedUp}> + $pressed={pressedUp} + > @@ -69,7 +72,8 @@ function GameBoyComponent( onPointerDown={() => onKeyDown(JsKeys.ArrowLeft)} onPointerUp={() => onKeyUp(JsKeys.ArrowLeft)} $orientation={ArrowOrientation.VERTICAL} - $pressed={pressedLeft}> + $pressed={pressedLeft} + > @@ -79,7 +83,8 @@ function GameBoyComponent( onPointerDown={() => onKeyDown(JsKeys.ArrowRight)} onPointerUp={() => onKeyUp(JsKeys.ArrowRight)} $orientation={ArrowOrientation.VERTICAL} - $pressed={pressedRight}> + $pressed={pressedRight} + > @@ -90,7 +95,8 @@ function GameBoyComponent( onPointerDown={() => onKeyDown(JsKeys.ArrowDown)} onPointerUp={() => onKeyUp(JsKeys.ArrowDown)} $orientation={ArrowOrientation.HORIZONTAL} - $pressed={pressedDown}> + $pressed={pressedDown} + > diff --git a/browser/components/gameboy/Zoom.tsx b/browser/components/gameboy/Zoom.tsx index 84c2707..48bee22 100644 --- a/browser/components/gameboy/Zoom.tsx +++ b/browser/components/gameboy/Zoom.tsx @@ -22,7 +22,7 @@ const Styled = { `, Value: styled.div` margin: 0 5px; - ` + `, } export function Zoom(props: Props) { diff --git a/browser/components/pages/Debugger.tsx b/browser/components/pages/Debugger.tsx index d29be39..0f137fe 100644 --- a/browser/components/pages/Debugger.tsx +++ b/browser/components/pages/Debugger.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import styled, { createGlobalStyle } from 'styled-components' -import { WebHandle } from '../../../debugger-web/pkg' +import type { WebHandle } from '../../../debugger-web/pkg' import { roms } from '../../romsList' const CANVAS_ID = 'debugger' @@ -49,7 +49,6 @@ export function Debugger() { } appHandler = await app.start(canvas, roms) } catch (error) { - // eslint-disable-next-line no-console console.error(error) } } diff --git a/browser/components/pages/Landing.tsx b/browser/components/pages/Landing.tsx index 555dc93..e915199 100644 --- a/browser/components/pages/Landing.tsx +++ b/browser/components/pages/Landing.tsx @@ -1,8 +1,8 @@ import { Link } from 'react-router-dom' import styled from 'styled-components' -import { ReactComponent as Feather } from '../../assets/images/feather.svg' -import { ReactComponent as GameboyLogo } from '../../assets/images/gameboy.svg' +import Feather from '../../assets/images/feather.svg?react' +import GameboyLogo from '../../assets/images/gameboy.svg?react' const Nintendo = styled.span` font-size: 30px; @@ -28,7 +28,9 @@ export function Landing() { Just another
Nintendo - GAME BOY + + GAME BOY +
emulator
diff --git a/browser/components/pages/Play.tsx b/browser/components/pages/Play.tsx index 86dee1e..567c299 100644 --- a/browser/components/pages/Play.tsx +++ b/browser/components/pages/Play.tsx @@ -3,12 +3,12 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { ThemeProvider } from 'styled-components' -import { WebEmulator } from '../../../gb-web/pkg' +import type { WebEmulator } from '../../../gb-web/pkg' import { memory } from '../../../gb-web/pkg/gb_web_bg.wasm' import { InputContextProvider } from '../../context/InputContext' import { useInputHandler } from '../../hooks/useInputHandler' -import { useWasmModule, WasmModule } from '../../hooks/useWasmModule' -import { Rom, Theme } from '../../types' +import { useWasmModule, type WasmModule } from '../../hooks/useWasmModule' +import type { Rom, Theme } from '../../types' import { warmupAudio } from '../../utils/audio' import { range } from '../../utils/std' import { FullscreenLoader } from '../common/FullscreenLoader' @@ -32,7 +32,7 @@ type Props = { } const audioContext = new AudioContext({ - sampleRate: 44100 + sampleRate: 44100, }) function renderFrame(emulator: WebEmulator, ctx: CanvasRenderingContext2D) { @@ -42,7 +42,7 @@ function renderFrame(emulator: WebEmulator, ctx: CanvasRenderingContext2D) { const canvasData = new Uint8Array( memory.buffer, canvasDataPointer, - DISPLAY_WIDTH * DISPLAY_HEIGHT * 3 + DISPLAY_WIDTH * DISPLAY_HEIGHT * 3, ) range(0, DISPLAY_HEIGHT).forEach((y) => { @@ -79,37 +79,41 @@ function DeviceHandler(props: Props) { initScreen(ctx) }, [ctx]) - const onAudioBufferCallback = useCallback((bufferPtr: number) => { - const BUFFER_SIZE = wasmModule.get_audio_buffer_size() - - const audioData = new Float32Array(memory.buffer, bufferPtr, BUFFER_SIZE) - - const frameCount = audioData.length / CHANNELS_COUNT - const audioBuffer = audioContext.createBuffer( - CHANNELS_COUNT, - frameCount, - audioContext.sampleRate - ) - for (let channel = 0; channel < CHANNELS_COUNT; channel += 1) { - const nowBuffering = audioBuffer.getChannelData(channel) - for (let i = 0; i < frameCount; i += 1) { - // audio data frames are interleaved - nowBuffering[i] = audioData[i * CHANNELS_COUNT + channel] + const onAudioBufferCallback = useCallback( + (bufferPtr: number) => { + const BUFFER_SIZE = wasmModule.get_audio_buffer_size() + + const audioData = new Float32Array(memory.buffer, bufferPtr, BUFFER_SIZE) + + const frameCount = audioData.length / CHANNELS_COUNT + const audioBuffer = audioContext.createBuffer( + CHANNELS_COUNT, + frameCount, + audioContext.sampleRate, + ) + for (let channel = 0; channel < CHANNELS_COUNT; channel += 1) { + const nowBuffering = audioBuffer.getChannelData(channel) + for (let i = 0; i < frameCount; i += 1) { + // audio data frames are interleaved + nowBuffering[i] = audioData[i * CHANNELS_COUNT + channel] + } } - } - const audioSource = audioContext.createBufferSource() - audioSource.buffer = audioBuffer - audioSource.connect(audioContext.destination) - - // taken from here https://github.com/Powerlated/OptimeGB/blob/master/src/core/audioplayer.ts#L91-L94 - // TODO after some time, the game gets audio delay - // Reset time if close to buffer underrun - if (currentAudioSeconds.current <= audioContext.currentTime + 0.02) { - currentAudioSeconds.current = audioContext.currentTime + 0.06 - } - audioSource.start(currentAudioSeconds.current) - currentAudioSeconds.current += BUFFER_SIZE / CHANNELS_COUNT / audioContext.sampleRate - }, []) + const audioSource = audioContext.createBufferSource() + audioSource.buffer = audioBuffer + audioSource.connect(audioContext.destination) + + // taken from here https://github.com/Powerlated/OptimeGB/blob/master/src/core/audioplayer.ts#L91-L94 + // TODO after some time, the game gets audio delay + // Reset time if close to buffer underrun + if (currentAudioSeconds.current <= audioContext.currentTime + 0.02) { + currentAudioSeconds.current = audioContext.currentTime + 0.06 + } + audioSource.start(currentAudioSeconds.current) + currentAudioSeconds.current += + BUFFER_SIZE / CHANNELS_COUNT / audioContext.sampleRate + }, + [wasmModule.get_audio_buffer_size], + ) // Create emulator on cartridge load useEffect(() => { @@ -123,7 +127,7 @@ function DeviceHandler(props: Props) { initScreen(ctx) registerInputs(emulator.current) - }, [bytes, wasmModule, registerInputs]) + }, [ctx, bytes, wasmModule, registerInputs]) useEffect(() => { const e = emulator.current @@ -132,7 +136,7 @@ function DeviceHandler(props: Props) { } e.set_audio_buffer_callback(soundEnabled ? onAudioBufferCallback : () => {}) - }, [emulator.current, soundEnabled, onAudioBufferCallback]) + }, [soundEnabled, onAudioBufferCallback]) // Handle stop/start useEffect(() => { @@ -151,14 +155,17 @@ function DeviceHandler(props: Props) { } else { window.cancelAnimationFrame(loopId.current) } - }, [running]) + }, [running, ctx]) return null } export function Play() { const [zoom, setZoom] = useLocalStorage('zoom', DEFAULT_ZOOM) - const [soundEnabled, setSoundEnabled] = useLocalStorage('sound_enabled', false) + const [soundEnabled, setSoundEnabled] = useLocalStorage( + 'sound_enabled', + false, + ) const [running, setRunning] = useState(false) const [ctx, setCtx] = useState() const [rom, setRom] = useState(null) @@ -201,10 +208,10 @@ export function Play() { }) }, []) - const onCartridgeLoad = (loadedRom: Rom) => { + const onCartridgeLoad = useCallback((loadedRom: Rom) => { setRunning(false) setRom(loadedRom) - } + }, []) if (!wasmModule) { return @@ -236,15 +243,22 @@ export function Play() { - + Upload ROM -