peppi 1.0.0-alpha.6

Parser for Slippi replay files
Documentation
use std::{collections::HashMap, fs, io};

use chrono::{DateTime, Utc};

use peppi::{
	model::{
		buttons::{Logical, Physical},
		enums::{
			action_state::{State, Zelda},
			character::{Internal, External},
			item,
			stage::Stage,
		},
		frame::Buttons,
		game::{DashBack, End, EndMethod, Frames, Game, Player, PlayerType, Start, ShieldDrop, Ucf},
		item::Item,
		metadata::{self, Metadata},
		primitives::{Direction, Port, Position, Velocity},
		slippi::{Slippi, Version},
	},
	serde,
};

fn read_game(path: &str) -> Result<Game, String> {
	let mut buf = io::BufReader::new(
		fs::File::open(path).unwrap());
	peppi::game(&mut buf, None, None).map_err(|e| format!("couldn't deserialize game: {:?}", e))
}

fn game(name: &str) -> Result<Game, String> {
	read_game(&format!("tests/data/{}.slp", name))
}

fn button_seq(game:&Game) -> Result<Vec<Buttons>, String> {
	match &game.frames {
		Frames::P2(frames) => {
			let mut last_buttons:Option<Buttons> = None;
			let mut button_seq = Vec::<Buttons>::new();
			for frame in frames {
				let b = frame.ports[0].leader.pre.buttons;
				if (b.logical.0 > 0 || b.physical.0 > 0) && Some(b) != last_buttons {
					button_seq.push(b);
					last_buttons = Some(b);
				}
			}
			Ok(button_seq)
		},
		_ => Err("wrong number of ports".to_string()),
	}
}

#[test]
fn slippi_old_version() -> Result<(), String> {
	let game = game("v0.1")?;
	let players = game.start.players;

	assert_eq!(game.start.slippi.version, Version(0, 1, 0));
	assert_eq!(game.metadata.duration, None);

	assert_eq!(players.len(), 2);
	assert_eq!(players[0].character, External::FOX);
	assert_eq!(players[1].character, External::GANONDORF);

	Ok(())
}

#[test]
fn basic_game() -> Result<(), String> {
	let game = game("game")?;

	assert_eq!(game.metadata, Metadata {
		date: "2018-06-22T07:52:59Z".parse::<DateTime<Utc>>().ok(),
		duration: Some(5209),
		platform: Some("dolphin".to_string()),
		players: Some(vec![
			metadata::Player {
				port: Port::P1,
				characters: {
					let mut m = HashMap::new();
					m.insert(Internal::MARTH, 5209);
					Some(m)
				},
				netplay: None,
			},
			metadata::Player {
				port: Port::P2,
				characters: {
					let mut m = HashMap::new();
					m.insert(Internal::FOX, 5209);
					Some(m)
				},
				netplay: None,
			},
		]),
		console: None,
	});

	assert_eq!(game.start, Start {
		slippi: Slippi { version: Version(1, 0, 0) },
		bitfield: [50, 1, 134, 76],
		is_raining_bombs: false,
		is_teams: false,
		item_spawn_frequency: -1,
		self_destruct_score: -1,
		stage: Stage::YOSHIS_STORY,
		timer: 480,
		item_spawn_bitfield: [255, 255, 255, 255, 255],
		damage_ratio: 1.0,
		players: vec![
			Player {
				port: Port::P1,
				character: External::MARTH,
				r#type: PlayerType::HUMAN,
				stocks: 4,
				costume: 3,
				team: None,
				handicap: 9,
				bitfield: 192,
				cpu_level: None,
				offense_ratio: 0.0,
				defense_ratio: 1.0,
				model_scale: 1.0,
				ucf: Some(Ucf {
					dash_back: None,
					shield_drop: None,
				}),
				name_tag: None,
				netplay: None,
			},
			Player {
				port: Port::P2,
				character: External::FOX,
				r#type: PlayerType::CPU,
				stocks: 4,
				costume: 0,
				team: None,
				handicap: 9,
				bitfield: 64,
				cpu_level: Some(1),
				offense_ratio: 0.0,
				defense_ratio: 1.0,
				model_scale: 1.0,
				ucf: Some(Ucf {
					dash_back: None,
					shield_drop: None,
				}),
				name_tag: None,
				netplay: None,
			},
		],
		random_seed: 3803194226,
		is_pal: None,
		is_frozen_ps: None,
		scene: None,
		raw_bytes: vec![
			1, 0, 0, 0, 50, 1, 134, 76, 195, 0, 0, 0, 0, 0, 0, 255, 255, 110, 0, 8, 0, 0, 1, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 4, 3, 0, 0, 0, 0, 9, 0, 120, 0, 192, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 2, 1, 4, 0, 0, 1, 0, 0, 9, 0, 120, 0, 64, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 26, 3, 4, 0, 0, 255, 0, 0, 9, 0, 120, 0, 64, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 26, 3, 4, 0, 0, 255, 0, 0, 9, 0, 120, 0, 64, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 33, 3, 4, 0, 0, 255, 0, 0, 9, 0, 120, 0, 64, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 33, 3, 4, 0, 0, 255, 0, 0, 9, 0, 120, 0, 64, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 63, 128, 0, 0, 226, 176, 35, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
		],
	});

	assert_eq!(game.end, End {
		method: EndMethod::RESOLVED,
		lras_initiator: None,
	});

	match game.frames {
		Frames::P2(frames) => assert_eq!(frames.len(), 5209),
		_ => Err("wrong number of ports")?,
	};

	Ok(())
}

#[test]
fn ics() -> Result<(), String> {
	let game = game("ics")?;
	assert_eq!(game.metadata.players, Some(vec![
		metadata::Player {
			port: Port::P1,
			characters: Some({
				let mut m = HashMap::new();
				m.insert(Internal::NANA, 344);
				m.insert(Internal::POPO, 344);
				m
			}),
			netplay: None,
		},
		metadata::Player {
			port: Port::P2,
			characters: Some({
				let mut m = HashMap::new();
				m.insert(Internal::JIGGLYPUFF, 344);
				m
			}),
			netplay: None,
		},
	]));
	assert_eq!(game.start.players[0].character, External::ICE_CLIMBERS);
	match game.frames {
		Frames::P2(frames) => assert!(frames[0].ports[0].follower.is_some()),
		_ => Err("wrong number of ports")?,
	};
	Ok(())
}

#[test]
fn ucf() -> Result<(), String> {
	assert_eq!(game("shield_drop")?.start.players[0].ucf,
		Some(Ucf { dash_back: None, shield_drop: Some(ShieldDrop::UCF) }));
	assert_eq!(game("dash_back")?.start.players[0].ucf,
		Some(Ucf { dash_back: Some(DashBack::UCF), shield_drop: None }));
	Ok(())
}

#[test]
fn buttons_lzrs() -> Result<(), String> {
	let game = game("buttons_lrzs")?;
	assert_eq!(button_seq(&game)?, vec![
		Buttons {
			logical: Logical::TRIGGER_ANALOG,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::TRIGGER_ANALOG | Logical::L,
			physical: Physical::L,
		},
		Buttons {
			logical: Logical::TRIGGER_ANALOG,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::TRIGGER_ANALOG | Logical::R,
			physical: Physical::R,
		},
		Buttons {
			logical: Logical::TRIGGER_ANALOG | Logical::A | Logical::Z,
			physical: Physical::Z,
		},
		Buttons {
			logical: Logical::START,
			physical: Physical::START,
		},
	]);
	Ok(())
}

#[test]
fn buttons_abxy() -> Result<(), String> {
	let game = game("buttons_abxy")?;
	assert_eq!(button_seq(&game)?, vec![
		Buttons {
			logical: Logical::A,
			physical: Physical::A,
		},
		Buttons {
			logical: Logical::B,
			physical: Physical::B,
		},
		Buttons {
			logical: Logical::X,
			physical: Physical::X,
		},
		Buttons {
			logical: Logical::Y,
			physical: Physical::Y,
		},
	]);
	Ok(())
}

#[test]
fn dpad_udlr() -> Result<(), String> {
	let game = game("dpad_udlr")?;
	assert_eq!(button_seq(&game)?, vec![
		Buttons {
			logical: Logical::DPAD_UP,
			physical: Physical::DPAD_UP,
		},
		Buttons {
			logical: Logical::DPAD_DOWN,
			physical: Physical::DPAD_DOWN,
		},
		Buttons {
			logical: Logical::DPAD_LEFT,
			physical: Physical::DPAD_LEFT,
		},
		Buttons {
			logical: Logical::DPAD_RIGHT,
			physical: Physical::DPAD_RIGHT,
		},
	]);
	Ok(())
}

#[test]
fn cstick_udlr() -> Result<(), String> {
	let game = game("cstick_udlr")?;
	assert_eq!(button_seq(&game)?, vec![
		Buttons {
			logical: Logical::CSTICK_UP,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::CSTICK_DOWN,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::CSTICK_LEFT,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::CSTICK_RIGHT,
			physical: Physical::NONE,
		},
	]);
	Ok(())
}

#[test]
fn joystick_udlr() -> Result<(), String> {
	let game = game("joystick_udlr")?;
	assert_eq!(button_seq(&game)?, vec![
		Buttons {
			logical: Logical::JOYSTICK_UP,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::JOYSTICK_DOWN,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::JOYSTICK_LEFT,
			physical: Physical::NONE,
		},
		Buttons {
			logical: Logical::JOYSTICK_RIGHT,
			physical: Physical::NONE,
		},
	]);
	Ok(())
}

#[test]
fn nintendont() -> Result<(), String> {
	let game = game("nintendont")?;
	assert_eq!(game.metadata.platform, Some("nintendont".to_string()));
	Ok(())
}

#[test]
fn netplay() -> Result<(), String> {
	let game = game("netplay")?;
	let players = game.metadata.players.ok_or("missing metadata.players")?;
	let names: Vec<_> = players.into_iter().flat_map(|p| p.netplay).map(|n| n.name).collect();
	assert_eq!(names, vec!["abcdefghijk", "nobody"]);
	Ok(())
}

#[test]
fn console_name() -> Result<(), String> {
	let game = game("console_name")?;
	assert_eq!(game.metadata.console, Some("Station 1".to_string()));
	Ok(())
}

#[test]
fn v2() -> Result<(), String> {
	let game = game("v2.0")?;
	assert_eq!(game.start.slippi.version, Version(2, 0, 1));
	Ok(())
}

#[test]
fn unknown_event() -> Result<(), String> {
	game("unknown_event")?;
	// TODO: check for warning
	Ok(())
}

#[test]
fn zelda_sheik_transformation() -> Result<(), String> {
	let game = game("transform")?;
	match game.frames {
		Frames::P2(frames) => assert_eq!(frames[400].ports[1].leader.pre.state, State::Zelda(Zelda::TRANSFORM_GROUND)),
		_ => Err("wrong number of ports")?,
	};
	Ok(())
}

#[test]
fn items() -> Result<(), String> {
	let game = game("items")?;
	match game.frames {
		Frames::P2(frames) => {
			let mut items: HashMap<u32, Item> = HashMap::new();
			for f in frames {
				for i in f.items.unwrap() {
					if !items.contains_key(&i.id) {
						items.insert(i.id, i);
					}
				}
			}
			assert_eq!(items[&0], Item {
				id: 0,
				damage: 0,
				direction: Some(Direction::Right),
				position: Position { x: -62.7096061706543, y: -1.4932749271392822 },
				state: item::State(0),
				timer: 140.0,
				r#type: item::Type::PEACH_TURNIP,
				velocity: Velocity { x: 0.0, y: 0.0 },
				misc: Some([5, 5, 5, 5]),
				owner: Some(Some(Port::P1)),
			});
			assert_eq!(items[&1], Item {
				id: 1,
				damage: 0,
				direction: Some(Direction::Left),
				position: Position { x: 20.395559310913086, y: -1.4932749271392822 },
				state: item::State(0),
				timer: 140.0,
				r#type: item::Type::PEACH_TURNIP,
				velocity: Velocity { x: 0.0, y: 0.0 },
				misc: Some([5, 0, 5, 5]),
				owner: Some(Some(Port::P1)),
			});
			assert_eq!(items[&2], Item {
				id: 2,
				damage: 0,
				direction: Some(Direction::Right),
				position: Position { x: -3.982539176940918, y: -1.4932749271392822 },
				state: item::State(0),
				timer: 140.0,
				r#type: item::Type::PEACH_TURNIP,
				velocity: Velocity { x: 0.0, y: 0.0 },
				misc: Some([5, 0, 5, 5]),
				owner: Some(Some(Port::P1)),
			});
		},
		_ => Err("wrong number of ports")?,
	};
	Ok(())
}

#[test]
fn round_trip() -> Result<(), String> {
	let game1 = game("v2.0")?;
	let path = "/tmp/peppi_test_round_trip.slp";
	let mut buf = fs::File::create(path).unwrap();
	serde::ser::serialize(&mut buf, &game1).map_err(|e| format!("couldn't serialize game: {:?}", e))?;
	let game2 = read_game(path)?;

	assert_eq!(game1.start, game2.start);
	assert_eq!(game1.end, game2.end);
	assert_eq!(game1.metadata, game2.metadata);
	assert_eq!(game1.metadata_raw, game2.metadata_raw);

	match (game1.frames, game2.frames) {
		(Frames::P2(f1), Frames::P2(f2)) => {
			assert_eq!(f1.len(), f2.len());
			for idx in 0 .. f1.len() {
				assert_eq!(f1[idx], f2[idx], "frame: {}", idx);
			}
		},
		_ => Err("wrong number of ports")?,
	}

	Ok(())
}