twsnap 0.7.4

Common snapshot format between TwGpu and TwGame
Documentation
use crate::enums as twenums;
use crate::time;
use crate::SnapId;
use crate::{Events, SkinColor, Snap};
use bitflags::Flags;
use libtw2_gamenet_ddnet::enums as libenums;
use libtw2_gamenet_ddnet::snap_obj::Tick;
use libtw2_gamenet_ddnet::{snap_obj, SnapObj};
use std::iter;
use vek::Vec2;

fn i32_string<const N: usize>(s: &str) -> [i32; N] {
    let padding_size = N * 4 - s.len() - 1;
    let mut data = s
        .bytes()
        .map(|b| b.wrapping_add(128))
        .chain(iter::repeat(128).take(padding_size))
        .chain([0]);
    let mut nums = [0; N];
    for num in &mut nums {
        *num = i32::from_be_bytes([
            data.next().unwrap(),
            data.next().unwrap(),
            data.next().unwrap(),
            data.next().unwrap(),
        ]);
    }
    assert_eq!(data.next(), None);
    nums
}

fn u64_flags_lower<T: Flags<Bits = u64>>(flags: T) -> i32 {
    (flags.bits() & 0xffff_ffff) as u32 as i32
}

fn u64_flags_upper<T: Flags<Bits = u64>>(flags: T) -> i32 {
    ((flags.bits() >> 32) & 0xffff_ffff) as u32 as i32
}

fn tick(t: time::Instant) -> Tick {
    Tick(t.snap_tick())
}

/// Temporary function until https://github.com/heinrich5991/libtw2/issues/93 is resolved
fn duration_to_tick(t: time::Duration) -> Tick {
    Tick(t.ticks())
}

fn fix<T: fixed::traits::Fixed<Bits = i32>>(x: T) -> i32 {
    x.to_bits()
}

fn cmn<T: fixed::traits::Fixed<Bits = i32>>(v: Vec2<T>) -> snap_obj::Common {
    snap_obj::Common {
        x: fix(v.x),
        y: fix(v.y),
    }
}

fn skin_color(c: SkinColor) -> i32 {
    i32::from_le_bytes([c.h, c.s, c.l_upper, c.a])
}

impl Snap {
    pub fn to_ddnet(&self, snap: &mut Vec<(SnapObj, u16)>) -> [u8; 64] {
        snap.clear();

        for (id, item) in self.game_infos.iter() {
            snap.push((
                SnapObj::GameInfo(snap_obj::GameInfo {
                    game_flags: item.flags.bits(),
                    game_state_flags: item.game_state_flags.bits(),
                    round_start_tick: tick(item.round_start_tick),
                    warmup_timer: item.warmup_timer.ticks(),
                    score_limit: item.score_limit,
                    time_limit: item.time_limit.ticks(),
                    round_num: item.round_num,
                    round_current: item.round_current,
                }),
                id.ddnet_snap_id(),
            ));
            snap.push((
                SnapObj::GameInfoEx(snap_obj::GameInfoEx {
                    flags: u64_flags_lower(item.flags_ex),
                    version: 7,
                    flags2: u64_flags_upper(item.flags_ex),
                }),
                id.ddnet_snap_id(),
            ))
        }

        for (id, item) in self.lasers.iter() {
            let type_ = match item.kind {
                twenums::LaserType::Rifle => libenums::Lasertype::Rifle,
                twenums::LaserType::Shotgun => libenums::Lasertype::Shotgun,
                twenums::LaserType::Door => libenums::Lasertype::Door,
                twenums::LaserType::Freeze => libenums::Lasertype::Freeze,
                twenums::LaserType::Dragger(_) => libenums::Lasertype::Dragger,
                twenums::LaserType::Gun(_) => libenums::Lasertype::Gun,
                twenums::LaserType::Plasma => libenums::Lasertype::Plasma,
            }
            .to_i32();
            let subtype = match item.kind {
                twenums::LaserType::Dragger(sub) => sub.into(),
                twenums::LaserType::Gun(sub) => sub.into(),
                _ => 0,
            };
            snap.push((
                SnapObj::DdnetLaser(snap_obj::DdnetLaser {
                    to_x: fix(item.to.x),
                    to_y: fix(item.to.y),
                    from_x: fix(item.from.x),
                    from_y: fix(item.from.y),
                    start_tick: tick(item.start_tick),
                    owner: SnapId::snap_opt_to_id(item.owner),
                    type_,
                    switch_number: item.switch_number as i32,
                    subtype,
                }),
                id.ddnet_snap_id(),
            ))
        }

        for (id, item) in self.map_projectiles.iter() {
            let type_ = libenums::Weapon::from_i32(item.kind.into()).unwrap();
            snap.push((
                SnapObj::DdnetProjectile(snap_obj::DdnetProjectile {
                    x: fix(item.pos.x),
                    y: fix(item.pos.y),
                    vel_x: item.direction.x,
                    vel_y: item.direction.y,
                    type_,
                    start_tick: tick(item.start_tick),
                    owner: -1,
                    switch_number: item.switch_number as i32,
                    tune_zone: item.tune_zone as i32,
                    flags: item.flags.bits(),
                }),
                id.ddnet_snap_id(),
            ));
        }

        for (id, item) in self.pickups.iter() {
            use twenums::CollectableWeapon::*;
            let type_ = match item.kind {
                twenums::Powerup::Health => libenums::Powerup::Health,
                twenums::Powerup::Armor => libenums::Powerup::Armor,
                twenums::Powerup::Weapon(_) => libenums::Powerup::Weapon,
                twenums::Powerup::Shield(Shotgun) => libenums::Powerup::ArmorShotgun,
                twenums::Powerup::Shield(Grenade) => libenums::Powerup::ArmorGrenade,
                twenums::Powerup::Shield(Rifle) => libenums::Powerup::ArmorLaser,
                twenums::Powerup::Shield(Ninja) => libenums::Powerup::ArmorNinja,
            }
            .to_i32();
            let subtype = match item.kind {
                twenums::Powerup::Weapon(weapon) => weapon.into(),
                _ => 0,
            };
            snap.push((
                SnapObj::DdnetPickup(snap_obj::DdnetPickup {
                    x: fix(item.pos.x),
                    y: fix(item.pos.y),
                    type_,
                    subtype,
                    switch_number: item.switch_number as i32,
                }),
                id.ddnet_snap_id(),
            ));
        }

        let mut teams = [0u8; 64];
        for (id, item) in self.players.iter() {
            let id = id.ddnet_snap_id();
            teams[id as usize] = item.team.try_into().unwrap();
            snap.push((
                SnapObj::PlayerInfo(snap_obj::PlayerInfo {
                    local: item.local as i32,
                    client_id: id as i32,
                    team: libenums::Team::from_i32(item.teeworlds_team.into()).unwrap(),
                    score: item.score,
                    latency: item.latency,
                }),
                id,
            ));
            snap.push((
                SnapObj::ClientInfo(snap_obj::ClientInfo {
                    name: i32_string(&item.name),
                    clan: i32_string(&item.clan),
                    country: item.country,
                    skin: i32_string(&item.skin),
                    use_custom_color: item.use_custom_color as i32,
                    color_body: skin_color(item.color_body),
                    color_feet: skin_color(item.color_feet),
                }),
                id,
            ));
            snap.push((
                SnapObj::DdnetPlayer(snap_obj::DdnetPlayer {
                    flags: item.flags.bits(),
                    auth_level: item.auth_level.into(),
                }),
                id,
            ));
            let tee = match &item.tee {
                None => continue,
                Some(tee) => tee,
            };
            snap.push((
                SnapObj::Character(snap_obj::Character {
                    character_core: snap_obj::CharacterCore {
                        tick: tee.tick.snap_tick(),
                        x: fix(tee.pos.x),
                        y: fix(tee.pos.y),
                        vel_x: fix(tee.vel.x),
                        vel_y: fix(tee.vel.y),
                        angle: fix(tee.angle),
                        direction: tee.direction.into(),
                        jumped: tee.jumped.bits(),
                        hooked_player: SnapId::snap_opt_to_id(tee.hooked_player),
                        hook_state: tee.hook_state.into(),
                        hook_tick: duration_to_tick(tee.hook_tick),
                        hook_x: fix(tee.hook_pos.x),
                        hook_y: fix(tee.hook_pos.y),
                        hook_dx: fix(tee.hook_direction.x),
                        hook_dy: fix(tee.hook_direction.y),
                    },
                    player_flags: u64_flags_upper(tee.flags),
                    health: tee.health,
                    armor: tee.armor,
                    ammo_count: tee.ammo_count,
                    weapon: libenums::Weapon::from_i32(tee.weapon.into()).unwrap(),
                    emote: libenums::Emote::from_i32(tee.emote.into()).unwrap(),
                    attack_tick: tee.attack_tick.snap_tick(),
                }),
                id,
            ));
            snap.push((
                SnapObj::DdnetCharacter(snap_obj::DdnetCharacter {
                    flags: u64_flags_lower(tee.flags),
                    freeze_end: tick(tee.freeze_end),
                    jumps: tee.jumps,
                    tele_checkpoint: tee.tele_checkpoint,
                    strong_weak_id: tee.strong_weak_id,
                    jumped_total: tee.jumped_total,
                    ninja_activation_tick: tick(tee.ninja_activation_tick),
                    freeze_start: tick(tee.freeze_start),
                    target_x: fix(tee.target.x),
                    target_y: fix(tee.target.y),
                }),
                id,
            ));
        }

        for (id, item) in self.projectiles.iter() {
            snap.push((
                SnapObj::DdnetProjectile(snap_obj::DdnetProjectile {
                    x: fix(item.pos.x) * 100,
                    y: fix(item.pos.y) * 100,
                    vel_x: item.direction.x,
                    vel_y: item.direction.y,
                    type_: libenums::Weapon::from_i32(item.kind.into()).unwrap(),
                    start_tick: tick(item.start_tick),
                    owner: item.owner.to_id(),
                    switch_number: 0, // TODO
                    tune_zone: item.tune_zone as i32,
                    flags: 1 << 4, // NORMALIZE_VEL
                }),
                id.ddnet_snap_id(),
            ))
        }

        //for (item, id) in self.pvp_flags.sort_id_iter() {} // TODO

        let mut damage_indicator_id = 0;
        let mut death_id = 0;
        let mut explosion_id = 0;
        let mut hammer_hit_id = 0;
        let mut sound_id = 0;
        let mut sound_global_id = 0;
        let mut spawn_id = 0;
        for event in &self.events {
            match event {
                Events::DamageIndicator(event) => {
                    snap.push((
                        SnapObj::DamageInd(snap_obj::DamageInd {
                            common: cmn(event.pos),
                            angle: fix(event.angle),
                        }),
                        damage_indicator_id,
                    ));
                    damage_indicator_id += 1;
                }
                Events::Death(event) => {
                    snap.push((
                        SnapObj::Death(snap_obj::Death {
                            common: cmn(event.pos),
                            client_id: event.player.to_id(),
                        }),
                        death_id,
                    ));
                    death_id += 1;
                }
                Events::Explosion(event) => {
                    snap.push((
                        SnapObj::Explosion(snap_obj::Explosion {
                            common: cmn(event.pos),
                        }),
                        explosion_id,
                    ));
                    explosion_id += 1;
                }
                Events::HammerHit(event) => {
                    snap.push((
                        SnapObj::HammerHit(snap_obj::HammerHit {
                            common: cmn(event.pos),
                        }),
                        hammer_hit_id,
                    ));
                    hammer_hit_id += 1;
                }
                Events::Sound(event) => {
                    snap.push((
                        SnapObj::SoundWorld(snap_obj::SoundWorld {
                            common: cmn(event.pos),
                            sound_id: libenums::Sound::from_i32(event.sound.into()).unwrap(),
                        }),
                        sound_id,
                    ));
                    sound_id += 1;
                }
                Events::SoundGlobal(event) => {
                    snap.push((
                        SnapObj::SoundGlobal(snap_obj::SoundGlobal {
                            common: snap_obj::Common { x: 0, y: 0 },
                            sound_id: libenums::Sound::from_i32(event.sound.into()).unwrap(),
                        }),
                        sound_global_id,
                    ));
                    sound_global_id += 1;
                }
                Events::Spawn(event) => {
                    snap.push((
                        SnapObj::Spawn(snap_obj::Spawn {
                            common: cmn(event.pos),
                        }),
                        spawn_id,
                    ));
                    spawn_id += 1;
                }
            }
        }
        teams
    }
}