diff --git a/Cargo.toml b/Cargo.toml index 6d75a68f..af29369a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "emulator/mgba", "emulator/mgba-sys", "emulator/test-runner", + "emulator/screenshot-generator", "website/backtrace", ] diff --git a/agb/examples/just_build.rs b/agb/examples/just_build.rs deleted file mode 100644 index b19262db..00000000 --- a/agb/examples/just_build.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![no_std] -#![no_main] - -#[panic_handler] -fn panic_handler(_info: &core::panic::PanicInfo) -> ! { - loop {} -} - -#[no_mangle] -pub extern "C" fn __RUST_INTERRUPT_HANDLER(_: u16) {} - -// implementation of tonc's "My first GBA demo" -// https://coranac.com/tonc/text/first.htm - -#[no_mangle] -pub fn main() -> ! { - unsafe { - *(0x0400_0000 as *mut u32) = 0x0403; - let video = 0x0600_0000 as *mut u16; - *video.offset(120 + 80 * 240) = 0x001F; - *video.offset(136 + 80 * 240) = 0x03E0; - *video.offset(120 + 96 * 240) = 0x7C00; - } - loop {} -} diff --git a/emulator/screenshot-generator/Cargo.toml b/emulator/screenshot-generator/Cargo.toml new file mode 100644 index 00000000..1b3001c2 --- /dev/null +++ b/emulator/screenshot-generator/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "screenshot-generator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mgba = { path = "../mgba" } +clap = { version = "4", features = ["derive"] } +anyhow = "1" +image = { version = "0.24", default-features = false, features = [ "png", "bmp" ] } +agb-gbafix = { path = "../../agb-gbafix" } \ No newline at end of file diff --git a/emulator/screenshot-generator/src/image_generate.rs b/emulator/screenshot-generator/src/image_generate.rs new file mode 100644 index 00000000..b4ed863c --- /dev/null +++ b/emulator/screenshot-generator/src/image_generate.rs @@ -0,0 +1,23 @@ +use image::{DynamicImage, GenericImage, Rgba}; + +const WIDTH: usize = 240; +const HEIGHT: usize = 160; + +pub fn generate_image(video_buffer: &[u32]) -> DynamicImage { + let mut dynamic_image = DynamicImage::new( + WIDTH.try_into().unwrap(), + HEIGHT.try_into().unwrap(), + image::ColorType::Rgba8, + ); + for y in 0..HEIGHT { + for x in 0..WIDTH { + let video_pixel = video_buffer[x + y * WIDTH]; + let mut pixels = video_pixel.to_le_bytes(); + pixels[3] = 255; + + dynamic_image.put_pixel(x.try_into().unwrap(), y.try_into().unwrap(), Rgba(pixels)); + } + } + + dynamic_image +} diff --git a/emulator/screenshot-generator/src/main.rs b/emulator/screenshot-generator/src/main.rs new file mode 100644 index 00000000..2146b414 --- /dev/null +++ b/emulator/screenshot-generator/src/main.rs @@ -0,0 +1,96 @@ +use std::{ + error::Error, + fs::File, + io::{BufWriter, Read}, + path::{Path, PathBuf}, +}; + +use anyhow::anyhow; +use clap::Parser; +use image::DynamicImage; +use image_generate::generate_image; +use mgba::{LogLevel, Logger, MCore, MemoryBacked, VFile}; + +mod image_generate; + +static LOGGER: Logger = Logger::new(my_logger); + +fn my_logger(_category: &str, _level: LogLevel, _s: String) {} + +#[derive(Parser)] +struct CliArguments { + #[arg(long)] + rom: PathBuf, + #[arg(long)] + frames: usize, + #[arg(long)] + output: PathBuf, +} + +struct ScreenshotGenerator { + mgba: MCore, +} + +impl ScreenshotGenerator { + fn new(rom: V) -> Result> { + let mut mgba = MCore::new().ok_or(anyhow!("cannot create core"))?; + + mgba::set_global_default_logger(&LOGGER); + + mgba.load_rom(rom); + + Ok(Self { mgba }) + } + + fn run(mut self, frames: usize) -> DynamicImage { + for _ in 0..frames { + self.mgba.frame(); + } + + generate_image(self.mgba.video_buffer()) + } +} + +fn main() -> Result<(), Box> { + let args = CliArguments::parse(); + + let rom = load_rom(args.rom)?; + let rom = MemoryBacked::new(rom); + + let image = ScreenshotGenerator::new(rom)?.run(args.frames); + + let mut output = BufWriter::new( + File::options() + .write(true) + .create(true) + .truncate(true) + .open(args.output)?, + ); + image.write_to(&mut output, image::ImageOutputFormat::Png)?; + + Ok(()) +} + +fn load_rom>(path: P) -> anyhow::Result> { + let mut input_file = File::open(path)?; + let mut input_file_buffer = Vec::new(); + + input_file.read_to_end(&mut input_file_buffer)?; + + let mut elf_buffer = Vec::new(); + + let inculde_debug_info = false; + if agb_gbafix::write_gba_file( + &input_file_buffer, + Default::default(), + agb_gbafix::PaddingBehaviour::DoNotPad, + inculde_debug_info, + &mut elf_buffer, + ) + .is_ok() + { + Ok(elf_buffer) + } else { + Ok(input_file_buffer) + } +} diff --git a/justfile b/justfile index 95e4a4b6..a3164741 100644 --- a/justfile +++ b/justfile @@ -112,8 +112,35 @@ build-combo-rom-site: mkdir -p website/agb/src/roms gzip -9 -c examples/target/examples/combo.gba > website/agb/src/roms/combo.gba.gz +generate-screenshot *args: + (cd emulator/screenshot-generator && cargo build --release && cd "{{invocation_directory()}}" && "$CARGO_TARGET_DIR/release/screenshot-generator" {{args}}) -setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace + +build-site-examples: build-release + #!/usr/bin/env bash + set -euxo pipefail + + mkdir -p website/agb/src/roms/examples + + EXAMPLES="$(cd agb/examples; ls *.rs)" + EXAMPLE_DEFINITIONS="export const Examples: {url: URL, example_name: string, screenshot: StaticImageData }[] = [" > website/agb/src/roms/examples/examples.ts + EXAMPLE_IMAGE_IMPORTS="import { StaticImageData } from 'next/image';"; + + for EXAMPLE_NAME in $EXAMPLES; do + EXAMPLE="${EXAMPLE_NAME%.rs}" + just gbafix "$CARGO_TARGET_DIR/thumbv4t-none-eabi/release/examples/$EXAMPLE" --output="$CARGO_TARGET_DIR/thumbv4t-none-eabi/release/examples/$EXAMPLE.gba" + gzip -9 -c $CARGO_TARGET_DIR/thumbv4t-none-eabi/release/examples/$EXAMPLE.gba > website/agb/src/roms/examples/$EXAMPLE.gba.gz + just generate-screenshot --rom="$CARGO_TARGET_DIR/thumbv4t-none-eabi/release/examples/$EXAMPLE.gba" --frames=10 --output=website/agb/src/roms/examples/$EXAMPLE.png + EXAMPLE_IMAGE_IMPORTS="$EXAMPLE_IMAGE_IMPORTS import $EXAMPLE from './$EXAMPLE.png';" + EXAMPLE_DEFINITIONS="$EXAMPLE_DEFINITIONS {url: new URL('./$EXAMPLE.gba.gz', import.meta.url), example_name: '$EXAMPLE', screenshot: $EXAMPLE}," + done + + EXAMPLE_DEFINITIONS="$EXAMPLE_DEFINITIONS ];" + echo "$EXAMPLE_IMAGE_IMPORTS" > website/agb/src/roms/examples/examples.ts + echo "$EXAMPLE_DEFINITIONS" >> website/agb/src/roms/examples/examples.ts + + +setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace build-site-examples (cd website/agb && npm install --no-save --prefer-offline --no-audit) build-site-app: setup-app-build diff --git a/website/agb/.gitignore b/website/agb/.gitignore index ccbe9f5d..9fabce27 100644 --- a/website/agb/.gitignore +++ b/website/agb/.gitignore @@ -38,4 +38,5 @@ next-env.d.ts vendor *.gba -*.gba.gz \ No newline at end of file +*.gba.gz +src/roms \ No newline at end of file diff --git a/website/agb/package-lock.json b/website/agb/package-lock.json index b0c7d808..d0416d4f 100644 --- a/website/agb/package-lock.json +++ b/website/agb/package-lock.json @@ -11,6 +11,7 @@ "next": "14.2.3", "react": "^18", "react-dom": "^18", + "react-syntax-highlighter": "^15.5.0", "sharp": "^0.33.3", "styled-components": "^6.1.8" }, @@ -18,6 +19,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-syntax-highlighter": "^15.5.13", "@types/styled-components": "^5.1.34", "eslint": "^8", "eslint-config-next": "14.2.3", @@ -37,7 +39,6 @@ "version": "7.24.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -861,6 +862,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", @@ -911,6 +921,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/styled-components": { "version": "5.1.34", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", @@ -927,6 +947,12 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, + "node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", @@ -1453,6 +1479,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1495,6 +1551,16 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2355,6 +2421,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2440,6 +2519,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2742,6 +2829,42 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2815,6 +2938,30 @@ "node": ">= 0.4" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -2933,6 +3080,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2990,6 +3147,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3352,6 +3519,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", @@ -3684,6 +3865,24 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3809,6 +4008,15 @@ "node": ">= 0.8.0" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3820,6 +4028,19 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3878,6 +4099,22 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-syntax-highlighter": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", + "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -3899,11 +4136,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -4247,6 +4507,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -4936,6 +5206,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/website/agb/package.json b/website/agb/package.json index b7c6f864..f3f04c6b 100644 --- a/website/agb/package.json +++ b/website/agb/package.json @@ -12,6 +12,7 @@ "next": "14.2.3", "react": "^18", "react-dom": "^18", + "react-syntax-highlighter": "^15.5.0", "sharp": "^0.33.3", "styled-components": "^6.1.8" }, @@ -19,6 +20,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-syntax-highlighter": "^15.5.13", "@types/styled-components": "^5.1.34", "eslint": "^8", "eslint-config-next": "14.2.3", diff --git a/website/agb/src/app/examples/[example]/emulator.tsx b/website/agb/src/app/examples/[example]/emulator.tsx new file mode 100644 index 00000000..972b475a --- /dev/null +++ b/website/agb/src/app/examples/[example]/emulator.tsx @@ -0,0 +1,21 @@ +"use client"; + +import MgbaWrapper from "@/components/mgba/mgbaWrapper"; +import { Examples } from "@/roms/examples/examples"; +import { slugify } from "@/sluggify"; +import { useMemo } from "react"; + +function gameUrl(exampleName: string) { + const example = Examples.find((x) => slugify(x.example_name) === exampleName); + if (!example) { + throw new Error(`cannot find example ${exampleName}`); + } + + return example.url; +} + +export function Emulator({ exampleName }: { exampleName: string }) { + const example = useMemo(() => gameUrl(exampleName), [exampleName]); + + return ; +} diff --git a/website/agb/src/app/examples/[example]/page.tsx b/website/agb/src/app/examples/[example]/page.tsx new file mode 100644 index 00000000..b4438087 --- /dev/null +++ b/website/agb/src/app/examples/[example]/page.tsx @@ -0,0 +1,58 @@ +import { Examples } from "@/roms/examples/examples"; +import { slugify } from "@/sluggify"; +import { Emulator } from "./emulator"; +import { ContentBlock } from "@/components/contentBlock"; +import * as fs from "node:fs/promises"; +import { BackToExampleLink, Code } from "./styles"; + +export async function generateStaticParams() { + return Examples.map((example) => ({ + example: slugify(example.example_name), + })); +} + +function getExample(sluggedExample: string) { + const example = Examples.find( + (x) => slugify(x.example_name) === sluggedExample + ); + if (!example) { + throw new Error(`cannot find example ${sluggedExample}`); + } + + return example; +} + +async function loadSourceCode(exampleName: string) { + const source = await fs.readFile(`../../agb/examples/${exampleName}.rs`); + + return source.toString(); +} + +export default async function Page({ + params, +}: { + params: { example: string }; +}) { + const exmaple = getExample(params.example); + const source = await loadSourceCode(exmaple.example_name); + + return ( + <> + +

Example: {params.example}

+ + < Back to examples + +
+ + + + + {source} + + + <> + + + ); +} diff --git a/website/agb/src/app/examples/[example]/styles.tsx b/website/agb/src/app/examples/[example]/styles.tsx new file mode 100644 index 00000000..7588c1aa --- /dev/null +++ b/website/agb/src/app/examples/[example]/styles.tsx @@ -0,0 +1,14 @@ +"use client"; + +import Link from "next/link"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { styled } from "styled-components"; + +export const Code = styled(SyntaxHighlighter)` + font-size: 0.8rem; +`; + +export const BackToExampleLink = styled(Link)` + text-decoration: none; + color: black; +`; diff --git a/website/agb/src/app/examples/page.tsx b/website/agb/src/app/examples/page.tsx new file mode 100644 index 00000000..1a256403 --- /dev/null +++ b/website/agb/src/app/examples/page.tsx @@ -0,0 +1,42 @@ +import { Metadata } from "next"; +import { ContentBlock } from "@/components/contentBlock"; +import { slugify } from "@/sluggify"; +import { GameDisplay, GameGrid, GameImage } from "./styles"; +import { Examples } from "@/roms/examples/examples"; + +export const metadata: Metadata = { + title: "Examples - agb", +}; + +export default function ShowcasePage() { + return ( + <> + +

Examples

+
+ + + {Examples.map((example, idx) => ( + + ))} + + + + ); +} + +function Game({ example }: { example: (typeof Examples)[number] }) { + const screenshot = example.screenshot; + return ( + + +

{example.example_name}

+
+ ); +} diff --git a/website/agb/src/app/examples/styles.tsx b/website/agb/src/app/examples/styles.tsx new file mode 100644 index 00000000..6eac2dae --- /dev/null +++ b/website/agb/src/app/examples/styles.tsx @@ -0,0 +1,34 @@ +"use client"; + +import Link from "next/link"; +import styled from "styled-components"; +import Image from "next/image"; + +export const GameGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, min(100vw, 600px)); + justify-content: center; + gap: 48px; +`; + +export const GameImage = styled(Image)` + width: 100%; + width: max( + round(down, 100%, calc(240 * var(--device-pixel))), + min(calc(240 * var(--device-pixel)), 100vw) + ); + height: auto; + image-rendering: pixelated; +`; + +export const GameDisplay = styled(Link)` + width: 100%; + text-align: center; + color: black; + text-decoration: none; + + h2 { + margin: 0; + margin-top: 8px; + } +`;