# 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.