# Peppi
[![test](https://github.com/hohav/peppi/actions/workflows/test.yml/badge.svg)](https://github.com/hohav/peppi/actions/workflows/Build.yml)
[![](https://img.shields.io/crates/v/peppi.svg)](https://crates.io/crates/peppi)
[![](https://docs.rs/peppi/badge.svg)](https://docs.rs/peppi/)
Peppi is a Rust parser for [.slp](https://github.com/project-slippi/slippi-wiki/blob/master/SPEC.md) game replay files for [Super Smash Brothers Melee](https://en.wikipedia.org/wiki/Super_Smash_Bros._Melee) for the Nintendo GameCube. These replays are generated by Jas Laferriere's [Slippi](https://github.com/JLaferri/project-slippi) recording code, which runs on a Wii or the [Dolphin](https://dolphin-emu.org/) emulator.
⚠️ The `slp` tool has moved to the [peppi-slp](https://github.com/hohav/peppi-slp) crate.
## Installation
In your `Cargo.toml`:
```toml
[dependencies]
peppi = "2.0"
```
## Usage
One-shot `.slp` parsing with [slippi::read](https://docs.rs/peppi/latest/peppi/io/slippi/de/fn.read.html) (use [peppi::read](https://docs.rs/peppi/latest/peppi/io/peppi/de/fn.read.html) instead for `.slpp`):
```rust
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);
}
```
<details>
<summary>A more involved example</summary>
```rust
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,
}
}
}
}
```
</details>
<details>
<summary>Live parsing</summary>
```rust
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();
}
}
```
</details>
## Development
The Rust source files in [`src/frame`](src/frame) are generated using Clojure from [`frames.json`](gen/resources/frames.json), which describes all the per-frame fields present in each version of the [spec](https://github.com/project-slippi/slippi-wiki/blob/master/SPEC.md). 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](#peppi-format) that is more compressible and easier to work with than `.slp`.
## Peppi Format
The Peppi format (`.slpp`) is a [GNU tar](https://en.wikipedia.org/wiki/Tar_(computing)) archive containing the following files, in order:
- `peppi.json`: Peppi-specific info.
- `metadata.json`: Slippi's [metadata block](https://github.com/project-slippi/slippi-wiki/blob/master/SPEC.md#the-metadata-element).
- `start.json`: JSON representation of the [Game Start](https://github.com/project-slippi/slippi-wiki/blob/master/SPEC.md#game-start) event.
- `start.raw`: Raw binary Game Start event.
- `end.json`: JSON representation of the [Game End](https://github.com/project-slippi/slippi-wiki/blob/master/SPEC.md#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](https://arrow.apache.org/docs/format/Columnar.html#ipc-file-format) 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`](https://github.com/hohav/peppi-slp) CLI tool.