15 releases (2 stable)
2.0.1 | Apr 7, 2024 |
---|---|
2.0.0 | Mar 30, 2024 |
2.0.0-alpha.4 | Jan 7, 2024 |
2.0.0-alpha.2 | Dec 9, 2023 |
1.0.0-alpha.3 | Jul 5, 2021 |
#27 in Emulators
128 downloads per month
215KB
6.5K
SLoC
Peppi
Peppi is a Rust parser for .slp game replay files for Super Smash Brothers Melee for the Nintendo GameCube. These replays are generated by Jas Laferriere's Slippi recording code, which runs on a Wii or the Dolphin emulator.
⚠️ The slp
tool has moved to the peppi-slp crate.
Installation
In your Cargo.toml
:
[dependencies]
peppi = "2.0"
Usage
One-shot .slp
parsing with slippi::read (use peppi::read instead for .slpp
):
use std::{fs, io};
use peppi::io::slippi::read;
fn main() {
let mut r = io::BufReader::new(fs::File::open("tests/data/game.slp").unwrap());
let game = read(&mut r, None).unwrap();
println!("{:#?}", game);
}
A more involved example
use std::{fs, io};
use peppi::io::slippi::read;
use peppi::frame::Rollbacks;
// `ssbm-data` provides enums for characters, stages, action states, etc.
// You can just hard-code constants instead, if you prefer.
use ssbm_data::action_state::Common::{self, *};
/// Print the frames on which each player died.
fn main() {
let mut r = io::BufReader::new(fs::File::open("tests/data/game.slp").unwrap());
let game = read(&mut r, None).unwrap();
let mut is_dead = vec![false; game.frames.ports.len()];
let rollbacks = game.frames.rollbacks(Rollbacks::ExceptLast);
for frame_idx in 0..game.frames.len() {
if rollbacks[frame_idx] {
continue;
}
for (port_idx, port_data) in game.frames.ports.iter().enumerate() {
match port_data
.leader
.post
.state
.get(frame_idx)
.and_then(|s| Common::try_from(s).ok())
{
Some(DeadDown)
| Some(DeadLeft)
| Some(DeadRight)
| Some(DeadUp)
| Some(DeadUpStar)
| Some(DeadUpStarIce)
| Some(DeadUpFall)
| Some(DeadUpFallHitCamera)
| Some(DeadUpFallHitCameraFlat)
| Some(DeadUpFallIce)
| Some(DeadUpFallHitCameraIce) => {
if !is_dead[port_idx] {
is_dead[port_idx] = true;
println!(
"{} died on frame {}",
game.start.players[port_idx].port,
game.frames.id.get(frame_idx).unwrap(),
)
}
}
_ => is_dead[port_idx] = false,
}
}
}
}
Live parsing
use std::fs;
use std::io::BufReader;
use byteorder::ReadBytesExt;
use peppi::io::slippi::de;
fn main() {
let mut r = BufReader::new(fs::File::open("tests/data/game.slp").unwrap());
// UBJSON wrapper (skip if using spectator protocol)
let size = de::parse_header(&mut r, None).unwrap() as usize;
// payload sizes & game start
let mut state = de::parse_start(&mut r, None).unwrap();
// loop until we hit GameEnd or run out of bytes
while de::parse_event(&mut r, &mut state, None).unwrap() != de::Event::GameEnd as u8
&& state.bytes_read() < size
{
println!(
"current frame number: {:?}",
state.frames().id.iter().last()
);
}
// `U` (0x55) means metadata next (skip if using spectator protocol)
if r.read_u8().unwrap() == 0x55 {
de::parse_metadata(&mut r, &mut state, None).unwrap();
}
}
Development
The Rust source files in src/frame
are generated using Clojure from frames.json
, which describes all the per-frame fields present in each version of the spec. If you modify frames.json
or the generator code in gen/src
, run gen/scripts/frames
to regenerate those Rust files.
If you're adding support for a new version of the spec, you'll also need to bump peppi::io::slippi::MAX_SUPPORTED_VERSION
.
Goals
- Performance: Peppi aims to be the fastest parser for
.slp
files. - Ergonomics: It should be easy and natural to work with parsed data.
- Lenience: accept-and-warn on malformed data, when feasible.
- Cross-language support: other languages should be able to interact with Peppi easily and efficiently.
- Round-tripping: Peppi can parse a replay and then write it back, bit-for-bit identically.
- Alternative format: Peppi provides an alternative format that is more compressible and easier to work with than
.slp
.
Peppi Format
The Peppi format (.slpp
) is a GNU tar archive containing the following files, in order:
peppi.json
: Peppi-specific info.metadata.json
: Slippi's metadata block.start.json
: JSON representation of the Game Start event.start.raw
: Raw binary Game Start event.end.json
: JSON representation of the Game End event.end.raw
: Raw binary Game End event.frames.arrow
: Frame data in Arrow format (see below).
The bulk of this data is in frames.arrow
, an Arrow IPC file containing all of the game's frame data. This is a columnar format, which makes .slpp
about twice as compressible as .slp
.
To convert between formats, use the slp
CLI tool.
Dependencies
~16–26MB
~478K SLoC