twgame 0.11.0

DDNet physics implementation
Documentation
use crate::entities::tee::Tee;
use crate::entities::SpawnableEntity;
use crate::ids::{PlayerUid, TeamId};
use crate::state::globals::Globals;
use crate::state::Tees;
use crate::state::{EntityList, PrivilegedGameState};
use arrayvec::ArrayString;
use slotmap::SecondaryMap;
use twgame_core::net_msg::{ClPlayerInfo, Skin};
use twgame_core::twsnap;
use twgame_core::twsnap::time::{Duration, Instant};
use twgame_core::twsnap::{SkinColor, Snap};
use twgame_core::Input;

#[derive(Debug)]
pub struct Players {
    // TODO: make non-public (it's currently for convenience, but doesn't guaranty consistency,
    //       because Team can just modify either id_order or players without the other
    pub(super) players: SecondaryMap<PlayerUid, Player>,
}

impl Players {
    pub(super) fn new() -> Self {
        Self {
            players: Default::default(),
        }
    }

    pub(super) fn player_join(&mut self, now: Instant, player_uid: PlayerUid) {
        self.players
            .insert(player_uid, Player::new(now, player_uid));
    }

    pub(super) fn player_join_from_spectator(&mut self, player_uid: PlayerUid, mut player: Player) {
        player.mark_spawn_at_end_of_tick(SpawnMode::Normal);
        self.players.insert(player_uid, player);
    }

    pub(super) fn player_join_from_other_team(&mut self, player_uid: PlayerUid, player: Player) {
        self.players.insert(player_uid, player);
    }

    pub(super) fn player_leave(&mut self, player_uid: PlayerUid) -> Option<Player> {
        self.players.remove(player_uid)
    }

    pub(crate) fn get(&self, player_uid: PlayerUid) -> Option<&Player> {
        self.players.get(player_uid)
    }

    pub(super) fn get_mut(&mut self, player_uid: PlayerUid) -> Option<&mut Player> {
        self.players.get_mut(player_uid)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpawnMode {
    Normal,
    /// spawn with weak hook on players who spawn in the same tick
    /// relevant for team restarts in locked teams, or load/save
    ForceWeak,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PauseMode {
    /// `/pause` command
    Pause,
    //       /// `/spec` command (with `sv_pauseable` enabled)
    // TODO: Spec,
}

#[derive(Clone, Debug)]
pub struct Player {
    pub(crate) name: ArrayString<15>,
    pub(crate) clan: ArrayString<11>,
    pub(crate) country: i32,
    pub(crate) skin: ArrayString<23>,
    pub(crate) use_custom_color: bool,
    pub(crate) color_body: SkinColor,
    pub(crate) color_feet: SkinColor,

    player_uid: PlayerUid,
    pub(super) tee: bool,
    pub(super) input: Input,

    /// Stores whether CServer::ClientIngame returns true,
    /// in this implementation player_ready was called
    pub(super) in_game: bool,

    /// store whether player is in spectator mode (m_Team == TEAM_SPECTATORS in DDNet source code)
    pub(super) spectator: bool,

    /// stores active pause mode. None if not paused
    pub(super) pause: Option<PauseMode>,

    /// spawns in next tick()
    pub(super) spawning: Option<SpawnMode>,

    /// respawning
    pub(super) die_tick: Instant,
    pub(super) previous_die_tick: Instant,
}

// TODO: use TwSnap variant or don't use ddnet snapshot as input anymore
fn skin_color(c: i32) -> SkinColor {
    let [h, s, l_upper, a] = c.to_le_bytes();
    SkinColor { h, s, l_upper, a }
}

// functions for physics
impl Player {
    pub(super) fn new(now: Instant, player_uid: PlayerUid) -> Player {
        Player {
            name: ArrayString::new(),
            clan: ArrayString::new(),
            country: 0,
            skin: ArrayString::new(),
            use_custom_color: false,
            color_body: SkinColor::default(),
            color_feet: SkinColor::default(),
            player_uid,
            tee: false,
            input: Input::new(),
            in_game: false,
            spectator: false,
            pause: None,
            spawning: None,

            die_tick: now,
            previous_die_tick: now,
        }
    }

    pub(super) fn should_spawn(&self, spawn_mode: SpawnMode) -> bool {
        self.spawning == Some(spawn_mode) && !self.spectator
    }

    pub(super) fn spawned(&mut self) {
        self.tee = true;
        self.spawning = None;
    }

    pub(super) fn on_player_info(&mut self, player_info: &ClPlayerInfo) {
        // TODO: check for retry intervals
        // TODO: use Arc/Rc instead?
        self.name = ArrayString::from(String::from_utf8_lossy(player_info.name).as_ref())
            .unwrap_or_default();
        self.clan = ArrayString::from(String::from_utf8_lossy(player_info.clan).as_ref())
            .unwrap_or_default();
        self.country = player_info.country;
        // TODO: put 0.6/0.7 skin into TwSnap
        match &player_info.skin {
            Skin::V6(skin) => {
                self.skin = ArrayString::from(String::from_utf8_lossy(skin.skin).as_ref())
                    .unwrap_or_default();
                self.use_custom_color = skin.use_custom_color;
                self.color_body = skin_color(skin.color_body);
                self.color_feet = skin_color(skin.color_feet);
            }
            Skin::V7(_) => {
                // TODO: convert 0.7 skins to 0.6 somehow
            }
        }
    }

    pub(super) fn on_input(&mut self) {
        if !self.tee && !self.spectator && self.input.fire & 1 != 0 {
            self.mark_spawn_at_end_of_tick(SpawnMode::Normal)
        }
        // TODO: ...
    }

    /// returns whether to kill remaining tees in team, because team was locked
    pub(super) fn kill_tee(
        &mut self,
        now: Instant,
        tees: &mut Tees<Tee>,
        spawn_mode: SpawnMode,
    ) -> bool {
        if !self.tee {
            return false;
        };
        let Some(tee) = tees.get_tee_mut(self.player_uid) else {
            return false;
        };
        if tee.is_marked_for_destroy() {
            return false;
        }

        tee.mark_for_destroy();
        self.mark_spawn_at_end_of_tick(spawn_mode);
        self.previous_die_tick = self.die_tick;
        self.die_tick = now;
        true
    }

    pub(super) fn switch_team(
        &mut self,
        from_team: &mut PrivilegedGameState,
        to_team: &mut PrivilegedGameState,
        from_entities: &mut EntityList,
        to_entities: Option<&mut EntityList>,
    ) {
        if self.tee {
            from_team.move_player(to_team, self.player_uid, from_entities, to_entities);
        }
    }

    pub(super) fn snap(&self, team_id: TeamId, player_uid: PlayerUid, snap: &mut Snap) {
        let player = snap.players.get_mut_default(player_uid.snap_id());
        // todo: same type
        player.team = team_id.to_i32();
        // PlayerInfo
        player.local = false;
        player.teeworlds_team = twsnap::enums::ClientTeam::Red;
        player.score = 0;
        player.latency = 0;
        // ClientInfo
        player.name = self.name;
        player.clan = self.clan;
        player.country = self.country;
        player.skin = self.skin;
        player.use_custom_color = self.use_custom_color;
        player.color_body = self.color_body;
        player.color_feet = self.color_feet;
    }
}

// functions concerning the teehistorian validator
impl Player {
    /// mark the player to spawn at the end of the tick
    pub(super) fn mark_spawn_at_end_of_tick(&mut self, spawn_mode: SpawnMode) {
        // TODO: check if spawn is called when player is in spectator mode
        if !self.spectator {
            self.spawning = Some(spawn_mode);
        }
    }

    /// called CPlayer::PostTick in DDNet
    pub(super) fn tick(&mut self, now: Instant, tees: &Tees<Tee>, globals: &Globals) {
        if !self.in_game {
            return; // don't process tick for not-ingame player
        }
        if self.tee {
            let tee = tees.get_tee(self.player_uid);
            // TODO: is_marked_for_destroyed check unnecessary, only .is_none() necessary?
            if tee.map(|tee| tee.is_marked_for_destroy()).unwrap_or(true) {
                self.tee = false;
                self.previous_die_tick = self.die_tick;
                self.die_tick = now;
                if globals.kill_propagates() && self.spawning.is_none() {
                    self.spawning = Some(SpawnMode::ForceWeak)
                }
            } else {
                // TODO: ProcessPause (/spec and /pause chat commands)
                // tee.process_pause();
            }
        } else if self.spawning.is_none() {
            // rate limit automatic respawning to 3 seconds
            let earliest_respawn_tick = self.previous_die_tick + Duration::from_secs(3);
            let respawn_tick = earliest_respawn_tick.max(self.die_tick) + Duration::T40MS;

            if respawn_tick <= now {
                self.mark_spawn_at_end_of_tick(SpawnMode::Normal)
            }
        }
    }
}