From 6c8fde85a17c970e0fd01996104af7ccf2d46b56 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 22 May 2023 14:24:03 -0400 Subject: [PATCH 1/8] Update RomLoader to include rom config values --- js/components/App/App.tsx | 2 +- js/components/EmulatorInfo/EmulatorInfo.tsx | 28 ++- js/components/RomLoader/RomLoader.tsx | 254 ++++++++++---------- js/redux/state/rustGameboy.ts | 4 +- src/cpu/mod.rs | 28 ++- src/gameboy/mod.rs | 10 +- src/lib.rs | 12 +- src/mmu/mod.rs | 7 +- src/rom_config/mod.rs | 15 ++ 9 files changed, 198 insertions(+), 162 deletions(-) create mode 100644 src/rom_config/mod.rs diff --git a/js/components/App/App.tsx b/js/components/App/App.tsx index 8c667d9..bffd742 100644 --- a/js/components/App/App.tsx +++ b/js/components/App/App.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; import { Gameboy } from '../Gameboy/Gameboy'; -import RomLoader from '../RomLoader/RomLoader'; +import { RomLoader } from '../RomLoader/RomLoader'; const StyledApp = styled.div` background-color: black; diff --git a/js/components/EmulatorInfo/EmulatorInfo.tsx b/js/components/EmulatorInfo/EmulatorInfo.tsx index 9dbe2f0..0741b47 100644 --- a/js/components/EmulatorInfo/EmulatorInfo.tsx +++ b/js/components/EmulatorInfo/EmulatorInfo.tsx @@ -1,12 +1,12 @@ -import { useSelector } from "react-redux"; -import { Emulator } from "gameboy"; -import { State } from "../../redux/state/state"; -import { useCallback, useState } from "react"; -import { Button, Modal, Tab, Tabs } from "react-bootstrap"; -import chunk from "chunk"; -import styled from "styled-components"; -import { TileInfo } from "./TileInfo"; -import { CANVAS_WIDTH } from "./constants"; +import { useSelector } from 'react-redux'; +import { Emulator } from 'gameboy'; +import { State } from '../../redux/state/state'; +import { useCallback, useMemo, useState } from 'react'; +import { Button, Modal, Tab, Tabs } from 'react-bootstrap'; +import chunk from 'chunk'; +import styled from 'styled-components'; +import { TileInfo } from './TileInfo'; +import { CANVAS_WIDTH } from './constants'; type Props = { show: boolean; @@ -22,8 +22,11 @@ export function EmulatorInfo({ show, setShow }: Props) { const handleClose = useCallback(() => setShow(false), [setShow]); if (!emulator) return null; - const cartridgeType = JSON.parse(emulator.get_header_info()).header - .cartridge_type; + const header = useMemo( + () => JSON.parse(emulator.get_header_info()).header, + [emulator] + ); + const colors = chunk(emulator.get_tiles(), 3); const tiles = chunk(colors, 64); console.log({ tileLEngth: tiles.length }); @@ -43,7 +46,8 @@ export function EmulatorInfo({ show, setShow }: Props) { -

Cartridge Type: {cartridgeType}

+

Cartridge Type: {header.cartridge_type}

+

CGB Mode: {header.cgb_mode}

diff --git a/js/components/RomLoader/RomLoader.tsx b/js/components/RomLoader/RomLoader.tsx index c97b591..e32a1a8 100644 --- a/js/components/RomLoader/RomLoader.tsx +++ b/js/components/RomLoader/RomLoader.tsx @@ -1,37 +1,37 @@ -import { useState, useEffect } from "react"; -import styled from "styled-components"; -import DropdownButton from "react-bootstrap/DropdownButton"; -import Dropdown from "react-bootstrap/Dropdown"; -import { connect } from "react-redux"; -import { useFilePicker } from "use-file-picker"; - -import { loadRom } from "../../redux/actions/gameboy"; -import { setCurrentGame } from "../../redux/actions/currentGame"; -import { loadWasm } from "../../helpers/wasm"; -import { State } from "../../redux/state/state"; -import { RustGameboy } from "../../redux/state/rustGameboy"; -import { Emulator } from "gameboy"; - -type Props = StateProps & DispatchProps; - -interface StateProps { - emulator: Emulator | null; -} - -interface DispatchProps { - loadRom: (emulator: Emulator) => any; - setCurrentGame: (currentGame: string) => any; -} +import { useState, useEffect, useCallback } from 'react'; +import styled from 'styled-components'; +import DropdownButton from 'react-bootstrap/DropdownButton'; +import Dropdown from 'react-bootstrap/Dropdown'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { useFilePicker } from 'use-file-picker'; + +import { loadRom } from '../../redux/actions/gameboy'; +import { setCurrentGame } from '../../redux/actions/currentGame'; +import { loadWasm } from '../../helpers/wasm'; +import { State } from '../../redux/state/state'; +import { RustGameboy } from '../../redux/state/rustGameboy'; +import { Emulator } from 'gameboy'; +import { Form } from 'react-bootstrap'; const StyledDropdownButton = styled(DropdownButton)` margin-top: 20px; `; -const RomLoader = (props: Props) => { +const StyledCheck = styled(Form.Check)` + color: white; +`; + +export function RomLoader() { + const dispatch = useDispatch(); const [gameboy, setGameboy] = useState(null); + const [runBootRom, setRunBootRom] = useState(false); + const [cgb, setCgb] = useState(false); + const emulator = useSelector( + (state) => state.gameboy.emulator + ); const [openFileSelector, { plainFiles }] = useFilePicker({ - accept: ".gb", - multiple: false, + accept: '.gb', + multiple: false }); useEffect(() => { @@ -43,102 +43,114 @@ const RomLoader = (props: Props) => { getGameboy(); }); - if (!!props.emulator) return null; + const readFile = useCallback( + async (fileName: string) => { + if (!gameboy?.Emulator || !gameboy?.RomConfig) { + return; + } + + const response = await fetch(`/roms/${fileName}`); + const blob = await response.blob(); + const buffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const key = fileName.replace(/.gb$/, ''); + const fileData = getFileData(key); + const romConfig = new gameboy.RomConfig(runBootRom, cgb); + if (fileData === null) { + const emulator = new gameboy.Emulator(bytes, romConfig); + dispatch(setCurrentGame(key)); + dispatch(loadRom(emulator)); + } else { + const emulator = gameboy.Emulator.from_save_data( + bytes, + fileData, + romConfig + ); + dispatch(setCurrentGame(key)); + dispatch(loadRom(emulator)); + } + }, + [runBootRom, cgb, gameboy, dispatch] + ); + + const pickFile = useCallback( + async (file: File) => { + if (!gameboy?.Emulator || !gameboy?.RomConfig) { + return; + } + + const buffer = await file.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const romConfig = new gameboy.RomConfig(runBootRom, cgb); + const emulator = new gameboy.Emulator(bytes, romConfig); + dispatch(loadRom(emulator)); + }, + [runBootRom, cgb, gameboy, dispatch] + ); + + if (!!emulator) return null; if (plainFiles.length > 0) { - pickFile(props, gameboy, plainFiles[0]); + pickFile(plainFiles[0]); } return ( - - await readFile(props, gameboy, "cpu_instrs.gb")} +
+ - CPU All Tests - - await readFile(props, gameboy, "Dr. Mario.gb")} - > - Dr. Mario - - - await readFile(props, gameboy, "Super Mario Land.gb") + await readFile('cpu_instrs.gb')}> + CPU All Tests + + await readFile('Dr. Mario.gb')}> + Dr. Mario + + await readFile('Super Mario Land.gb')} + > + Super Mario Land + + await readFile('Tetris.gb')}> + Tetris + + await readFile("Kirby's Dream Land.gb")} + > + Kirby's Dream Land + + await readFile('Zelda.gb')}> + The Legend of Zelda Link's Awakening + + await readFile('Pokemon Blue.gb')}> + Pokemon Blue + + openFileSelector()}> + Pick File... + + + ) => + setRunBootRom(e.target.checked) } - > - Super Mario Land - - await readFile(props, gameboy, "Tetris.gb")} - > - Tetris - - - await readFile(props, gameboy, "Kirby's Dream Land.gb") + /> + ) => + setCgb(e.target.checked) } - > - Kirby's Dream Land - - await readFile(props, gameboy, "Zelda.gb")} - > - The Legend of Zelda Link's Awakening - - await readFile(props, gameboy, "Pokemon Blue.gb")} - > - Pokemon Blue - - openFileSelector()}> - Pick File... - - + /> +
); -}; - -const readFile = async ( - props: Props, - gameboy: RustGameboy | null, - fileName: string -) => { - if (!gameboy?.Emulator) return; - - const response = await fetch(`/roms/${fileName}`); - const blob = await response.blob(); - const buffer = await blob.arrayBuffer(); - const bytes = new Uint8Array(buffer); - const key = fileName.replace(/.gb$/, ""); - const fileData = getFileData(key); - if (fileData === null) { - const emulator = new gameboy.Emulator(bytes); - props.setCurrentGame(key); - props.loadRom(emulator); - } else { - const emulator = gameboy.Emulator.from_save_data(bytes, fileData); - props.setCurrentGame(key); - props.loadRom(emulator); - } -}; - -const pickFile = async ( - props: Props, - gameboy: RustGameboy | null, - file: File -) => { - if (!gameboy?.Emulator) { - return; - } - - const buffer = await file.arrayBuffer(); - const bytes = new Uint8Array(buffer); - let emulator = new gameboy.Emulator(bytes); - props.loadRom(emulator); -}; +} const getFileData = (key: string): Uint8Array | null => { if (!window || !window.localStorage) { @@ -152,17 +164,3 @@ const getFileData = (key: string): Uint8Array | null => { return new Uint8Array(JSON.parse(item)); }; - -const mapStateToProps = (state: State) => { - return { - emulator: state.gameboy.emulator, - }; -}; - -const mapDispatchToProps = (dispatch: any) => ({ - loadRom: (emulator: Emulator) => dispatch(loadRom(emulator)), - setCurrentGame: (currentGame: string) => - dispatch(setCurrentGame(currentGame)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(RomLoader); diff --git a/js/redux/state/rustGameboy.ts b/js/redux/state/rustGameboy.ts index b7f76c9..fb26f3d 100644 --- a/js/redux/state/rustGameboy.ts +++ b/js/redux/state/rustGameboy.ts @@ -1,13 +1,15 @@ -import { Emulator, EmulatorState, Input } from "gameboy"; +import { Emulator, EmulatorState, Input, RomConfig } from 'gameboy'; export interface RustGameboy { Input: typeof Input | null; Emulator: typeof Emulator | null; EmulatorState: typeof EmulatorState | null; + RomConfig: typeof RomConfig | null; } export const defaultState: RustGameboy = { Input: null, Emulator: null, EmulatorState: null, + RomConfig: null }; diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index d693389..11ffb0b 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -6,10 +6,10 @@ pub mod registers; #[cfg(test)] mod tests; -use crate::cartridge::Cartridge; use crate::constants::cpu::PROGRAM_START; use crate::mmu::interrupts::Interrupt; use crate::mmu::Mmu; +use crate::{cartridge::Cartridge, rom_config::RomConfig}; use opcodes::{ cb_opcode::CbOpcode, cb_opcode_table::CB_OPCODE_TABLE, @@ -18,6 +18,10 @@ use opcodes::{ }; use serde::{Deserialize, Serialize}; +use self::opcodes::opcode::CpuRegister; + +const CGB_HARDWARE_DETECTED: u8 = 0x11; + #[derive(Serialize, Deserialize, Default)] pub struct Cpu { pub master_clock_cycles: u32, @@ -36,13 +40,20 @@ pub struct Cpu { } impl Cpu { - pub fn new(cartridge: Cartridge, run_boot_rom: bool) -> Self { + pub fn new(cartridge: Cartridge, rom_config: &RomConfig) -> Self { let mut cpu = Cpu { - mmu: Mmu::new(cartridge, run_boot_rom), + mmu: Mmu::new(cartridge, rom_config), ..Default::default() }; - cpu.program_start(run_boot_rom); + cpu.program_start(rom_config); + + if cpu.registers.a == CGB_HARDWARE_DETECTED { + info!("CGB Hardware detected"); + } else { + info!("GB Hardware detected"); + } + cpu } @@ -272,8 +283,8 @@ impl Cpu { } } - fn program_start(&mut self, run_boot_rom: bool) { - if run_boot_rom { + fn program_start(&mut self, rom_config: &RomConfig) { + if rom_config.run_boot_rom { return; } @@ -283,5 +294,10 @@ impl Cpu { self.registers.set_target_16(&CpuRegister16::BC, 0x0013); self.registers.set_target_16(&CpuRegister16::DE, 0x00D8); self.registers.set_target_16(&CpuRegister16::HL, 0x014D); + + if rom_config.cgb { + // https://gbdev.io/pandocs/CGB_Registers.html#detecting-cgb-and-gba-functions + self.registers.set_target(&CpuRegister::A, 0x11); + } } } diff --git a/src/gameboy/mod.rs b/src/gameboy/mod.rs index d6d335c..d45a603 100644 --- a/src/gameboy/mod.rs +++ b/src/gameboy/mod.rs @@ -3,9 +3,9 @@ pub mod render; #[cfg(test)] mod tests; -use crate::cartridge::Cartridge; use crate::cpu; use crate::input::Input; +use crate::{cartridge::Cartridge, rom_config::RomConfig}; use serde::{Deserialize, Serialize}; use std::str; @@ -16,16 +16,16 @@ pub struct Gameboy { } impl Gameboy { - pub fn new(bytes: Vec, run_boot_rom: bool) -> Self { + pub fn new(bytes: Vec, rom_config: &RomConfig) -> Self { Gameboy { - cpu: cpu::Cpu::new(Cartridge::new(bytes), run_boot_rom), + cpu: cpu::Cpu::new(Cartridge::new(bytes), rom_config), input: Input::new(), } } - pub fn from_save_data(bytes: Vec, save_data: Vec, run_boot_rom: bool) -> Self { + pub fn from_save_data(bytes: Vec, save_data: Vec, rom_config: &RomConfig) -> Self { Gameboy { - cpu: cpu::Cpu::new(Cartridge::from_save_data(bytes, save_data), run_boot_rom), + cpu: cpu::Cpu::new(Cartridge::from_save_data(bytes, save_data), rom_config), input: Input::new(), } } diff --git a/src/lib.rs b/src/lib.rs index 5c1e4b6..7228e99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use gameboy::Gameboy; use input::Input; use log::Level; +use rom_config::RomConfig; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -22,10 +23,9 @@ pub mod gpu; pub mod input; pub mod mbc; pub mod mmu; +pub mod rom_config; pub mod utils; -const RUN_BOOT_ROM: bool = false; - #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; @@ -40,17 +40,17 @@ pub struct Emulator { #[wasm_bindgen] impl Emulator { #[wasm_bindgen(constructor)] - pub fn new(bytes: Vec) -> Self { + pub fn new(bytes: Vec, rom_config: RomConfig) -> Self { init_console_log(); - let gameboy = Gameboy::new(bytes, RUN_BOOT_ROM); + let gameboy = Gameboy::new(bytes, &rom_config); Self { cycles: 0, gameboy } } - pub fn from_save_data(bytes: Vec, save_data: Vec) -> Self { + pub fn from_save_data(bytes: Vec, save_data: Vec, rom_config: RomConfig) -> Self { init_console_log(); - let gameboy = Gameboy::from_save_data(bytes, save_data, RUN_BOOT_ROM); + let gameboy = Gameboy::from_save_data(bytes, save_data, &rom_config); Self { cycles: 0, gameboy } } diff --git a/src/mmu/mod.rs b/src/mmu/mod.rs index 4d6beca..74c18fc 100644 --- a/src/mmu/mod.rs +++ b/src/mmu/mod.rs @@ -17,6 +17,7 @@ use crate::cartridge::Cartridge; use crate::constants::boot_rom::*; use crate::controls::*; use crate::input::Input; +use crate::rom_config::RomConfig; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default)] @@ -30,14 +31,14 @@ pub struct Mmu { } impl Mmu { - pub fn new(cartridge: Cartridge, run_boot_rom: bool) -> Self { + pub fn new(cartridge: Cartridge, rom_config: &RomConfig) -> Self { let mut mmu = Mmu { controls: Default::default(), ram: Default::default(), boot_rom: Default::default(), - boot_rom_finished: !run_boot_rom, + boot_rom_finished: !rom_config.run_boot_rom, cartridge, - run_boot_rom, + run_boot_rom: rom_config.run_boot_rom, }; if !mmu.run_boot_rom { diff --git a/src/rom_config/mod.rs b/src/rom_config/mod.rs new file mode 100644 index 0000000..d2226ce --- /dev/null +++ b/src/rom_config/mod.rs @@ -0,0 +1,15 @@ +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub struct RomConfig { + pub run_boot_rom: bool, + pub cgb: bool, +} + +#[wasm_bindgen] +impl RomConfig { + #[wasm_bindgen(constructor)] + pub fn new(run_boot_rom: bool, cgb: bool) -> Self { + Self { run_boot_rom, cgb } + } +} From a73c9924fd6bbf809fa1df5a91f06d3364e2cc18 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 22 May 2023 16:22:53 -0400 Subject: [PATCH 2/8] Add linter and fix linting issues --- .eslintrc.js | 21 ++- .../Controls/AbButtons/AbButtons.tsx | 120 +++++++------- .../Controls/AllButtons/AllButtons.tsx | 101 ++++++------ .../Controls/ControlButton/ControlButton.tsx | 18 +-- js/components/Controls/Controls.tsx | 11 +- .../DesktopControls/DesktopControls.tsx | 59 ++++--- .../MobileControls/MobileControls.tsx | 42 ++--- .../StartSelectButtons/StartSelectButtons.tsx | 125 +++++++-------- js/components/EmulatorInfo/EmulatorInfo.tsx | 16 +- js/components/EmulatorInfo/TileInfo.tsx | 18 +-- js/components/Gameboy/Gameboy.tsx | 2 +- js/components/RomLoader/RomLoader.tsx | 12 +- js/components/Screen/Screen.tsx | 146 +++++++++--------- js/helpers/input.ts | 34 ++-- js/helpers/wasm.ts | 6 +- package.json | 4 +- src/gpu/lcd/mod.rs | 2 - yarn.lock | 5 + 18 files changed, 377 insertions(+), 365 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fe66667..8756c6e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,9 @@ module.exports = { + settings: { + react: { + version: 'detect' + } + }, env: { browser: true, es2021: true @@ -6,17 +11,27 @@ module.exports = { extends: [ 'plugin:react/recommended', 'standard-with-typescript', - 'plugin:react-hooks/recommended' + 'plugin:react-hooks/recommended', + 'prettier' ], overrides: [], parserOptions: { ecmaVersion: 'latest', - sourceType: 'module' + sourceType: 'module', + project: ['./tsconfig.json'] // Specify it only for TypeScript files }, plugins: ['react', 'react-hooks'], rules: { 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn' + 'react-hooks/exhaustive-deps': 'error', + '@typescript-eslint/consistent-type-imports': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + 'react/react-in-jsx-scope': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-extra-boolean-cast': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/no-misused-promises': 'off' } // eslint-disable-next-line semi }; diff --git a/js/components/Controls/AbButtons/AbButtons.tsx b/js/components/Controls/AbButtons/AbButtons.tsx index 5c87d56..ef52d93 100644 --- a/js/components/Controls/AbButtons/AbButtons.tsx +++ b/js/components/Controls/AbButtons/AbButtons.tsx @@ -1,17 +1,16 @@ -import { connect } from "react-redux"; -import styled from "styled-components"; -import { getInput } from "../../../helpers/input"; -import { State } from "../../../redux/state/state"; -import { ButtonState } from "../../../redux/state/buttons"; -import { setButtons } from "../../../redux/actions/buttons"; -import { DirectionState } from "../../../redux/state/direction"; -import { RustGameboy } from "../../../redux/state/rustGameboy"; -import ControlButton from "../ControlButton/ControlButton"; -import { mediaMinMd } from "../../../constants/screenSizes"; -import GridCell from "../../GridCell/GridCell"; -import { Emulator } from "gameboy"; - -type Props = StateProps & DispatchProps; +import { Emulator } from 'gameboy'; +import { useSelector, useDispatch } from 'react-redux'; +import { useCallback } from 'react'; +import styled from 'styled-components'; +import { getInput } from '../../../helpers/input'; +import { State } from '../../../redux/state/state'; +import { ButtonState } from '../../../redux/state/buttons'; +import { setButtons } from '../../../redux/actions/buttons'; +import { DirectionState } from '../../../redux/state/direction'; +import { RustGameboy } from '../../../redux/state/rustGameboy'; +import ControlButton from '../ControlButton/ControlButton'; +import { mediaMinMd } from '../../../constants/screenSizes'; +import GridCell from '../../GridCell/GridCell'; interface StateProps { buttons: ButtonState; @@ -20,11 +19,7 @@ interface StateProps { rustGameboy: RustGameboy; } -interface DispatchProps { - setButtons(buttons: ButtonState): void; -} - -type ButtonKey = "a" | "b"; +type ButtonKey = 'a' | 'b'; const StyledAbControls = styled.div` bottom: 90px; @@ -40,60 +35,65 @@ const StyledAbControls = styled.div` } `; -const AbButtons = (props: Props) => { +export function AbButtons() { + const dispatch = useDispatch(); + const stateProps = useSelector((state) => ({ + buttons: state.buttons, + direction: state.direction, + emulator: state.gameboy.emulator!, + rustGameboy: state.rustGameboy + })); + + const handleTouch = useCallback( + ( + _e: React.TouchEvent, + buttonKey: ButtonKey, + pressed: boolean + ) => { + const updatedState = { ...stateProps.buttons, [buttonKey]: pressed }; + const input = getInput( + stateProps.rustGameboy, + updatedState, + stateProps.direction + ); + dispatch(setButtons(updatedState)); + stateProps.emulator.update_controls(input); + + if (pressed) { + window.navigator.vibrate(10); + } + }, + [ + dispatch, + stateProps.buttons, + stateProps.direction, + stateProps.emulator, + stateProps.rustGameboy + ] + ); + return ( handleTouch(e, props, "a", true)} - onTouchEnd={(e) => handleTouch(e, props, "a", false)} - onTouchCancel={(e) => handleTouch(e, props, "a", false)} + onTouchStart={(e) => handleTouch(e, 'a', true)} + onTouchEnd={(e) => handleTouch(e, 'a', false)} + onTouchCancel={(e) => handleTouch(e, 'a', false)} /> handleTouch(e, props, "b", true)} - onTouchEnd={(e) => handleTouch(e, props, "b", false)} - onTouchCancel={(e) => handleTouch(e, props, "b", false)} + onTouchStart={(e) => handleTouch(e, 'b', true)} + onTouchEnd={(e) => handleTouch(e, 'b', false)} + onTouchCancel={(e) => handleTouch(e, 'b', false)} /> ); -}; - -const handleTouch = ( - e: React.TouchEvent, - props: Props, - buttonKey: ButtonKey, - pressed: boolean -) => { - const updatedState = { ...props.buttons, [buttonKey]: pressed }; - const input = getInput(props.rustGameboy, updatedState, props.direction); - props.setButtons(updatedState); - props.emulator.update_controls(input); - - if (pressed) { - window.navigator.vibrate(10); - } -}; - -const mapStateToProps = (state: State): StateProps => { - return { - buttons: state.buttons, - direction: state.direction, - emulator: state.gameboy.emulator!, - rustGameboy: state.rustGameboy, - }; -}; - -const mapDispatchToProps = (dispatch: any): DispatchProps => ({ - setButtons: (buttons: ButtonState) => dispatch(setButtons(buttons)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(AbButtons); +} diff --git a/js/components/Controls/AllButtons/AllButtons.tsx b/js/components/Controls/AllButtons/AllButtons.tsx index 030c8cc..73b8a7a 100644 --- a/js/components/Controls/AllButtons/AllButtons.tsx +++ b/js/components/Controls/AllButtons/AllButtons.tsx @@ -1,14 +1,13 @@ -import { Emulator } from "gameboy"; -import { connect } from "react-redux"; -import { getInput } from "../../../helpers/input"; -import { setButtons } from "../../../redux/actions/buttons"; -import { ButtonState } from "../../../redux/state/buttons"; -import { DirectionState } from "../../../redux/state/direction"; -import { RustGameboy } from "../../../redux/state/rustGameboy"; -import { State } from "../../../redux/state/state"; -import ControlButton from "../ControlButton/ControlButton"; - -type Props = StateProps & DispatchProps; +import { Emulator } from 'gameboy'; +import { useDispatch, useSelector } from 'react-redux'; +import { getInput } from '../../../helpers/input'; +import { setButtons } from '../../../redux/actions/buttons'; +import { ButtonState } from '../../../redux/state/buttons'; +import { DirectionState } from '../../../redux/state/direction'; +import { RustGameboy } from '../../../redux/state/rustGameboy'; +import { State } from '../../../redux/state/state'; +import ControlButton from '../ControlButton/ControlButton'; +import { useCallback } from 'react'; interface StateProps { buttons: ButtonState; @@ -17,53 +16,53 @@ interface StateProps { rustGameboy: RustGameboy; } -interface DispatchProps { - setButtons(buttons: ButtonState): void; -} +export function AllButtons() { + const dispatch = useDispatch(); + const stateProps = useSelector((state) => ({ + buttons: state.buttons, + direction: state.direction, + emulator: state.gameboy.emulator!, + rustGameboy: state.rustGameboy + })); -const mapStateToProps = (state: State): StateProps => ({ - buttons: state.buttons, - direction: state.direction, - emulator: state.gameboy.emulator!, - rustGameboy: state.rustGameboy, -}); + const handleTouch = useCallback( + (e: React.TouchEvent, pressed: boolean) => { + const updatedState = { + ...stateProps.buttons, + a: pressed, + b: pressed, + start: pressed, + select: pressed + }; + const input = getInput( + stateProps.rustGameboy, + updatedState, + stateProps.direction + ); + dispatch(setButtons(updatedState)); + stateProps.emulator.update_controls(input); -const mapDispatchToProps = (dispatch: any): DispatchProps => ({ - setButtons: (buttons: ButtonState) => dispatch(setButtons(buttons)), -}); + if (pressed) { + window.navigator.vibrate(10); + } + }, + [ + dispatch, + stateProps.buttons, + stateProps.direction, + stateProps.emulator, + stateProps.rustGameboy + ] + ); -const AllButtons = (props: Props) => { return ( handleTouch(e, props, true)} - onTouchEnd={(e) => handleTouch(e, props, false)} - onTouchCancel={(e) => handleTouch(e, props, false)} + onTouchStart={(e) => handleTouch(e, true)} + onTouchEnd={(e) => handleTouch(e, false)} + onTouchCancel={(e) => handleTouch(e, false)} /> ); -}; - -const handleTouch = ( - e: React.TouchEvent, - props: Props, - pressed: boolean -) => { - const updatedState = { - ...props.buttons, - a: pressed, - b: pressed, - start: pressed, - select: pressed, - }; - const input = getInput(props.rustGameboy, updatedState, props.direction); - props.setButtons(updatedState); - props.emulator.update_controls(input); - - if (pressed) { - window.navigator.vibrate(10); - } -}; - -export default connect(mapStateToProps, mapDispatchToProps)(AllButtons); +} diff --git a/js/components/Controls/ControlButton/ControlButton.tsx b/js/components/Controls/ControlButton/ControlButton.tsx index b04e7e3..80e7fc9 100644 --- a/js/components/Controls/ControlButton/ControlButton.tsx +++ b/js/components/Controls/ControlButton/ControlButton.tsx @@ -1,7 +1,7 @@ -import styled from "styled-components"; -import Button from "react-bootstrap/Button"; +import styled from 'styled-components'; +import Button from 'react-bootstrap/Button'; -type ButtonType = "circle" | "directional" | "start-select"; +type ButtonType = 'circle' | 'directional' | 'start-select'; const StyledButton = styled(Button)` font-size: 15px; @@ -38,21 +38,19 @@ const ControlButton = ({ text, onTouchStart, onTouchEnd, - onTouchCancel, + onTouchCancel }: Props) => { const getVariant = (pressed: boolean): string => - pressed ? "primary" : "secondary"; + pressed ? 'primary' : 'secondary'; const getButtonComponent = () => { switch (type) { - case "circle": + case 'circle': return CircleButton; - case "directional": + case 'directional': return DirectionalButton; - case "start-select": + case 'start-select': return StartSelectButton; - default: - throw new Error(`Invalid button type ${type}`); } }; diff --git a/js/components/Controls/Controls.tsx b/js/components/Controls/Controls.tsx index 83518ad..55e3e80 100644 --- a/js/components/Controls/Controls.tsx +++ b/js/components/Controls/Controls.tsx @@ -1,16 +1,11 @@ -import React from "react"; import { useMediaQuery } from 'react-responsive'; import { mobileMediaQuery } from '../../helpers/mediaQueries'; import MobileControls from './MobileControls/MobileControls'; import DesktopControls from './DesktopControls/DesktopControls'; const Controls = () => { - const isMobile = useMediaQuery(mobileMediaQuery); - return isMobile ? ( - - ) : ( - - ); + const isMobile = useMediaQuery(mobileMediaQuery); + return isMobile ? : ; }; -export default Controls; \ No newline at end of file +export default Controls; diff --git a/js/components/Controls/DesktopControls/DesktopControls.tsx b/js/components/Controls/DesktopControls/DesktopControls.tsx index 8f282cc..43d1d4d 100644 --- a/js/components/Controls/DesktopControls/DesktopControls.tsx +++ b/js/components/Controls/DesktopControls/DesktopControls.tsx @@ -1,26 +1,26 @@ -import { connect } from "react-redux"; -import styled from "styled-components"; -// @ts-ignore -import KeyboardEventHandler from "react-keyboard-event-handler"; -import { getInput } from "../../../helpers/input"; -import { State } from "../../../redux/state/state"; -import { ButtonState } from "../../../redux/state/buttons"; -import { DirectionState } from "../../../redux/state/direction"; -import { setButtons } from "../../../redux/actions/buttons"; -import { setDirection } from "../../../redux/actions/direction"; -import { RustGameboy } from "../../../redux/state/rustGameboy"; -import AbButtons from "../AbButtons/AbButtons"; -import ControlButton from "../ControlButton/ControlButton"; -import StartSelectButtons from "../StartSelectButtons/StartSelectButtons"; -import GridCell from "../../GridCell/GridCell"; -import { Emulator } from "gameboy"; - -const handleKeys = ["up", "down", "left", "right", "z", "x", "shift", "enter"]; +import { connect } from 'react-redux'; +import styled from 'styled-components'; +// @ts-expect-error react-keyboard-event-handler has no d.ts +import KeyboardEventHandler from 'react-keyboard-event-handler'; +import { getInput } from '../../../helpers/input'; +import { State } from '../../../redux/state/state'; +import { ButtonState } from '../../../redux/state/buttons'; +import { DirectionState } from '../../../redux/state/direction'; +import { setButtons } from '../../../redux/actions/buttons'; +import { setDirection } from '../../../redux/actions/direction'; +import { RustGameboy } from '../../../redux/state/rustGameboy'; +import { AbButtons } from '../AbButtons/AbButtons'; +import ControlButton from '../ControlButton/ControlButton'; +import { StartSelectButtons } from '../StartSelectButtons/StartSelectButtons'; +import GridCell from '../../GridCell/GridCell'; +import { Emulator } from 'gameboy'; + +const handleKeys = ['up', 'down', 'left', 'right', 'z', 'x', 'shift', 'enter']; const keyMapping = new Map(); -keyMapping.set("x", "a"); -keyMapping.set("z", "b"); -keyMapping.set("enter", "start"); -keyMapping.set("shift", "select"); +keyMapping.set('x', 'a'); +keyMapping.set('z', 'b'); +keyMapping.set('enter', 'start'); +keyMapping.set('shift', 'select'); type Props = StateProps & DispatchProps; @@ -32,8 +32,8 @@ interface StateProps { } interface DispatchProps { - setButtons(buttons: ButtonState): void; - setDirection(direction: DirectionState): void; + setButtons: (buttons: ButtonState) => void; + setDirection: (direction: DirectionState) => void; } const StyledDesktopControls = styled.div` @@ -100,7 +100,7 @@ const renderKeyboardHandlers = (props: Props) => { handleEventType="keydown" onKeyEvent={(key: string, _e: any) => { if (keyMapping.has(key)) { - let p: keyof ButtonState = keyMapping.get(key) as any; + const p: keyof ButtonState = keyMapping.get(key); if (props.buttons[p]) return; const updatedButtons = { ...props.buttons, [p]: true }; @@ -131,7 +131,7 @@ const renderKeyboardHandlers = (props: Props) => { handleEventType="keyup" onKeyEvent={(key: any, _e: any) => { if (keyMapping.has(key)) { - let p: keyof ButtonState = keyMapping.get(key) as any; + const p: keyof ButtonState = keyMapping.get(key); if (!props.buttons[p]) return; const updatedButtons = { ...props.buttons, [p]: false }; @@ -143,7 +143,7 @@ const renderKeyboardHandlers = (props: Props) => { props.setButtons(updatedButtons); props.emulator.update_controls(input); } else { - let p: keyof DirectionState = key as any; + const p: keyof DirectionState = key; if (!props.direction[p]) return; const updatedDirection = { ...props.direction, [p]: false }; @@ -165,13 +165,12 @@ const mapStateToProps = (state: State): StateProps => ({ buttons: state.buttons, direction: state.direction, emulator: state.gameboy.emulator!, - rustGameboy: state.rustGameboy, + rustGameboy: state.rustGameboy }); const mapDispatchToProps = (dispatch: any): DispatchProps => ({ setButtons: (buttons: ButtonState) => dispatch(setButtons(buttons)), - setDirection: (direction: DirectionState) => - dispatch(setDirection(direction)), + setDirection: (direction: DirectionState) => dispatch(setDirection(direction)) }); export default connect(mapStateToProps, mapDispatchToProps)(DesktopControls); diff --git a/js/components/Controls/MobileControls/MobileControls.tsx b/js/components/Controls/MobileControls/MobileControls.tsx index f084e27..a036fbb 100644 --- a/js/components/Controls/MobileControls/MobileControls.tsx +++ b/js/components/Controls/MobileControls/MobileControls.tsx @@ -1,20 +1,20 @@ -// @ts-ignore -import ReactNipple from "react-nipple"; -import { connect } from "react-redux"; -import styled from "styled-components"; -import AbButtons from "../AbButtons/AbButtons"; -import { getDirectionFromAngle } from "../../../helpers/direction"; -import { setDirection, clearDirection } from "../../../redux/actions/direction"; +// @ts-expect-error react-nipple has no d.ts +import ReactNipple from 'react-nipple'; +import { connect } from 'react-redux'; +import styled from 'styled-components'; +import { AbButtons } from '../AbButtons/AbButtons'; +import { getDirectionFromAngle } from '../../../helpers/direction'; +import { setDirection, clearDirection } from '../../../redux/actions/direction'; import { defaultState as defaultDirectionState, - DirectionState, -} from "../../../redux/state/direction"; -import { State } from "../../../redux/state/state"; -import { ButtonState } from "../../../redux/state/buttons"; -import { RustGameboy } from "../../../redux/state/rustGameboy"; -import { getInput } from "../../../helpers/input"; -import StartSelectButtons from "../StartSelectButtons/StartSelectButtons"; -import { Emulator } from "gameboy"; + DirectionState +} from '../../../redux/state/direction'; +import { State } from '../../../redux/state/state'; +import { ButtonState } from '../../../redux/state/buttons'; +import { RustGameboy } from '../../../redux/state/rustGameboy'; +import { getInput } from '../../../helpers/input'; +import { StartSelectButtons } from '../StartSelectButtons/StartSelectButtons'; +import { Emulator } from 'gameboy'; type Props = StateProps & DispatchProps; @@ -25,8 +25,8 @@ interface StateProps { } interface DispatchProps { - setDirection(direction: DirectionState): any; - clearDirection(): void; + setDirection: (direction: DirectionState) => any; + clearDirection: () => void; } const StyledControls = styled(ReactNipple)` @@ -37,10 +37,10 @@ const StyledControls = styled(ReactNipple)` const MobileControls = (props: Props) => (
onMove(props, data.angle.degree)} onEnd={() => onEnd(props)} @@ -70,13 +70,13 @@ const onEnd = (props: Props) => { const mapStateToProps = (state: State): StateProps => ({ buttons: state.buttons, emulator: state.gameboy.emulator!, - rustGameboy: state.rustGameboy, + rustGameboy: state.rustGameboy }); const mapDispatchToProps = (dispatch: any): DispatchProps => ({ setDirection: (direction: DirectionState) => dispatch(setDirection(direction)), - clearDirection: () => dispatch(clearDirection()), + clearDirection: () => dispatch(clearDirection()) }); export default connect(mapStateToProps, mapDispatchToProps)(MobileControls); diff --git a/js/components/Controls/StartSelectButtons/StartSelectButtons.tsx b/js/components/Controls/StartSelectButtons/StartSelectButtons.tsx index d7cd60f..565ee18 100644 --- a/js/components/Controls/StartSelectButtons/StartSelectButtons.tsx +++ b/js/components/Controls/StartSelectButtons/StartSelectButtons.tsx @@ -1,21 +1,19 @@ -import React from "react"; -import { connect } from "react-redux"; -import styled from "styled-components"; -import ControlButton from "../ControlButton/ControlButton"; -import { ButtonState } from "../../../redux/state/buttons"; -import { State } from "../../../redux/state/state"; -import { getInput } from "../../../helpers/input"; -import { RustGameboy } from "../../../redux/state/rustGameboy"; -import { setButtons } from "../../../redux/actions/buttons"; -import { DirectionState } from "../../../redux/state/direction"; -import { mediaMinMd } from "../../../constants/screenSizes"; -import GridCell from "../../GridCell/GridCell"; -import { Emulator } from "gameboy"; -import AllButtons from "../AllButtons/AllButtons"; +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; +import ControlButton from '../ControlButton/ControlButton'; +import { ButtonState } from '../../../redux/state/buttons'; +import { State } from '../../../redux/state/state'; +import { getInput } from '../../../helpers/input'; +import { RustGameboy } from '../../../redux/state/rustGameboy'; +import { setButtons } from '../../../redux/actions/buttons'; +import { DirectionState } from '../../../redux/state/direction'; +import { mediaMinMd } from '../../../constants/screenSizes'; +import GridCell from '../../GridCell/GridCell'; +import { Emulator } from 'gameboy'; +import { AllButtons } from '../AllButtons/AllButtons'; -type Props = OwnProps & StateProps & DispatchProps; - -interface OwnProps { +interface Props { isMobile?: boolean; } @@ -26,11 +24,7 @@ interface StateProps { rustGameboy: RustGameboy; } -interface DispatchProps { - setButtons(buttons: ButtonState): void; -} - -type ButtonKey = "start" | "select"; +type ButtonKey = 'start' | 'select'; const StyledStartSelectControls = styled.div` display: grid; @@ -45,63 +39,70 @@ const StyledStartSelectControls = styled.div` } `; -const StartSelectButtons = (props: Props) => { +export function StartSelectButtons({ isMobile }: Props) { + const dispatch = useDispatch(); + const stateProps = useSelector((state) => ({ + buttons: state.buttons, + direction: state.direction, + emulator: state.gameboy.emulator!, + rustGameboy: state.rustGameboy + })); + + const handleTouch = useCallback( + ( + e: React.TouchEvent, + buttonKey: ButtonKey, + pressed: boolean + ) => { + const updatedState = { ...stateProps.buttons, [buttonKey]: pressed }; + const input = getInput( + stateProps.rustGameboy, + updatedState, + stateProps.direction + ); + dispatch(setButtons(updatedState)); + stateProps.emulator.update_controls(input); + + if (pressed) { + window.navigator.vibrate(10); + } + }, + [ + dispatch, + stateProps.buttons, + stateProps.direction, + stateProps.emulator, + stateProps.rustGameboy + ] + ); + return ( handleTouch(e, props, "start", true)} - onTouchEnd={(e) => handleTouch(e, props, "start", false)} - onTouchCancel={(e) => handleTouch(e, props, "start", false)} + onTouchStart={(e) => handleTouch(e, 'start', true)} + onTouchEnd={(e) => handleTouch(e, 'start', false)} + onTouchCancel={(e) => handleTouch(e, 'start', false)} /> handleTouch(e, props, "select", true)} - onTouchEnd={(e) => handleTouch(e, props, "select", false)} - onTouchCancel={(e) => handleTouch(e, props, "select", false)} + onTouchStart={(e) => handleTouch(e, 'select', true)} + onTouchEnd={(e) => handleTouch(e, 'select', false)} + onTouchCancel={(e) => handleTouch(e, 'select', false)} /> - {props.isMobile && ( + {isMobile && ( )} ); -}; - -const handleTouch = ( - e: React.TouchEvent, - props: Props, - buttonKey: ButtonKey, - pressed: boolean -) => { - const updatedState = { ...props.buttons, [buttonKey]: pressed }; - const input = getInput(props.rustGameboy, updatedState, props.direction); - props.setButtons(updatedState); - props.emulator.update_controls(input); - - if (pressed) { - window.navigator.vibrate(10); - } -}; - -const mapStateToProps = (state: State): StateProps => ({ - buttons: state.buttons, - direction: state.direction, - emulator: state.gameboy.emulator!, - rustGameboy: state.rustGameboy, -}); - -const mapDispatchToProps = (dispatch: any): DispatchProps => ({ - setButtons: (buttons: ButtonState) => dispatch(setButtons(buttons)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(StartSelectButtons); +} diff --git a/js/components/EmulatorInfo/EmulatorInfo.tsx b/js/components/EmulatorInfo/EmulatorInfo.tsx index 0741b47..92a7b0a 100644 --- a/js/components/EmulatorInfo/EmulatorInfo.tsx +++ b/js/components/EmulatorInfo/EmulatorInfo.tsx @@ -1,17 +1,17 @@ import { useSelector } from 'react-redux'; import { Emulator } from 'gameboy'; import { State } from '../../redux/state/state'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { Button, Modal, Tab, Tabs } from 'react-bootstrap'; import chunk from 'chunk'; import styled from 'styled-components'; import { TileInfo } from './TileInfo'; import { CANVAS_WIDTH } from './constants'; -type Props = { +interface Props { show: boolean; setShow: (show: boolean) => void; -}; +} const GRID_GAP = CANVAS_WIDTH + 3; @@ -20,13 +20,15 @@ export function EmulatorInfo({ show, setShow }: Props) { (state) => state.gameboy.emulator! ); const handleClose = useCallback(() => setShow(false), [setShow]); - if (!emulator) return null; - const header = useMemo( () => JSON.parse(emulator.get_header_info()).header, [emulator] ); + if (!emulator) { + return null; + } + const colors = chunk(emulator.get_tiles(), 3); const tiles = chunk(colors, 64); console.log({ tileLEngth: tiles.length }); @@ -51,8 +53,8 @@ export function EmulatorInfo({ show, setShow }: Props) { - {tiles.map((tile) => ( - + {tiles.map((tile, i) => ( + ))} diff --git a/js/components/EmulatorInfo/TileInfo.tsx b/js/components/EmulatorInfo/TileInfo.tsx index f6377da..d978e8c 100644 --- a/js/components/EmulatorInfo/TileInfo.tsx +++ b/js/components/EmulatorInfo/TileInfo.tsx @@ -1,11 +1,11 @@ -import chunk from "chunk"; -import { useState } from "react"; -import styled from "styled-components"; -import { CANVAS_WIDTH, TILE_LENGTH } from "./constants"; +import chunk from 'chunk'; +import { useState } from 'react'; +import styled from 'styled-components'; +import { CANVAS_WIDTH, TILE_LENGTH } from './constants'; -type Props = { +interface Props { tile: number[][]; -}; +} const StyledCanvas = styled.canvas` border: 1px solid #000000; @@ -19,11 +19,11 @@ export function TileInfo({ tile }: Props) { return; } - const newCanvas = document.createElement("canvas"); + const newCanvas = document.createElement('canvas'); newCanvas.width = 8; newCanvas.height = 8; - const ctx = newCanvas.getContext("2d")!; + const ctx = newCanvas.getContext('2d')!; const imageData = ctx.createImageData(TILE_LENGTH, TILE_LENGTH); const data = imageData.data; @@ -42,7 +42,7 @@ export function TileInfo({ tile }: Props) { ctx.putImageData(imageData, 0, 0); - const ctx2 = canvas.getContext("2d")!; + const ctx2 = canvas.getContext('2d')!; ctx2.drawImage(newCanvas, 0, 0, CANVAS_WIDTH, CANVAS_WIDTH); }; diff --git a/js/components/Gameboy/Gameboy.tsx b/js/components/Gameboy/Gameboy.tsx index af4a531..93c583c 100644 --- a/js/components/Gameboy/Gameboy.tsx +++ b/js/components/Gameboy/Gameboy.tsx @@ -2,7 +2,7 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { useMediaQuery } from 'react-responsive'; import { useBeforeunload } from 'react-beforeunload'; -import { Button, Form, ToggleButton } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { useCallback, useState } from 'react'; import gameboyDimensions from '../../constants/gameboy'; import { mobileMediaQuery } from '../../helpers/mediaQueries'; diff --git a/js/components/RomLoader/RomLoader.tsx b/js/components/RomLoader/RomLoader.tsx index e32a1a8..384df45 100644 --- a/js/components/RomLoader/RomLoader.tsx +++ b/js/components/RomLoader/RomLoader.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import DropdownButton from 'react-bootstrap/DropdownButton'; import Dropdown from 'react-bootstrap/Dropdown'; -import { connect, useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useFilePicker } from 'use-file-picker'; import { loadRom } from '../../redux/actions/gameboy'; @@ -40,7 +40,7 @@ export function RomLoader() { setGameboy(gameboy); }; - getGameboy(); + getGameboy().catch((error) => console.error(error)); }); const readFile = useCallback( @@ -91,7 +91,7 @@ export function RomLoader() { if (!!emulator) return null; if (plainFiles.length > 0) { - pickFile(plainFiles[0]); + pickFile(plainFiles[0]).catch((error) => console.error(error)); } return ( @@ -118,10 +118,10 @@ export function RomLoader() { await readFile("Kirby's Dream Land.gb")} > - Kirby's Dream Land + Kirby's Dream Land await readFile('Zelda.gb')}> - The Legend of Zelda Link's Awakening + The Legend of Zelda Link's Awakening await readFile('Pokemon Blue.gb')}> Pokemon Blue @@ -153,7 +153,7 @@ export function RomLoader() { } const getFileData = (key: string): Uint8Array | null => { - if (!window || !window.localStorage) { + if (!window?.localStorage) { return null; } diff --git a/js/components/Screen/Screen.tsx b/js/components/Screen/Screen.tsx index 887cff3..b13c76c 100644 --- a/js/components/Screen/Screen.tsx +++ b/js/components/Screen/Screen.tsx @@ -1,25 +1,20 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { connect } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import chunk from 'chunk'; import { loadWasm } from '../../helpers/wasm'; -import { - SET_RUST_GAMEBOY, - setRustGameboy -} from '../../redux/actions/rustGameboy'; +import { setRustGameboy } from '../../redux/actions/rustGameboy'; import { State } from '../../redux/state/state'; import { RustGameboy } from '../../redux/state/rustGameboy'; import { mediaMinMd } from '../../constants/screenSizes'; import { Emulator, EmulatorState } from 'gameboy'; -import { useSelector } from 'react-redux'; -import { useDispatch } from 'react-redux'; -type Props = { +interface Props { className?: string; width: number; height: number; pixelSize: number; -}; +} interface ScreenState { width: number; @@ -46,18 +41,17 @@ const StyledCanvas = styled.canvas` `; const maxCycles = 69_905; -const sampleRate = 44_100.0; -const sampleCount = 4096; -const latency = 0.032; -const audioCtx = new AudioContext(); +// const sampleRate = 44_100.0; +// const sampleCount = 4096; +// const latency = 0.032; +// const audioCtx = new AudioContext(); export function Screen(props: Props) { - let bytesPerColumn = props.pixelSize * 4; - let bytesPerRow = bytesPerColumn * props.width; + const bytesPerColumn = props.pixelSize * 4; + const bytesPerRow = bytesPerColumn * props.width; - const [requestId, setRequestId] = useState(0); const [canvas, setCanvas] = useState(null); - const [state, setState] = useState({ + const [state] = useState({ width: props.width * props.pixelSize, height: props.height * props.pixelSize, bytesPerRow, @@ -83,17 +77,17 @@ export function Screen(props: Props) { const imageData = ctx.createImageData(state.width, state.height); const data = imageData.data; for (let i = 0; i < chunked.length; i++) { - let rgb = chunked[i]; - let x = i % props.width; - let y = Math.floor(i / props.width); - let yOffset = y * state.bytesPerRow * props.pixelSize; + const rgb = chunked[i]; + const x = i % props.width; + const y = Math.floor(i / props.width); + const yOffset = y * state.bytesPerRow * props.pixelSize; for (let rowNum = 0; rowNum < props.pixelSize; rowNum++) { - let rowOffset = yOffset + rowNum * state.bytesPerRow; - let xOffset = x * state.bytesPerColumn; + const rowOffset = yOffset + rowNum * state.bytesPerRow; + const xOffset = x * state.bytesPerColumn; for (let colNum = 0; colNum < props.pixelSize; colNum++) { - let colOffset = xOffset + colNum * 4; - let offset = rowOffset + colOffset; + const colOffset = xOffset + colNum * 4; + const offset = rowOffset + colOffset; let color = 0; while (color < rgb.length) { data[offset + color] = rgb[color]; @@ -106,62 +100,71 @@ export function Screen(props: Props) { } ctx.putImageData(imageData, 0, 0); - }, [emulator, canvas, state]); + }, [ + emulator, + canvas, + state.width, + state.height, + state.bytesPerRow, + state.bytesPerColumn, + props.width, + props.pixelSize + ]); const animate = useCallback(() => { - setRequestId(requestAnimationFrame(animate)); + requestAnimationFrame(animate); if (!canvas || !emulator || !emulatorState) return; while (true) { const event = emulator.clock_until_event(maxCycles); if (event && event === emulatorState.AudioFull) { - //playAudio(); + // playAudio(); } else if (event === emulatorState.MaxCycles) { break; } } renderScreen(); - }, [canvas, emulator, emulatorState, setRequestId, renderScreen]); - - const playAudio = useCallback(() => { - const audio = emulator.get_audio_buffer(); - let audioBuffer: AudioBuffer; - if (state.emptyAudioBuffers.length === 0) { - audioBuffer = audioCtx.createBuffer(2, sampleCount, sampleRate * 2); - } else { - audioBuffer = state.emptyAudioBuffers[state.emptyAudioBuffers.length - 1]; - setState({ - ...state, - emptyAudioBuffers: state.emptyAudioBuffers.slice(0, -1) - }); - } - - audioBuffer.getChannelData(0).set(audio); - audioBuffer.getChannelData(1).set(audio); - - const node = audioCtx.createBufferSource(); - node.connect(audioCtx.destination); - node.buffer = audioBuffer; - node.onended = () => { - setState({ - ...state, - emptyAudioBuffers: [...state.emptyAudioBuffers, audioBuffer] - }); - }; - - const playTimestamp = Math.max( - audioCtx.currentTime + latency, - state.timestamp - ); - node.start(playTimestamp); - - setState({ - ...state, - timestamp: playTimestamp + sampleCount / 2 / sampleRate - }); - }, [state, setState]); + }, [canvas, emulator, emulatorState, renderScreen]); + + // const playAudio = useCallback(() => { + // const audio = emulator.get_audio_buffer(); + // let audioBuffer: AudioBuffer; + // if (state.emptyAudioBuffers.length === 0) { + // audioBuffer = audioCtx.createBuffer(2, sampleCount, sampleRate * 2); + // } else { + // audioBuffer = state.emptyAudioBuffers[state.emptyAudioBuffers.length - 1]; + // setState({ + // ...state, + // emptyAudioBuffers: state.emptyAudioBuffers.slice(0, -1) + // }); + // } + + // audioBuffer.getChannelData(0).set(audio); + // audioBuffer.getChannelData(1).set(audio); + + // const node = audioCtx.createBufferSource(); + // node.connect(audioCtx.destination); + // node.buffer = audioBuffer; + // node.onended = () => { + // setState({ + // ...state, + // emptyAudioBuffers: [...state.emptyAudioBuffers, audioBuffer] + // }); + // }; + + // const playTimestamp = Math.max( + // audioCtx.currentTime + latency, + // state.timestamp + // ); + // node.start(playTimestamp); + + // setState({ + // ...state, + // timestamp: playTimestamp + sampleCount / 2 / sampleRate + // }); + // }, [state, setState]); useEffect(() => { const fetchWasm = async () => { @@ -173,13 +176,8 @@ export function Screen(props: Props) { console.log('Loaded WASM'); }; - fetchWasm(); - return () => { - if (requestId) { - cancelAnimationFrame(requestId); - } - }; - }, [wasm]); + fetchWasm().catch((error) => console.error(error)); + }, [animate, dispatch, wasm]); return ( diff --git a/js/helpers/input.ts b/js/helpers/input.ts index ec8ec93..c10ace2 100644 --- a/js/helpers/input.ts +++ b/js/helpers/input.ts @@ -1,20 +1,20 @@ -import { ButtonState } from "../redux/state/buttons"; -import { DirectionState } from "../redux/state/direction"; -import { RustGameboy } from "../redux/state/rustGameboy"; +import { ButtonState } from '../redux/state/buttons'; +import { DirectionState } from '../redux/state/direction'; +import { RustGameboy } from '../redux/state/rustGameboy'; export function getInput( - rustGameboy: RustGameboy, - buttons: ButtonState, - direction: DirectionState + rustGameboy: RustGameboy, + buttons: ButtonState, + direction: DirectionState ) { - let input = new rustGameboy.Input!(); - input.a = buttons.a; - input.b = buttons.b; - input.start = buttons.start; - input.select = buttons.select; - input.up = direction.up; - input.down = direction.down; - input.left = direction.left; - input.right = direction.right; - return input; -} \ No newline at end of file + const input = new rustGameboy.Input!(); + input.a = buttons.a; + input.b = buttons.b; + input.start = buttons.start; + input.select = buttons.select; + input.up = direction.up; + input.down = direction.down; + input.left = direction.left; + input.right = direction.right; + return input; +} diff --git a/js/helpers/wasm.ts b/js/helpers/wasm.ts index fd33516..65b2027 100644 --- a/js/helpers/wasm.ts +++ b/js/helpers/wasm.ts @@ -1,4 +1,4 @@ -import { RustGameboy } from "../redux/state/rustGameboy"; +import { RustGameboy } from '../redux/state/rustGameboy'; let wasm: RustGameboy | null = null; export async function loadWasm(): Promise { @@ -7,12 +7,12 @@ export async function loadWasm(): Promise { } try { - const loadedWasm = await import("gameboy"); + const loadedWasm = await import('gameboy'); wasm = loadedWasm; return wasm; } catch (err) { console.error( - `Unexpected error in loadWasm. [Message: ${(err as any).message}]` + `Unexpected error in loadWasm. [Message: ${(err as Error).message}]` ); throw err; diff --git a/package.json b/package.json index d307a27..1280acf 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "build": "rimraf dist pkg && webpack", "start": "rimraf dist pkg && webpack-dev-server --open -d", "start:dev": "yarn build && yarn --force && yarn start", - "test": "cargo test" + "test": "cargo test", + "lint": "yarn eslint js" }, "eslintConfig": { "extends": "react-app" @@ -57,6 +58,7 @@ "copy-webpack-plugin": "^5.0.3", "css-loader": "^5.2.7", "eslint": "^8.0.1", + "eslint-config-prettier": "^8.8.0", "eslint-config-standard-with-typescript": "^34.0.1", "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0", diff --git a/src/gpu/lcd/mod.rs b/src/gpu/lcd/mod.rs index 35f09c4..c24f5c9 100644 --- a/src/gpu/lcd/mod.rs +++ b/src/gpu/lcd/mod.rs @@ -142,12 +142,10 @@ impl Lcd { let previous_lcd_on = self.control.lcd_display_enable(); self.control.set(data); if previous_lcd_on && !self.control.lcd_display_enable() { - info!("Turned off"); self.mode_clock = 0; self.mode = LcdMode::HorizontalBlank; self.line_number = 0; } else if !previous_lcd_on && self.control.lcd_display_enable() { - info!("Turned on"); } } LCD_STATUS => self.set_status(data), diff --git a/yarn.lock b/yarn.lock index 60b59f9..69aabf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2134,6 +2134,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-prettier@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== + eslint-config-standard-with-typescript@^34.0.1: version "34.0.1" resolved "https://registry.yarnpkg.com/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-34.0.1.tgz#4cf797c7f54b2eb1683c7e990b45a257ed4a9992" From 6aa4ce9987814dd48a59aec798912604ad3c622b Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 22 May 2023 16:56:53 -0400 Subject: [PATCH 3/8] Start adding GBC addresses --- src/addresses/gpu/lcd.rs | 6 ++++++ src/gpu/lcd/mod.rs | 7 +++++++ src/gpu/mod.rs | 3 ++- src/mmu/mod.rs | 3 ++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/addresses/gpu/lcd.rs b/src/addresses/gpu/lcd.rs index 9a9ef29..127b630 100644 --- a/src/addresses/gpu/lcd.rs +++ b/src/addresses/gpu/lcd.rs @@ -10,3 +10,9 @@ pub const LCD_OBJ_0_PALETTE_DATA: u16 = 0xFF48; pub const LCD_OBJ_1_PALETTE_DATA: u16 = 0xFF49; pub const LCD_WINDOW_Y: u16 = 0xFF4A; pub const LCD_WINDOW_X: u16 = 0xFF4B; + +// CGB Background color palette specification / Background palette index +pub const LCD_BCPS_BGPI: u16 = 0xFF68; + +// CGB Background color palette data / Background palette data +pub const LCD_BCPD_BGPD: u16 = 0xFF69; diff --git a/src/gpu/lcd/mod.rs b/src/gpu/lcd/mod.rs index c24f5c9..f7880a4 100644 --- a/src/gpu/lcd/mod.rs +++ b/src/gpu/lcd/mod.rs @@ -132,6 +132,10 @@ impl Lcd { SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => { self.video_oam.read(address) } + LCD_BCPS_BGPI..=LCD_BCPD_BGPD => { + info!("Read from CGB Background color palette addresses"); + 0 + } _ => panic!("Invalid lcd address: 0x{:4X}", address), } } @@ -168,6 +172,9 @@ impl Lcd { SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => { self.video_oam.write(address, data) } + LCD_BCPS_BGPI..=LCD_BCPD_BGPD => { + info!("{} written to CGB Background color palette addresses", data); + } _ => panic!("Invalid lcd address: 0x{:4X}", address), } } diff --git a/src/gpu/mod.rs b/src/gpu/mod.rs index 0e715d6..e6f1d08 100644 --- a/src/gpu/mod.rs +++ b/src/gpu/mod.rs @@ -27,7 +27,8 @@ impl Gpu { LCD_CONTROL..=LCD_LYC | LCD_BG_PALETTE_DATA..=LCD_WINDOW_X | VIDEO_RAM_LOWER..=VIDEO_RAM_UPPER - | SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => self.lcd.read(address), + | SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER + | LCD_BCPS_BGPI..=LCD_BCPD_BGPD => self.lcd.read(address), _ => panic!("Invalid GPU address 0x{:4X}", address), } } diff --git a/src/mmu/mod.rs b/src/mmu/mod.rs index 74c18fc..e55fe33 100644 --- a/src/mmu/mod.rs +++ b/src/mmu/mod.rs @@ -13,6 +13,7 @@ mod tests; use crate::addresses::boot_rom::*; use crate::addresses::cartridge::*; use crate::addresses::controls::*; +use crate::addresses::gpu::lcd::LCD_BG_PALETTE_DATA; use crate::cartridge::Cartridge; use crate::constants::boot_rom::*; use crate::controls::*; @@ -133,7 +134,7 @@ impl Mmu { self.write_byte(0xFF42, 0x00); self.write_byte(0xFF43, 0x00); self.write_byte(0xFF45, 0x00); - self.write_byte(0xFF47, 0xFC); + self.write_byte(LCD_BG_PALETTE_DATA, 0xFC); self.write_byte(0xFF48, 0xFF); self.write_byte(0xFF49, 0xFF); self.write_byte(0xFF4A, 0x00); From c139c334e1a8e321a4f119119a10bce71a6bbd1c Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Tue, 23 May 2023 13:58:20 -0400 Subject: [PATCH 4/8] Start implementing gbc palette addresses --- Cargo.toml | 3 ++- js/components/RomLoader/RomLoader.tsx | 2 +- public/roms/cgb-acid2.gbc | Bin 0 -> 32768 bytes src/gpu/lcd/bg_color_palette_spec.rs | 11 +++++++++++ src/gpu/lcd/color_ram/mod.rs | 21 +++++++++++++++++++++ src/gpu/lcd/mod.rs | 13 +++++++++---- src/lib.rs | 7 ++++--- src/mmu/memory_sizes.rs | 1 + 8 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 public/roms/cgb-acid2.gbc create mode 100644 src/gpu/lcd/bg_color_palette_spec.rs create mode 100644 src/gpu/lcd/color_ram/mod.rs diff --git a/Cargo.toml b/Cargo.toml index a982e8b..c54b02f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] bitfield = "0.14.0" +console_error_panic_hook = "0.1.7" console_log = { version = "1", features = ["color"] } # ggez = "0.9.0-rc0" log = "0.4.17" @@ -21,4 +22,4 @@ wasm-bindgen = "0.2.84" wee_alloc = "0.4.5" [package.metadata.wasm-pack.profile.release] -wasm-opt = ["-Oz", "--enable-mutable-globals"] \ No newline at end of file +wasm-opt = ["-Oz", "--enable-mutable-globals"] diff --git a/js/components/RomLoader/RomLoader.tsx b/js/components/RomLoader/RomLoader.tsx index 384df45..b7f8073 100644 --- a/js/components/RomLoader/RomLoader.tsx +++ b/js/components/RomLoader/RomLoader.tsx @@ -30,7 +30,7 @@ export function RomLoader() { (state) => state.gameboy.emulator ); const [openFileSelector, { plainFiles }] = useFilePicker({ - accept: '.gb', + accept: ['.gb', '.gbc'], multiple: false }); diff --git a/public/roms/cgb-acid2.gbc b/public/roms/cgb-acid2.gbc new file mode 100644 index 0000000000000000000000000000000000000000..5f71bd36060b46eefcf7785f81ce16a5a6c5fc67 GIT binary patch literal 32768 zcmeI4e`s6R702(B6iIIULvq*Rfw3ebr>2mSWRz%~_VVn=jpBvQHdA8u&xD&fT4z$% zrkOUxlcJ2yHpOuhmfGMZ#(%8TEA{qA3nLKHn%NPK?f*u@njw-bW^7%x1yW}b@9x}t z-_w&Nx!$1J#yI!!{OVbFZHNdXJDVr~T>63G<(ivd|^1e%jxEy@u?x4wEya zn$!@d+cPpsYCpMt^6t%>H>NIMKJnto+P_@C`N`C!%fI=WeDF~F?zV%6L;FOvR|`e| z`NF901Q`c+ zJ;?`h3T})F`yZBnA)f|0Ddc<0_vN!7-xl)X@?CiX(c8rR#w0s~f;0%!PTtE=~z)fI(nY{=>wgnRMhR#!@b!FCc;>`E*Q zXBV8k4kZ?aGY03NgYr~3rBO@Pw{LVY3wn1B4 zGqnp^FlcJ4m3f#tl~NS9+tjSLcdx0PPC_OpP0f1Q>}FlASPq-boJV)J+uhxL_N*k; z)-vxdm?TepOM_Sue(mX;@;IB`Pg{{Bd$zdr;`=v})a zk)uZ=k(L&rhll;Vf6{oH0>NeG~uPHbQxBg zpQ;KkcCwxEHk^Nia~(9LQ}jIC&A^qydyWZh5%h22%p~DOM>D>N7YrtjCyvC!i6e>Q zEMGj%dwzZwUQ2}hbK0VohI3AX8rXQAlkLyKYk_(c{WV5aRohu@W{bz;VK~6v)Fkgy z`?SUU>l>4~93e2D1^NaZg=!brm_#BGf=bxl=kay>J)RTh!}S#Xo8hnXNo#*^s&8uI z^`^Cp^bCDK&#-z`Rr`o*JwEU`O&`=%@uRK~Z^4L{Owtdb|6%gS-~o1>7ylKq`5M~b z~OQI^>Z_4%Bqmmnu! zAGKd?r@x2seEl>H<}xKtF(2SH1>X96bo&(ZVRoMVl9mP!UV)*ti@6EGTl)xXZ-76~ z2X;1=W#jqy+VuzhtO~xr*i2xp^LO$;&F78%rM(9GiTmer5El+3-{+sxw_r7Y&JQ5K zv;<*WcBg*+zwAxmi4pG^m(Br@%ZnJ?RfpS#QbsnVXgm1*FTX^pHB{ZzM33XpHCzT z&x6AAN|xd2wz@H?t-_9w34#5Zq378zvvSLYva`UTUcQofy8iK<@n3s4 zGVsNi1}l6j#zz#1IN@tCnPbgh`HA;Irn$zO&(yxuV5yDWPu;41`qmEb!_0~UK2g(_ zJqVwXxt8sK56Rrg9)i!i#O8e$!geQo35R|3X5I(ip&{OtXV~X#iiD47wKFUG!*X|4 z$rN6buUs_VMFL0w2_OL^fCP{L5 Self { + Self { + data: [255; COLOR_RAM], + } + } +} diff --git a/src/gpu/lcd/mod.rs b/src/gpu/lcd/mod.rs index f7880a4..3279de0 100644 --- a/src/gpu/lcd/mod.rs +++ b/src/gpu/lcd/mod.rs @@ -5,7 +5,9 @@ use crate::constants::gpu::*; use serde::{Deserialize, Serialize}; pub mod background; +pub mod bg_color_palette_spec; pub mod bg_palette_data; +pub mod color_ram; pub mod lcd_control; pub mod lcd_mode; pub mod lcd_status; @@ -26,6 +28,7 @@ pub struct Lcd { pub frame_complete: bool, pub screen: screen::Screen, pub video_ram: VideoRam, + bg_color_palette_spec: bg_color_palette_spec::BgColorPaletteSpec, bg_palette_data: bg_palette_data::BgPaletteData, control: lcd_control::LcdControl, line_number: u8, @@ -132,8 +135,9 @@ impl Lcd { SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => { self.video_oam.read(address) } - LCD_BCPS_BGPI..=LCD_BCPD_BGPD => { - info!("Read from CGB Background color palette addresses"); + LCD_BCPS_BGPI => self.bg_color_palette_spec.0, + LCD_BCPD_BGPD => { + info!("Read from LCD_BCPD_BGPD"); 0 } _ => panic!("Invalid lcd address: 0x{:4X}", address), @@ -172,8 +176,9 @@ impl Lcd { SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => { self.video_oam.write(address, data) } - LCD_BCPS_BGPI..=LCD_BCPD_BGPD => { - info!("{} written to CGB Background color palette addresses", data); + LCD_BCPS_BGPI => self.bg_color_palette_spec.set(data), + LCD_BCPD_BGPD => { + info!("Writing to LCD_BCPD_BGPD") } _ => panic!("Invalid lcd address: 0x{:4X}", address), } diff --git a/src/lib.rs b/src/lib.rs index 7228e99..38e94b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,14 +41,14 @@ pub struct Emulator { impl Emulator { #[wasm_bindgen(constructor)] pub fn new(bytes: Vec, rom_config: RomConfig) -> Self { - init_console_log(); + init_console_hooks(); let gameboy = Gameboy::new(bytes, &rom_config); Self { cycles: 0, gameboy } } pub fn from_save_data(bytes: Vec, save_data: Vec, rom_config: RomConfig) -> Self { - init_console_log(); + init_console_hooks(); let gameboy = Gameboy::from_save_data(bytes, save_data, &rom_config); Self { cycles: 0, gameboy } @@ -106,8 +106,9 @@ pub enum EmulatorState { MaxCycles, } -fn init_console_log() { +fn init_console_hooks() { console_log::init_with_level(Level::Debug).expect("Error initializing log"); + console_error_panic_hook::set_once(); info!("It works!"); } diff --git a/src/mmu/memory_sizes.rs b/src/mmu/memory_sizes.rs index 0d9872e..35e7bb2 100644 --- a/src/mmu/memory_sizes.rs +++ b/src/mmu/memory_sizes.rs @@ -12,3 +12,4 @@ pub const KILOBYTES_128: usize = 0x20000; pub const BOOT_ROM_SIZE: usize = 256; pub const HIGH_RAM: u16 = (HIGH_RAM_UPPER - HIGH_RAM_LOWER) + 1; pub const VIDEO_OAM: usize = 0xA0; +pub const COLOR_RAM: usize = 64; From 75c6bc5a1b7ed35a29333b1988656c648b9a1865 Mon Sep 17 00:00:00 2001 From: caklimas Date: Wed, 27 Dec 2023 17:06:34 -0500 Subject: [PATCH 5/8] Add CPU Register viewer --- README.md | 11 ++++++++++- js/components/EmulatorInfo/EmulatorInfo.tsx | 14 ++++++++++++++ js/components/RomLoader/RomLoader.tsx | 3 +++ public/roms/Tetris DX.gbc | Bin 0 -> 524288 bytes src/cpu/mod.rs | 2 +- src/gameboy/mod.rs | 4 ++++ src/lib.rs | 4 ++++ 7 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 public/roms/Tetris DX.gbc diff --git a/README.md b/README.md index f7dc6c2..51b957f 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,13 @@ Gameboy emulator written in Rust [![Actions Status](https://github.com/caklimas/rust-gameboy/workflows/Unit%20Tests/badge.svg)](https://github.com/caklimas/rust-gameboy/actions) -[![Actions Status](https://github.com/caklimas/rust-gameboy/workflows/Deployment/badge.svg)](https://github.com/caklimas/rust-gameboy/actions) \ No newline at end of file +[![Actions Status](https://github.com/caklimas/rust-gameboy/workflows/Deployment/badge.svg)](https://github.com/caklimas/rust-gameboy/actions) + +## Running Locally +Run the following commands + +1. `cd src` +2. `wasm-pack build` +3. `cd ..` +4. `yarn` +5. `yarn start:dev` \ No newline at end of file diff --git a/js/components/EmulatorInfo/EmulatorInfo.tsx b/js/components/EmulatorInfo/EmulatorInfo.tsx index 92a7b0a..ceda2a9 100644 --- a/js/components/EmulatorInfo/EmulatorInfo.tsx +++ b/js/components/EmulatorInfo/EmulatorInfo.tsx @@ -25,6 +25,11 @@ export function EmulatorInfo({ show, setShow }: Props) { [emulator] ); + const register = useMemo( + () => JSON.parse(emulator.get_register_info()), + [emulator] + ); + if (!emulator) { return null; } @@ -58,6 +63,15 @@ export function EmulatorInfo({ show, setShow }: Props) { ))} + +

A: {register.a}

+

B: {register.b}

+

C: {register.c}

+

D: {register.d}

+

E: {register.e}

+

H: {register.h}

+

L: {register.l}

+
diff --git a/js/components/RomLoader/RomLoader.tsx b/js/components/RomLoader/RomLoader.tsx index b7f8073..97aaccb 100644 --- a/js/components/RomLoader/RomLoader.tsx +++ b/js/components/RomLoader/RomLoader.tsx @@ -115,6 +115,9 @@ export function RomLoader() { await readFile('Tetris.gb')}> Tetris + await readFile('Tetris DX.gbc')}> + Tetris DX + await readFile("Kirby's Dream Land.gb")} > diff --git a/public/roms/Tetris DX.gbc b/public/roms/Tetris DX.gbc new file mode 100644 index 0000000000000000000000000000000000000000..dbdb8b2e530cb6c7da73c98477584dc5f353e333 GIT binary patch literal 524288 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%0DLbq~eq8((#Who8)749fpdQgp!onzu-q> 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 pIrsW+YYy1 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 literal 0 HcmV?d00001 diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index 11ffb0b..ac838e7 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -26,6 +26,7 @@ const CGB_HARDWARE_DETECTED: u8 = 0x11; pub struct Cpu { pub master_clock_cycles: u32, pub mmu: Mmu, + pub registers: registers::Registers, cb_opcode: bool, halted: bool, index_registers: index_registers::IndexRegisters, @@ -34,7 +35,6 @@ pub struct Cpu { memory_refresh: u8, opcode: usize, program_counter: u16, - registers: registers::Registers, stack_pointer: u16, stopped: bool, } diff --git a/src/gameboy/mod.rs b/src/gameboy/mod.rs index d45a603..e6f74bc 100644 --- a/src/gameboy/mod.rs +++ b/src/gameboy/mod.rs @@ -79,4 +79,8 @@ impl Gameboy { .lcd .set_use_green_colors(use_green_colors); } + + pub fn get_register_info(&self) -> String { + serde_json::to_string(&self.cpu.registers).expect("Could not serialize registers") + } } diff --git a/src/lib.rs b/src/lib.rs index 38e94b3..f1d4300 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,10 @@ impl Emulator { pub fn toggle_color(&mut self, use_green_colors: bool) { self.gameboy.toggle_color(use_green_colors) } + + pub fn get_register_info(&self) -> String { + self.gameboy.get_register_info() + } } #[wasm_bindgen] From e4104b200847dac2c609846d6f88c31b7ca185b2 Mon Sep 17 00:00:00 2001 From: caklimas Date: Wed, 27 Dec 2023 18:11:11 -0500 Subject: [PATCH 6/8] Add notion of pausing emulator --- js/components/Gameboy/Gameboy.tsx | 20 +++++++++++++++++++- js/components/RomLoader/RomLoader.tsx | 1 + js/components/Screen/Screen.tsx | 3 ++- js/redux/actions/paused.ts | 6 ++++++ js/redux/reducers/index.ts | 4 +++- js/redux/reducers/paused.ts | 10 ++++++++++ js/redux/state/state.ts | 6 ++++-- 7 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 js/redux/actions/paused.ts create mode 100644 js/redux/reducers/paused.ts diff --git a/js/components/Gameboy/Gameboy.tsx b/js/components/Gameboy/Gameboy.tsx index 93c583c..a8ec375 100644 --- a/js/components/Gameboy/Gameboy.tsx +++ b/js/components/Gameboy/Gameboy.tsx @@ -1,4 +1,5 @@ import { useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { useMediaQuery } from 'react-responsive'; import { useBeforeunload } from 'react-beforeunload'; @@ -12,6 +13,11 @@ import { mediaMinMd } from '../../constants/screenSizes'; import { Emulator } from 'gameboy'; import { State } from '../../redux/state/state'; import { EmulatorInfo } from '../EmulatorInfo/EmulatorInfo'; +import { setPaused } from '../../redux/actions/paused'; + +const StyledCheck = styled(Form.Check)` + color: white; +`; const StyledGameboy = styled.div` background-color: #bababa; @@ -55,12 +61,14 @@ export function Gameboy() { } }); + const dispatch = useDispatch(); const [checked, setChecked] = useState(false); const [showModal, setShowModal] = useState(false); const handleOpen = useCallback(() => setShowModal(true), [setShowModal]); const isMobile = useMediaQuery(mobileMediaQuery); const pixelSize = isMobile ? 1 : 3; - const currentGame = useSelector((state) => state.currentGame); + const currentGame = useSelector(state => state.currentGame); + const paused = useSelector(state => state.paused); const emulator = useSelector( (state) => state.gameboy.emulator! ); @@ -96,6 +104,16 @@ export function Gameboy() { {!isMobile && showModal && ( )} + {!isMobile && + ) => + dispatch(setPaused(e.target.checked)) + } + />} ); } diff --git a/js/components/RomLoader/RomLoader.tsx b/js/components/RomLoader/RomLoader.tsx index 97aaccb..ac115c7 100644 --- a/js/components/RomLoader/RomLoader.tsx +++ b/js/components/RomLoader/RomLoader.tsx @@ -26,6 +26,7 @@ export function RomLoader() { const [gameboy, setGameboy] = useState(null); const [runBootRom, setRunBootRom] = useState(false); const [cgb, setCgb] = useState(false); + const [paused, setPaused] = useState(false); const emulator = useSelector( (state) => state.gameboy.emulator ); diff --git a/js/components/Screen/Screen.tsx b/js/components/Screen/Screen.tsx index b13c76c..711b8cd 100644 --- a/js/components/Screen/Screen.tsx +++ b/js/components/Screen/Screen.tsx @@ -63,6 +63,7 @@ export function Screen(props: Props) { const [wasm, setWasm] = useState(null); const dispatch = useDispatch(); + const paused = useSelector(state => state.paused); const [emulatorState, setEmulatorState] = useState< typeof EmulatorState | null >(null); @@ -116,7 +117,7 @@ export function Screen(props: Props) { if (!canvas || !emulator || !emulatorState) return; - while (true) { + while (true && !paused) { const event = emulator.clock_until_event(maxCycles); if (event && event === emulatorState.AudioFull) { // playAudio(); diff --git a/js/redux/actions/paused.ts b/js/redux/actions/paused.ts new file mode 100644 index 0000000..0e70fd3 --- /dev/null +++ b/js/redux/actions/paused.ts @@ -0,0 +1,6 @@ +export const SET_PAUSED = Symbol("SET_PAUSED"); + +export const setPaused = (paused: boolean) => ({ + type: SET_PAUSED, + paused +}); \ No newline at end of file diff --git a/js/redux/reducers/index.ts b/js/redux/reducers/index.ts index 42e55e1..4837ded 100644 --- a/js/redux/reducers/index.ts +++ b/js/redux/reducers/index.ts @@ -3,6 +3,7 @@ import { buttons } from './buttons'; import { currentGame } from './currentGame'; import { direction } from './direction'; import { gameboy } from './gameboy'; +import { paused } from './paused'; import { rustGameboy } from './rustGameboy'; export const rootReducer = combineReducers({ @@ -10,5 +11,6 @@ export const rootReducer = combineReducers({ currentGame, direction, gameboy, - rustGameboy + rustGameboy, + paused }); \ No newline at end of file diff --git a/js/redux/reducers/paused.ts b/js/redux/reducers/paused.ts new file mode 100644 index 0000000..169dd01 --- /dev/null +++ b/js/redux/reducers/paused.ts @@ -0,0 +1,10 @@ +import { SET_PAUSED } from "../actions/paused"; + +export function paused(paused = false, action: any) { + switch (action.type) { + case SET_PAUSED: + return action.paused; + default: + return paused; + } +} \ No newline at end of file diff --git a/js/redux/state/state.ts b/js/redux/state/state.ts index 1e3b520..9ba437a 100644 --- a/js/redux/state/state.ts +++ b/js/redux/state/state.ts @@ -8,7 +8,8 @@ export interface State { direction: DirectionState, gameboy: GameboyState, rustGameboy: RustGameboy, - currentGame: string + currentGame: string, + paused: boolean }; export const defaultState: State = { @@ -16,5 +17,6 @@ export const defaultState: State = { direction: directionState, gameboy: gameboyState, rustGameboy: rustGameboyState, - currentGame: '' + currentGame: '', + paused: false }; \ No newline at end of file From 7fafe5b8be046260313f260b968b78d7b10a75a0 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 1 Jan 2024 14:12:08 -0500 Subject: [PATCH 7/8] update after startup sequence for cgb --- js/components/RomLoader/RomLoader.tsx | 19 ++++----------- src/apu/square_channel/mod.rs | 2 +- src/cartridge/mod.rs | 3 ++- src/cpu/mod.rs | 33 ++++++++++++++++++--------- src/rom_config/mod.rs | 5 ++-- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/js/components/RomLoader/RomLoader.tsx b/js/components/RomLoader/RomLoader.tsx index ac115c7..f9c3121 100644 --- a/js/components/RomLoader/RomLoader.tsx +++ b/js/components/RomLoader/RomLoader.tsx @@ -25,8 +25,6 @@ export function RomLoader() { const dispatch = useDispatch(); const [gameboy, setGameboy] = useState(null); const [runBootRom, setRunBootRom] = useState(false); - const [cgb, setCgb] = useState(false); - const [paused, setPaused] = useState(false); const emulator = useSelector( (state) => state.gameboy.emulator ); @@ -56,7 +54,7 @@ export function RomLoader() { const bytes = new Uint8Array(buffer); const key = fileName.replace(/.gb$/, ''); const fileData = getFileData(key); - const romConfig = new gameboy.RomConfig(runBootRom, cgb); + const romConfig = new gameboy.RomConfig(runBootRom); if (fileData === null) { const emulator = new gameboy.Emulator(bytes, romConfig); dispatch(setCurrentGame(key)); @@ -71,7 +69,7 @@ export function RomLoader() { dispatch(loadRom(emulator)); } }, - [runBootRom, cgb, gameboy, dispatch] + [runBootRom, gameboy, dispatch] ); const pickFile = useCallback( @@ -82,11 +80,11 @@ export function RomLoader() { const buffer = await file.arrayBuffer(); const bytes = new Uint8Array(buffer); - const romConfig = new gameboy.RomConfig(runBootRom, cgb); + const romConfig = new gameboy.RomConfig(runBootRom); const emulator = new gameboy.Emulator(bytes, romConfig); dispatch(loadRom(emulator)); }, - [runBootRom, cgb, gameboy, dispatch] + [runBootRom, gameboy, dispatch] ); if (!!emulator) return null; @@ -143,15 +141,6 @@ export function RomLoader() { setRunBootRom(e.target.checked) } /> - ) => - setCgb(e.target.checked) - } - />
); } diff --git a/src/apu/square_channel/mod.rs b/src/apu/square_channel/mod.rs index a48e4fa..a7f6132 100644 --- a/src/apu/square_channel/mod.rs +++ b/src/apu/square_channel/mod.rs @@ -205,7 +205,7 @@ impl SquareChannel { } fn set_sweep_register(&mut self, value: u8) { - let mut sweep_register = self + let sweep_register = self .sweep_register .as_mut() .unwrap_or_else(|| panic!("Sweep not available")); diff --git a/src/cartridge/mod.rs b/src/cartridge/mod.rs index 21f55ad..567c490 100644 --- a/src/cartridge/mod.rs +++ b/src/cartridge/mod.rs @@ -5,14 +5,15 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default)] pub struct Cartridge { + pub header: cartridge_header::CartridgeHeader, pub mbc: Mbc, - header: cartridge_header::CartridgeHeader, } impl Cartridge { pub fn new(bytes: Vec) -> Self { let header = cartridge_header::CartridgeHeader::new(&bytes, true); let mbc = get_mbc(&header, bytes); + info!("Cartridge type: {:?}", header.cgb_mode); Cartridge { header, mbc } } diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index ac838e7..60bd44a 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -6,6 +6,7 @@ pub mod registers; #[cfg(test)] mod tests; +use crate::cartridge::cartridge_header::cgb_mode::CgbMode; use crate::constants::cpu::PROGRAM_START; use crate::mmu::interrupts::Interrupt; use crate::mmu::Mmu; @@ -18,8 +19,6 @@ use opcodes::{ }; use serde::{Deserialize, Serialize}; -use self::opcodes::opcode::CpuRegister; - const CGB_HARDWARE_DETECTED: u8 = 0x11; #[derive(Serialize, Deserialize, Default)] @@ -27,6 +26,7 @@ pub struct Cpu { pub master_clock_cycles: u32, pub mmu: Mmu, pub registers: registers::Registers, + cb_opcode: bool, halted: bool, index_registers: index_registers::IndexRegisters, @@ -41,8 +41,9 @@ pub struct Cpu { impl Cpu { pub fn new(cartridge: Cartridge, rom_config: &RomConfig) -> Self { + let mmu = Mmu::new(cartridge, rom_config); let mut cpu = Cpu { - mmu: Mmu::new(cartridge, rom_config), + mmu, ..Default::default() }; @@ -290,14 +291,24 @@ impl Cpu { self.program_counter = PROGRAM_START; self.stack_pointer = 0xFFFE; - self.registers.set_target_16(&CpuRegister16::AF, 0x01B0); - self.registers.set_target_16(&CpuRegister16::BC, 0x0013); - self.registers.set_target_16(&CpuRegister16::DE, 0x00D8); - self.registers.set_target_16(&CpuRegister16::HL, 0x014D); - - if rom_config.cgb { - // https://gbdev.io/pandocs/CGB_Registers.html#detecting-cgb-and-gba-functions - self.registers.set_target(&CpuRegister::A, 0x11); + + match &self.mmu.cartridge.header.cgb_mode { + CgbMode::CgbMonochrome => { + self.registers.set_target_16(&CpuRegister16::AF, 0x1180); + self.registers.set_target_16(&CpuRegister16::BC, 0x0000); + self.registers.set_target_16(&CpuRegister16::DE, 0xFF56); + self.registers.set_target_16(&CpuRegister16::HL, 0x000D); + } + CgbMode::CgbOnly => panic!("Unsupported"), + CgbMode::NonCgb => { + self.registers.set_target_16(&CpuRegister16::AF, 0x01B0); + self.registers.set_target_16(&CpuRegister16::BC, 0x0013); + self.registers.set_target_16(&CpuRegister16::DE, 0x00D8); + self.registers.set_target_16(&CpuRegister16::HL, 0x014D); + } } + + // https://gbdev.io/pandocs/CGB_Registers.html#detecting-cgb-and-gba-functions + info!("A after startup: {}", self.registers.a); } } diff --git a/src/rom_config/mod.rs b/src/rom_config/mod.rs index d2226ce..088986f 100644 --- a/src/rom_config/mod.rs +++ b/src/rom_config/mod.rs @@ -3,13 +3,12 @@ use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] pub struct RomConfig { pub run_boot_rom: bool, - pub cgb: bool, } #[wasm_bindgen] impl RomConfig { #[wasm_bindgen(constructor)] - pub fn new(run_boot_rom: bool, cgb: bool) -> Self { - Self { run_boot_rom, cgb } + pub fn new(run_boot_rom: bool) -> Self { + Self { run_boot_rom } } } From 95de4b2bb1968be99624e30a919ed7c16bd7dd53 Mon Sep 17 00:00:00 2001 From: caklimas Date: Mon, 1 Jan 2024 14:30:42 -0500 Subject: [PATCH 8/8] Add more logging --- src/cpu/mod.rs | 8 +++----- src/gpu/lcd/mod.rs | 10 ++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index 60bd44a..fb60c17 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -289,9 +289,7 @@ impl Cpu { return; } - self.program_counter = PROGRAM_START; - self.stack_pointer = 0xFFFE; - + // https://gbdev.io/pandocs/Power_Up_Sequence.html#cpu-registers match &self.mmu.cartridge.header.cgb_mode { CgbMode::CgbMonochrome => { self.registers.set_target_16(&CpuRegister16::AF, 0x1180); @@ -308,7 +306,7 @@ impl Cpu { } } - // https://gbdev.io/pandocs/CGB_Registers.html#detecting-cgb-and-gba-functions - info!("A after startup: {}", self.registers.a); + self.program_counter = PROGRAM_START; + self.stack_pointer = 0xFFFE; } } diff --git a/src/gpu/lcd/mod.rs b/src/gpu/lcd/mod.rs index 3279de0..d8daad0 100644 --- a/src/gpu/lcd/mod.rs +++ b/src/gpu/lcd/mod.rs @@ -135,7 +135,10 @@ impl Lcd { SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => { self.video_oam.read(address) } - LCD_BCPS_BGPI => self.bg_color_palette_spec.0, + LCD_BCPS_BGPI => { + info!("Read from LCD_BCPS_BGPI"); + self.bg_color_palette_spec.0 + } LCD_BCPD_BGPD => { info!("Read from LCD_BCPD_BGPD"); 0 @@ -176,7 +179,10 @@ impl Lcd { SPRITE_ATTRIBUTE_TABLE_LOWER..=SPRITE_ATTRIBUTE_TABLE_UPPER => { self.video_oam.write(address, data) } - LCD_BCPS_BGPI => self.bg_color_palette_spec.set(data), + LCD_BCPS_BGPI => { + info!("Writing to LCD_BCPS_BGPI"); + self.bg_color_palette_spec.set(data) + } LCD_BCPD_BGPD => { info!("Writing to LCD_BCPD_BGPD") }