# play96
`play96` is a Rust library and CLI for running `*96` libretro fantasy-console
cores in automated test harnesses. It can step cartridges frame by frame,
control input, inspect framebuffer pixels, make assertions, capture PNG files,
exercise save states, and compare deterministic frame logs.
## Build and test
```sh
git submodule update --init --recursive
cargo test
cargo build --release
```
The build produces the `play96-cli` executable and Rust, C, and C++ compatible
libraries. C headers are in `include/`.
## Rust library
Add the repository dependency to a test harness or library:
```toml
[dev-dependencies]
play96 = { git = "https://codeberg.org/compu96/play96.git" }
```
For a packaged release, use `play96 = "0.3"` after publishing it to the
configured Cargo registry.
```rust,no_run
use play96::{Color, InputDevice, MouseButton, Session, key};
fn main() -> Result<(), play96::Error> {
let mut game = Session::new("wasm96_libretro.dll", "cartridge.wasm")?;
game.set_controller_device(0, InputDevice::Mouse);
game.set_key(key::SPACE, true);
game.set_mouse_delta(4, -2);
game.set_mouse_button(MouseButton::Left, true);
game.set_mouse_wheel(1, 0);
game.run_frames(10)?;
assert_eq!(game.framebuffer_width(), 320);
game.assert_pixel(12, 20, Color::rgb(255, 0, 0))?;
game.assert_pixel_xrgb(13, 20, 0x00ff00)?;
assert_eq!(game.frame_hash(), 0x0123_4567_89ab_cdef);
// Audio is interleaved signed 16-bit stereo PCM from the latest frame.
assert_eq!(game.audio_sample_rate(), 48_000.0);
game.assert_audio_sample(100, 0, 1200, 10)?;
game.assert_audio_hash(0xfedc_ba98_7654_3210)?;
let left_sample = game.audio_frame(100)?[0];
assert_eq!(left_sample, game.audio_samples()[200]);
game.write_png("actual.png")?;
Ok(())
}
```
`pixel` and `pixel_xrgb` return colors for custom assertions. `audio_samples`
returns the latest frame's interleaved stereo waveform, while `audio_frame`,
`assert_audio_sample`, and `assert_audio_hash` support exact deterministic
sound tests. Failed built-in assertions report expected and actual values.
Keyboard input accepts libretro key codes through `set_key`; common codes are
available in the `key` module. Mouse movement and wheel values are relative and
apply to the next frame, while mouse buttons remain pressed until changed or
cleared. Select `InputDevice::Mouse` for mouse-oriented cores; joypad remains
the default device.
Only one `Session` may execute at a time because libretro callbacks are global
to the dynamically loaded core.
## C++ wrapper
Link against the generated `play96` library and include `include/play96.hpp`:
```cpp
#include <play96.hpp>
int main() {
play96::Session game("wasm96_libretro.dll", "cartridge.wasm");
game.run_frames(10);
game.assert_pixel(12, 20, {255, 0, 0});
game.assert_pixel_xrgb(13, 20, 0x00ff00);
}
```
The wrapper throws `std::runtime_error` when loading, execution, or an
assertion fails. `include/play96.h` provides the underlying exception-free C
ABI for other languages.
## CLI
```sh
play96-cli --core ../wasm96/zig-out/bin/wasm96_libretro.dll \
--cart ../wasm96/templates/cartridge-c/cartridge.wasm \
--device mouse \
--frames 30 --shot-every 15 \
--macro "0-10:key.space+mouse.left+mouse.dx=4+mouse.dy=-2" \
--script input.rhai \
--assert-pixel 12,20,ff0000 \
--assert-audio-sample 100,0,1200,10 \
--assert-audio-hash fedcba9876543210 \
--netplay-log wasm96.netplay
```
Screenshots are written as PNG files. A recorded netplay log can be checked by
running the same command with `--netplay-check` instead of `--netplay-log`.
`--macro` specifies fixed frame ranges. For conditional input, pass a Rhai file
with `--script`; it must define `fn frame(frame)`. Each call may use
`button("a")`, `key("space")`, `mouse_button("left")`, `mouse_delta(x, y)`,
and `mouse_wheel(vertical, horizontal)`. Script input is combined with macro
input for that frame. For example:
```rhai
fn frame(frame) {
if frame % 30 == 0 {
button("a");
mouse_delta(4, -2);
}
}
```
Captured RGB565, 0RGB1555, and XRGB8888 frames are normalized to XRGB8888, so
pixel assertions, framebuffer reads, hashes, and PNG captures use the same
`0x00RRGGBB` values regardless of the core's native video format.