15 releases (2 stable)

new 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

#242 in Parser implementations

Download history 7/week @ 2023-12-26 1/week @ 2024-01-02 27/week @ 2024-02-20 63/week @ 2024-02-27 76/week @ 2024-03-12 70/week @ 2024-03-19 248/week @ 2024-03-26 156/week @ 2024-04-02

550 downloads per month

MIT license

215KB
6.5K SLoC

Rust 5.5K SLoC // 0.0% comments Clojure 1K SLoC // 0.0% comments Edn 3 SLoC

Peppi

test

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

~15–27MB
~472K SLoC