diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index 0baea01..2cbd8a0 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -97,5 +97,5 @@ jobs: See README.md for the author quickstart and the wasm ABI this SDK targets. NOTES - gh release create "$TAG" --target "$GITHUB_SHA" \ + gh release create "$TAG" --target "$GITHUB_SHA" --latest=false \ --title "$TAG" --notes-file .artifacts/sdk-release-notes.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6724eab..7f0d7c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,5 +123,6 @@ jobs: with: tools-ref: f412061a7a3f0914db9e09d1159e69504aee8706 binary: termcade + extra-files: THIRD_PARTY_NOTICES.txt tag: ${{ needs.identity.outputs.tag }} artifact-id: ${{ needs.accept-cli.outputs.artifact-id }} diff --git a/.gitignore b/.gitignore index 8e8f4f0..2c0854f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ games/*/build/ # …except the starter pack, embedded in the Rust binary. These committed # content assets come from termcade-games; ABI-v1 packages remain compatible. !internal/starter/packs/*.tcade +!internal/plugin/testdata/legacy-starters/*.tcade game.wasm .artifacts/ @@ -15,3 +16,5 @@ __pycache__/ /target/ /internal/plugin/testdata/*/target/ + +.playwright-cli/ diff --git a/Cargo.lock b/Cargo.lock index 1516224..67440ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2631,7 +2631,7 @@ dependencies = [ [[package]] name = "termcade" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "crossterm", @@ -2702,7 +2702,7 @@ dependencies = [ [[package]] name = "termcade-sdk" -version = "0.1.0" +version = "0.1.1" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index cba378a..51eaf72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "3" [package] name = "termcade" -version = "0.1.0" +version = "0.1.1" edition = "2024" rust-version = "1.94" publish = false diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt new file mode 100644 index 0000000..16a98a1 --- /dev/null +++ b/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,32 @@ +The seeded random compatibility implementation and seeding data in +internal/random derive from the Go 1.27.3 math/rand implementation. +Sources: src/math/rand/rng.go and src/math/rand/rand.go in the Go distribution. +https://go.googlesource.com/go/+/refs/tags/go1.27.3/src/math/rand/ + +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/plugin/src/lib.rs b/internal/plugin/src/lib.rs index 8e14d35..f1326f6 100644 --- a/internal/plugin/src/lib.rs +++ b/internal/plugin/src/lib.rs @@ -137,7 +137,7 @@ impl Runtime { { bail!("invalid host playfield requirements"); } - if shape.cols == 0 || shape.rows == 0 || shape.cols > 3 || shape.rows > 3 { + if shape.cols == 0 || shape.rows == 0 || shape.cols > 32 || shape.rows > 32 { bail!("invalid host cell subdivision"); } let module = self.compile(key, wasm)?; diff --git a/internal/plugin/src/tests.rs b/internal/plugin/src/tests.rs index a322c3a..04505d0 100644 --- a/internal/plugin/src/tests.rs +++ b/internal/plugin/src/tests.rs @@ -55,9 +55,55 @@ async fn rust_sdk_guest_roundtrip_keeps_ticks_keys_pixels_hud_gameover_and_reset } #[tokio::test] -async fn original_published_go_starters_run_in_the_rust_host_at_every_density() { +async fn rust_sdk_guest_supports_browser_framebuffers_above_terminal_density() { let runtime = Runtime::new().unwrap(); - let packs = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../starter/packs"); + let wasm = fs::read(fixture("probe")).expect("build the locked Rust WASIp1 probe first"); + for scale in [4, 8, 16, 32] { + let shape = CellShape::host(scale, scale); + let mut game = runtime + .load("browser-probe", &wasm, info(), shape.clone()) + .await + .unwrap(); + let mut canvas = Canvas::new(16, 8, BLACK, shape); + assert_eq!(canvas.pixel_size(), (16 * scale, 4 * scale)); + game.reset().await.unwrap(); + game.key(Key::A, true).await.unwrap(); + game.update().await.unwrap(); + game.draw(&mut canvas).await.unwrap(); + assert_eq!(game.score().await.unwrap(), 10); + assert_eq!(canvas.pixels().iter().filter(|&&p| p != BLACK).count(), 2); + game.key(Key::A, false).await.unwrap(); + game.draw(&mut canvas).await.unwrap(); + assert_eq!(canvas.pixels().iter().filter(|&&p| p != BLACK).count(), 1); + assert!(game.error().is_none()); + } +} + +#[tokio::test] +async fn legacy_go_starter_fixtures_run_in_the_rust_host_at_every_density() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/legacy-starters"); + let source: serde_json::Value = + serde_json::from_slice(&fs::read(root.join("SOURCE.json")).unwrap()).unwrap(); + for row in source["packages"].as_array().unwrap() { + assert_eq!( + format!( + "{:x}", + Sha256::digest(fs::read(root.join(row["file"].as_str().unwrap())).unwrap()) + ), + row["sha256"].as_str().unwrap() + ); + } + check_game_packages("testdata/legacy-starters").await; +} + +#[tokio::test] +async fn current_starters_run_in_the_rust_host_at_every_density() { + check_game_packages("../starter/packs").await; +} + +async fn check_game_packages(relative: &str) { + let runtime = Runtime::new().unwrap(); + let packs = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative); let mut count = 0; for entry in fs::read_dir(packs).unwrap() { let path = entry.unwrap().path(); diff --git a/internal/plugin/testdata/hang-rust/Cargo.lock b/internal/plugin/testdata/hang-rust/Cargo.lock index b276e0d..6879739 100644 --- a/internal/plugin/testdata/hang-rust/Cargo.lock +++ b/internal/plugin/testdata/hang-rust/Cargo.lock @@ -95,7 +95,7 @@ dependencies = [ [[package]] name = "termcade-sdk" -version = "0.1.0" +version = "0.1.1" dependencies = [ "serde", "serde_json", diff --git a/internal/plugin/testdata/legacy-starters/SOURCE.json b/internal/plugin/testdata/legacy-starters/SOURCE.json new file mode 100644 index 0000000..8e239c7 --- /dev/null +++ b/internal/plugin/testdata/legacy-starters/SOURCE.json @@ -0,0 +1,24 @@ +{ + "origin": "Termcade vendored Go starter packages preserved from main 9de13d1af144a89ba2ec1123b73b707a7f1bff60", + "purpose": "immutable legacy ABI compatibility fixtures; never installed as current starters", + "packages": [ + { + "file": "asteroid.tcade", + "id": "aviorstudio/asteroid", + "version": "0.0.2", + "sha256": "0d705e21e932e3c0e6bc8cc7534caad47fbdb8b701998928d73a4169f53385a0" + }, + { + "file": "brickough.tcade", + "id": "aviorstudio/brickough", + "version": "0.0.2", + "sha256": "3896be61e1499687a9fb26241923a3a79a2bd5c56d2875c3091016ec17ee50b5" + }, + { + "file": "tetris.tcade", + "id": "aviorstudio/tetris", + "version": "0.0.2", + "sha256": "cb5a5d8832bebb9501445ae03889ca5f3bcf9cb28da56344befed57b644a29be" + } + ] +} diff --git a/internal/plugin/testdata/legacy-starters/asteroid.tcade b/internal/plugin/testdata/legacy-starters/asteroid.tcade new file mode 100644 index 0000000..047b5ae Binary files /dev/null and b/internal/plugin/testdata/legacy-starters/asteroid.tcade differ diff --git a/internal/plugin/testdata/legacy-starters/brickough.tcade b/internal/plugin/testdata/legacy-starters/brickough.tcade new file mode 100644 index 0000000..4b92b47 Binary files /dev/null and b/internal/plugin/testdata/legacy-starters/brickough.tcade differ diff --git a/internal/plugin/testdata/legacy-starters/tetris.tcade b/internal/plugin/testdata/legacy-starters/tetris.tcade new file mode 100644 index 0000000..113fab5 Binary files /dev/null and b/internal/plugin/testdata/legacy-starters/tetris.tcade differ diff --git a/internal/plugin/testdata/probe-rust/Cargo.lock b/internal/plugin/testdata/probe-rust/Cargo.lock index 8b1f7c9..813a7b3 100644 --- a/internal/plugin/testdata/probe-rust/Cargo.lock +++ b/internal/plugin/testdata/probe-rust/Cargo.lock @@ -95,7 +95,7 @@ dependencies = [ [[package]] name = "termcade-sdk" -version = "0.1.0" +version = "0.1.1" dependencies = [ "serde", "serde_json", diff --git a/internal/starter/SOURCE.json b/internal/starter/SOURCE.json new file mode 100644 index 0000000..ab3db9d --- /dev/null +++ b/internal/starter/SOURCE.json @@ -0,0 +1,46 @@ +{ + "repository": "https://github.com/aviorstudio/termcade-games", + "source_commit": "863a053cab6672c77b4863d4876a69ffbad687e7", + "implementation": "Rust WASIp1, ABI 1", + "packages": [ + { + "file": "asteroid.tcade", + "id": "aviorstudio/asteroid", + "version": "0.1.0", + "tag": "asteroid-v0.1.0", + "sha256": "7fbafb0f2b2b3100c41dfb225d26ee512ada85a57c3518ea957864d7aaad69bb", + "bytes": 42780, + "width": 72, + "height": 40, + "release_ci": 41228408956, + "attestation_verified": true, + "marketplace_verified": true + }, + { + "file": "brickough.tcade", + "id": "aviorstudio/brickough", + "version": "0.1.0", + "tag": "brickough-v0.1.0", + "sha256": "7289719ea5a66e607188b5623ac61bae492f8a2f90a559abcfa83083b6cf148c", + "bytes": 31472, + "width": 64, + "height": 40, + "release_ci": 41228411148, + "attestation_verified": true, + "marketplace_verified": true + }, + { + "file": "tetris.tcade", + "id": "aviorstudio/tetris", + "version": "0.1.0", + "tag": "tetris-v0.1.0", + "sha256": "33eb430858972d7336013a8848ad42fc9748d67089d234fcf579f7dcf72c1384", + "bytes": 36061, + "width": 32, + "height": 40, + "release_ci": 41228413496, + "attestation_verified": true, + "marketplace_verified": true + } + ] +} diff --git a/internal/starter/packs/asteroid.tcade b/internal/starter/packs/asteroid.tcade index 047b5ae..f930e4f 100644 Binary files a/internal/starter/packs/asteroid.tcade and b/internal/starter/packs/asteroid.tcade differ diff --git a/internal/starter/packs/brickough.tcade b/internal/starter/packs/brickough.tcade index 4b92b47..874be23 100644 Binary files a/internal/starter/packs/brickough.tcade and b/internal/starter/packs/brickough.tcade differ diff --git a/internal/starter/packs/tetris.tcade b/internal/starter/packs/tetris.tcade index 113fab5..73a0775 100644 Binary files a/internal/starter/packs/tetris.tcade and b/internal/starter/packs/tetris.tcade differ diff --git a/scripts/build-native.py b/scripts/build-native.py index 0601348..74bcc11 100644 --- a/scripts/build-native.py +++ b/scripts/build-native.py @@ -18,3 +18,6 @@ assert system in ('Linux','Darwin','Windows') and arch in ('x86_64','arm64') env=dict(os.environ,RELEASE_PLATFORM=system,RELEASE_ARCH=arch,RELEASE_EXTENSION='zip' if system=='Windows' else 'tar.gz') subprocess.run(['python3','scripts/package-cli.py'],env=env,check=True) + +# Carry the notice for seed data embedded by the Rust starter games. +shutil.copyfile("THIRD_PARTY_NOTICES.txt", "dist/THIRD_PARTY_NOTICES.txt") diff --git a/scripts/check_archives.py b/scripts/check_archives.py index 58d61dd..7c5706c 100644 --- a/scripts/check_archives.py +++ b/scripts/check_archives.py @@ -41,7 +41,9 @@ def main(): if not os.environ.get('NATIVE_CLI_ARCHIVES'):expected={host_archive().name} actual={p.name for p in Path('dist').iterdir()if p.name.endswith(('.tar.gz','.zip'))} assert actual==expected,'incomplete or unexpected native archives' - sums=''.join(hashlib.sha256((Path('dist')/name).read_bytes()).hexdigest()+' '+name+'\n'for name in sorted(expected)) + notice=Path('dist/THIRD_PARTY_NOTICES.txt') + assert notice.read_bytes()==Path('THIRD_PARTY_NOTICES.txt').read_bytes(),'starter notices changed' + sums=''.join(hashlib.sha256((Path('dist')/name).read_bytes()).hexdigest()+' '+name+'\n'for name in sorted(expected|{'THIRD_PARTY_NOTICES.txt'})) receipt=Path('dist/checksums.txt') if args.record:receipt.write_text(sums);return assert receipt.read_text()==sums,'release archive bytes changed' diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 0638bd9..50fe965 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "termcade-sdk" -version = "0.1.0" +version = "0.1.1" edition = "2024" rust-version = "1.94" publish = false diff --git a/sdk/src/canvas.rs b/sdk/src/canvas.rs index 595ac37..86b2843 100644 --- a/sdk/src/canvas.rs +++ b/sdk/src/canvas.rs @@ -26,7 +26,10 @@ impl Canvas { "canvas needs a positive width and positive even height" ); assert!( - shape.cols > 0 && shape.rows > 0 && shape.cols * shape.rows <= 9, + shape.cols > 0 + && shape.rows > 0 + && (shape.cols.checked_mul(shape.rows).is_some_and(|n| n <= 9) + || shape.glyphs.is_empty()), "invalid cell subdivision" ); let fw = w.checked_mul(shape.cols).expect("canvas width overflow"); @@ -179,6 +182,10 @@ impl Canvas { } } pub fn cell(&self, x: usize, y: usize) -> Cell { + assert!( + !self.shape.glyphs.is_empty() && self.shape.cols * self.shape.rows <= 9, + "glyph rendering needs a terminal cell shape" + ); let mut buffer = [Color(0); 9]; let mut n = 0; for row in 0..self.shape.rows { diff --git a/sdk/src/tcgame.rs b/sdk/src/tcgame.rs index 4e64e22..f370653 100644 --- a/sdk/src/tcgame.rs +++ b/sdk/src/tcgame.rs @@ -22,7 +22,7 @@ mod reactor { ((u64::from(high) << 32) | u64::from(low)) as i64 } pub fn initialize(cols: i32, rows: i32, game: Box) -> i64 { - if !(1..=3).contains(&cols) || !(1..=3).contains(&rows) { + if !(1..=32).contains(&cols) || !(1..=32).contains(&rows) { return 0; } let info = game.info(); diff --git a/sdk/tests/browser_framebuffer.rs b/sdk/tests/browser_framebuffer.rs new file mode 100644 index 0000000..0e5ebda --- /dev/null +++ b/sdk/tests/browser_framebuffer.rs @@ -0,0 +1,22 @@ +use termcade_sdk::{BLACK, Canvas, CellShape, RED}; +#[test] +fn browser_framebuffers_keep_logical_coordinates_above_terminal_density() { + for scale in [4, 8, 16, 32] { + let mut canvas = Canvas::new(16, 8, BLACK, CellShape::host(scale, scale)); + assert_eq!(canvas.pixel_size(), (16 * scale, 8 * scale / 2)); + canvas.fill_rect_f(1., 1., 2., 3., RED); + assert_eq!(canvas.at_pixel(scale as i64, (scale / 2) as i64), RED); + assert_eq!( + canvas.at_pixel((3 * scale - 1) as i64, (4 * scale / 2 - 1) as i64), + RED + ); + assert_eq!( + canvas.at_pixel((scale - 1) as i64, (scale / 2) as i64), + BLACK + ); + assert_eq!( + canvas.at_pixel((3 * scale) as i64, (scale / 2) as i64), + BLACK + ); + } +}