hohav / peppi

Rust parser for Slippi SSBM replay files
MIT License
37 stars 9 forks source link

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 ```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, } } } } ```
Live parsing ```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(); } } ```

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

Peppi Format

The Peppi format (.slpp) is a GNU tar archive containing the following files, in order:

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.