use std::fmt::{self, Debug, Display, Formatter};
use base64::prelude::{BASE64_STANDARD, Engine};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::{Map, Value};
use crate::{
frame::{PortOccupancy, transpose},
game::shift_jis::MeleeString,
io::slippi::{self, Version},
};
pub mod immutable;
pub mod mutable;
pub mod shift_jis;
pub const NUM_PORTS: usize = 4;
pub const MAX_PLAYERS: usize = 6;
pub const ICE_CLIMBERS: u8 = 14;
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
IntoPrimitive,
TryFromPrimitive,
)]
pub enum Port {
P1 = 0,
P2 = 1,
P3 = 2,
P4 = 3,
}
impl Port {
pub fn parse(s: &str) -> Result<Self, String> {
match s {
"P1" => Ok(Port::P1),
"P2" => Ok(Port::P2),
"P3" => Ok(Port::P3),
"P4" => Ok(Port::P4),
_ => Err(format!("invalid port: {}", s)),
}
}
}
impl Display for Port {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Port::*;
match *self {
P1 => write!(f, "P1"),
P2 => write!(f, "P2"),
P3 => write!(f, "P3"),
P4 => write!(f, "P4"),
}
}
}
impl Default for Port {
fn default() -> Self {
Self::P1
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TryFromPrimitive)]
pub enum PlayerType {
Human = 0,
Cpu = 1,
Demo = 2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Team {
pub color: u8,
pub shade: u8,
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TryFromPrimitive)]
pub enum DashBack {
Ucf = 1,
Arduino = 2,
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TryFromPrimitive)]
pub enum ShieldDrop {
Ucf = 1,
Arduino = 2,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TryFromPrimitive)]
pub enum Language {
Japanese = 0,
English = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ucf {
pub dash_back: Option<DashBack>,
pub shield_drop: Option<ShieldDrop>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Netplay {
pub name: MeleeString,
pub code: MeleeString,
#[serde(skip_serializing_if = "Option::is_none")]
pub suid: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Player {
pub port: Port,
pub character: u8,
pub r#type: PlayerType,
pub stocks: u8,
pub costume: u8,
pub team: Option<Team>,
pub handicap: u8,
pub bitfield: u8,
pub cpu_level: Option<u8>,
pub damage_start: u16,
pub damage_spawn: u16,
pub offense_ratio: f32,
pub defense_ratio: f32,
pub model_scale: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub ucf: Option<Ucf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_tag: Option<MeleeString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub netplay: Option<Netplay>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Scene {
pub minor: u8,
pub major: u8,
}
#[derive(PartialEq, Eq, Clone, Default)]
pub struct Bytes(pub Vec<u8>);
impl Serialize for Bytes {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&BASE64_STANDARD.encode(&self.0))
}
}
struct BytesVisitor;
impl<'de> de::Visitor<'de> for BytesVisitor {
type Value = Bytes;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a base64-encoded string")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(Bytes(
BASE64_STANDARD
.decode(value)
.map_err(|_| E::custom("invalid base64"))?,
))
}
}
impl<'de> Deserialize<'de> for Bytes {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_string(BytesVisitor)
}
}
impl Debug for Bytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Bytes {{ len: {} }}", self.0.len())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Match {
pub id: String,
pub game: u32,
pub tiebreaker: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Start {
pub slippi: slippi::Slippi,
pub bitfield: [u8; 4],
pub is_raining_bombs: bool,
pub is_teams: bool,
pub item_spawn_frequency: i8,
pub self_destruct_score: i8,
pub stage: u16,
pub timer: u32,
pub item_spawn_bitfield: [u8; 5],
pub damage_ratio: f32,
pub players: Vec<Player>,
pub random_seed: u32,
pub bytes: Bytes,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_pal: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_frozen_ps: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scene: Option<Scene>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<Language>,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#match: Option<Match>,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TryFromPrimitive)]
pub enum EndMethod {
Unresolved = 0,
Time = 1,
Game = 2,
Resolved = 3,
NoContest = 7,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlayerEnd {
pub port: Port,
pub placement: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct End {
pub method: EndMethod,
pub bytes: Bytes,
#[serde(skip_serializing_if = "Option::is_none")]
pub lras_initiator: Option<Option<Port>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub players: Option<Vec<PlayerEnd>>,
}
impl End {
pub(crate) fn size(version: Version) -> usize {
if version.gte(3, 13) {
6
} else if version.gte(2, 0) {
2
} else {
1
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct GeckoCodes {
pub bytes: Vec<u8>,
pub actual_size: u32,
}
pub trait Game {
fn start(&self) -> &Start;
fn end(&self) -> &Option<End>;
fn metadata(&self) -> &Option<Map<String, Value>>;
fn gecko_codes(&self) -> &Option<GeckoCodes>;
fn len(&self) -> usize;
fn frame(&self, idx: usize) -> transpose::Frame;
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct Quirks {
pub double_game_end: bool,
}
pub fn port_occupancy(start: &Start) -> Vec<PortOccupancy> {
start
.players
.iter()
.map(|p| PortOccupancy {
port: p.port,
follower: p.character == ICE_CLIMBERS,
})
.collect()
}