teehistorian_replayer/
lib.rs

1use core::str;
2use log::{debug, info};
3use std::collections::HashMap;
4use teehistorian::{Chunk, ThStream};
5use twgame_core::console::Command;
6use twgame_core::database::{FinishTeam, FinishTee, Finishes};
7use twgame_core::net_msg::NetVersion;
8use twgame_core::replay::{DemoPtr, GameReplayerAll, ReplayerTeeInfo};
9use twgame_core::teehistorian::chunks::ConsoleCommand;
10use twgame_core::twsnap::time::{Duration, Instant};
11use twgame_core::twsnap::Snap;
12use twgame_core::{info_demo, DisplayChunk, Input};
13use vek::Vec2;
14
15/// [TwGame-Core](https://crates.io/crate/twgame-core) crate
16pub use twgame_core;
17/// [Teehistorian](https://crates.io/crate/teehistorian) crate
18pub use twgame_core::teehistorian;
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21enum ThPlayerEvent {
22    /// optional event specifying protocol version for NetMessages
23    JoinVer,
24    Join,
25    // PlayerInfo,
26    // DDNetVersion,
27    // finally joined the game
28    InGame,
29    Drop,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
33struct ThPlayer {
34    event: ThPlayerEvent,
35    version: NetVersion,
36    team: i32,
37    name: String,
38}
39
40impl ThPlayer {
41    fn new(version: NetVersion, event: ThPlayerEvent) -> Self {
42        Self {
43            event,
44            version,
45            team: 0,
46            name: String::new(),
47        }
48    }
49}
50
51struct ThTeam {
52    practice: bool,
53}
54
55impl ThTeam {
56    fn new() -> Self {
57        Self { practice: false }
58    }
59}
60
61pub struct ThReplayer {
62    cur_time: Instant,
63    players: Vec<Option<ThPlayer>>,
64    tees: Vec<Option<ReplayerTeeInfo>>,
65    // count players to generate `Empty` and `NonEmpty` events
66    num_player: u32,
67    teams: HashMap<i32, ThTeam>,
68    // store last input per player not in player struct, since it survives rejoins
69    last_input: Vec<Option<Input>>,
70    // store last player_id of messages PLAYER_DIFF, PLAYER_NEW, PLAYER_OLD to execute implicit ticks
71    implicit_tick_player_id: Option<u32>,
72    // the tick has to get validated after all Player{New,Diff,Old} are parsed and before the new
73    // input from the next tick is passed to the Game world.
74    last_tick_validated: bool,
75    // after a TickSkip, a Load/Save result can return. This has to be passed to the game world,
76    // before the tick is executed. Using this variable to dalay executing the tick until after the
77    // database results
78    delayed_tick: bool,
79    snap: Snap,
80}
81
82impl ThReplayer {
83    pub fn new<T: GameReplayerAll>(header: &[u8], world: &mut T) -> Self {
84        world.on_teehistorian_header(header);
85        let mut teams = HashMap::new();
86        teams.insert(0, ThTeam::new());
87        ThReplayer {
88            cur_time: Instant::zero(),
89            players: vec![],
90            tees: vec![],
91            teams,
92            last_input: vec![],
93            num_player: 0,
94            implicit_tick_player_id: None, // first tick is always implicit
95            last_tick_validated: false,
96            delayed_tick: false,
97            snap: Snap::default(),
98        }
99    }
100
101    pub fn is_empty(&self) -> bool {
102        self.num_player == 0
103    }
104
105    pub fn cur_time(&self) -> Instant {
106        self.cur_time
107    }
108
109    fn player_name(&self, cid: i32) -> &str {
110        assert!(0 <= cid && cid <= self.players.len() as i32);
111        let player = self.players[cid as usize]
112            .as_ref()
113            .expect("PlayerName for non-existing player");
114        player.name.as_str()
115    }
116
117    fn team_names(&self, team: i32) -> Vec<&str> {
118        let mut players = vec![];
119        for player in self.players.iter() {
120            if let Some(player) = player.as_ref() {
121                if player.team == team {
122                    players.push(player.name.as_str())
123                }
124            }
125        }
126        players
127    }
128
129    fn format_console_command(cmd: &ConsoleCommand) -> Option<String> {
130        let mut command = "/".to_owned();
131        command.push_str(std::str::from_utf8(cmd.cmd).ok()?);
132        for arg in cmd.args.iter() {
133            command.push(' ');
134            command.push_str(std::str::from_utf8(arg).ok()?);
135        }
136        Some(command)
137    }
138
139    /// returns false if the validations fails
140    fn maybe_validate_last_tick<T: GameReplayerAll>(
141        &mut self,
142        world: &mut T,
143        mut demo: DemoPtr<T>,
144    ) {
145        if !self.last_tick_validated {
146            self.last_tick_validated = true;
147
148            world.check_tees(self.cur_time, &self.tees, demo.as_mut().map(|d| d.chat()));
149
150            // snap after setting tees to position from teehistorian file
151            if let Some(demo) = demo {
152                demo.snap_and_write(self.cur_time, world, &mut self.snap)
153                    .unwrap();
154            }
155        }
156    }
157
158    fn tick<T: GameReplayerAll>(&mut self, world: &mut T, demo: DemoPtr<T>) {
159        self.maybe_validate_last_tick(world, demo);
160
161        self.cur_time = self.cur_time.advance();
162        world.tick(self.cur_time);
163
164        self.last_tick_validated = false;
165    }
166
167    // returns whether PlayerNew, PlayerOld, PlayerDiff with given cid would result into a implicit
168    // tick
169    fn is_implicid_tick(&self, cid: u32) -> bool {
170        if let Some(implicit_tick_player_id) = self.implicit_tick_player_id {
171            if cid <= implicit_tick_player_id {
172                return true;
173            }
174        }
175        false
176    }
177
178    /// executes implicit tick(), if necessary
179    fn implicit_tick<T: GameReplayerAll>(&mut self, world: &mut T, cid: u32, demo: DemoPtr<T>) {
180        if self.is_implicid_tick(cid) {
181            self.tick(world, demo);
182        }
183        self.implicit_tick_player_id = Some(cid);
184    }
185
186    pub fn validate<T: GameReplayerAll>(
187        mut self,
188        world: &mut T,
189        th: &mut dyn ThStream,
190        mut demo: DemoPtr<T>,
191    ) {
192        if let Some(demo) = &mut demo {
193            demo.write_chat(
194                "Demo created by Teehistorian replayer TwGame https://gitlab.com/ddnet-rs/TwGame",
195            )
196            .unwrap();
197        }
198
199        loop {
200            match th.next_chunk() {
201                Ok(chunk) => {
202                    debug!("{}: {}", self.cur_time, DisplayChunk(&chunk));
203                    self.replay_next_chunk(world, Some(chunk), demo.as_deref_mut());
204                }
205                Err(teehistorian::Error::Eof) => {
206                    // Eos chunk missing?, don't panic. It's fine.
207                    self.replay_next_chunk(world, None, demo.as_deref_mut());
208                    break;
209                }
210                Err(err) => {
211                    info!("teehistorian_chunk_err {err}");
212                    self.replay_next_chunk(world, None, demo.as_deref_mut());
213                    break;
214                }
215            }
216        }
217    }
218
219    fn join_ver(&mut self, cid: i32, net_version: NetVersion) {
220        assert!(0 <= cid);
221        let cid = cid as usize;
222        if cid >= self.players.len() {
223            self.players.resize(cid + 1, None);
224            self.last_input.resize(cid + 1, None);
225        } else if self.players[cid]
226            .as_ref()
227            .map(|p| p.event != ThPlayerEvent::Drop)
228            .unwrap_or(false)
229        {
230            return;
231        }
232
233        self.players[cid] = Some(ThPlayer::new(net_version, ThPlayerEvent::JoinVer));
234    }
235
236    fn join<T: GameReplayerAll>(&mut self, world: &mut T, cid: i32) {
237        self.num_player = self.num_player.checked_add(1).unwrap();
238        // player joined on engine level (via 0.6 if not previously specified via JoinVer6/JoinVer7)
239        assert!(0 <= cid);
240        let cid = cid as usize;
241        if cid >= self.players.len() {
242            self.players.resize(cid + 1, None);
243            self.last_input.resize(cid + 1, None);
244        }
245        if let Some(player) = self.players[cid].as_mut() {
246            // Join must be preceded by JoinVer
247            assert_eq!(player.event, ThPlayerEvent::JoinVer);
248            player.event = ThPlayerEvent::Join;
249        } else {
250            self.players[cid] = Some(ThPlayer::new(NetVersion::Unknown, ThPlayerEvent::Join));
251        }
252        world.player_join(cid as u32);
253    }
254
255    /// Return value contains chunk and no chunk if no further chunks exist.
256    pub fn replay_next_chunk<T: GameReplayerAll>(
257        &mut self,
258        world: &mut T,
259        chunk: Option<Chunk<'_>>,
260        mut demo: DemoPtr<T>,
261    ) {
262        let Some(chunk) = chunk else {
263            self.maybe_validate_last_tick(world, demo);
264            world.finalize();
265            return;
266        };
267
268        if self.delayed_tick
269            && !matches!(
270                chunk,
271                Chunk::TeamLoadSuccess(_)
272                    | Chunk::TeamLoadFailure { team: _ }
273                    | Chunk::TeamSaveSuccess(_)
274                    | Chunk::TeamSaveFailure { team: _ }
275            )
276        {
277            self.delayed_tick = false;
278            self.tick(world, demo.as_deref_mut());
279        }
280
281        if !matches!(
282            chunk,
283            Chunk::PlayerNew(_)
284                | Chunk::PlayerOld { cid: _ }
285                | Chunk::PlayerDiff(_)
286                | Chunk::TeamLoadFailure { team: _ }
287                | Chunk::TeamLoadSuccess(_)
288                | Chunk::TeamSaveFailure { team: _ }
289                | Chunk::TeamSaveSuccess(_)
290                | Chunk::TeamPractice {
291                    team: _,
292                    practice: _
293                }
294                | Chunk::PlayerTeam { cid: _, team: _ }
295                | Chunk::UnknownEx(_)
296                | Chunk::Eos
297        ) {
298            self.maybe_validate_last_tick(world, demo.as_deref_mut());
299        }
300
301        match chunk {
302            Chunk::Eos => {}
303            // getting new input
304            // next player to join joins via 0.6 protocol
305            Chunk::JoinVer6 { cid } => self.join_ver(cid, NetVersion::V06),
306            // next player to join joins via 0.7 vanilla protocol
307            Chunk::JoinVer7 { cid } => self.join_ver(cid, NetVersion::V07),
308            // ignore rejoins
309            Chunk::RejoinVer6 { cid: _ } => {}
310            Chunk::Join { cid } => self.join(world, cid),
311            Chunk::Drop(ref p) => {
312                // player left on engine level
313                assert!(0 <= p.cid && p.cid < self.players.len() as i32);
314                if let Some(player) = self.players[p.cid as usize].as_mut() {
315                    self.num_player = self.num_player.checked_sub(1).unwrap();
316                    // check if only joined players can drop
317                    assert_ne!(player.event, ThPlayerEvent::Drop);
318                    player.event = ThPlayerEvent::Drop;
319                    world.player_leave(p.cid as u32);
320                } else {
321                    //TODO: figure out why I can't panic!("Non-existing player dropped from engine");
322                }
323            }
324
325            Chunk::InputNew(ref inp) => {
326                assert!(0 <= inp.cid && inp.cid < self.players.len() as i32);
327                //assert_eq!(self.last_input[inp.cid as usize], None);
328                let input = Input::from(inp.input);
329                world.player_input(inp.cid as u32, &input);
330                self.last_input[inp.cid as usize] = Some(input);
331            }
332            Chunk::InputDiff(ref inp) => {
333                assert!(0 <= inp.cid && inp.cid < self.players.len() as i32);
334                assert_ne!(self.last_input[inp.cid as usize], None);
335                if let Some(cur_inp) = self.last_input[inp.cid as usize].as_mut() {
336                    cur_inp.add_input_diff(inp.dinput);
337                    world.player_input(inp.cid as u32, cur_inp);
338                }
339            }
340
341            Chunk::NetMessage(ref msg) => {
342                assert!(0 <= msg.cid && msg.cid < self.players.len() as i32);
343                if let Some(p) = self.players[msg.cid as usize].as_mut() {
344                    match twgame_core::net_msg::parse_net_msg(msg.msg, &mut p.version) {
345                        Ok(net_msg) => world.on_net_msg(msg.cid as u32, &net_msg),
346                        Err(err) => info_demo!(
347                            demo.as_deref_mut(),
348                            self.cur_time,
349                            "Error on NetMessage ({})",
350                            err,
351                        ),
352                    }
353                } else {
354                    panic!(
355                        "player_id {}: NetMessage event for non-existing player",
356                        msg.cid
357                    );
358                }
359            }
360            Chunk::ConsoleCommand(ref cmd) => {
361                assert!(-1 <= cmd.cid && cmd.cid < self.players.len() as i32);
362                if cmd.cid == -1 {
363                    // initiated by server, ignore for now.
364                } else {
365                    let cid = cmd.cid as u32;
366
367                    if let Some(command) = Self::format_console_command(cmd) {
368                        if let Some(demo) = demo.as_deref_mut() {
369                            if command.starts_with("/timeout") {
370                                // do not add timeouts to demo
371                            } else if command.starts_with("/save") {
372                                demo.write_player_chat(cmd.cid, "/save **hidden**").unwrap();
373                            } else if command == "/load" {
374                                demo.write_player_chat(cmd.cid, "/load").unwrap();
375                            } else if command.starts_with("/load") {
376                                demo.write_player_chat(cmd.cid, "/load **hidden**").unwrap();
377                            } else {
378                                demo.write_player_chat(cmd.cid, &command).unwrap();
379                            }
380                        }
381                    }
382                    match Command::from_teehistorian(cmd) {
383                        None => {
384                            info_demo!(
385                                demo.as_deref_mut(),
386                                self.cur_time,
387                                "Ignoring console command"
388                            );
389                        }
390                        Some(cmd) => {
391                            // ignore team command in the first three seconds. Hacky workaround to
392                            // tests running for a behavior that might change in future ddnet versions
393                            if matches!(cmd, Command::Team(_))
394                                && !self
395                                    .cur_time
396                                    .duration_passed_since(Instant::zero(), Duration::from_secs(3))
397                            {
398                                // ignore command for now
399                            } else {
400                                world.on_command(cid, &cmd);
401                            }
402                        }
403                    }
404                }
405            }
406
407            // checking input
408            Chunk::PlayerReady { cid } => {
409                assert!(0 <= cid && cid < self.players.len() as i32);
410                if let Some(player) = self.players[cid as usize].as_mut() {
411                    assert_eq!(player.event, ThPlayerEvent::Join);
412                    player.event = ThPlayerEvent::InGame;
413                    world.player_ready(cid as u32);
414                } else {
415                    panic!(
416                        "player_id {}: PlayerReady event for non-existing player",
417                        cid
418                    );
419                }
420            }
421            Chunk::PlayerNew(ref p) => {
422                assert!(0 <= p.cid && p.cid < self.players.len() as i32);
423                self.implicit_tick(world, p.cid as u32, demo.as_deref_mut());
424
425                if self.tees.len() as i32 <= p.cid {
426                    self.tees.resize(p.cid as usize + 1, None);
427                }
428
429                if let Some(player) = self.players[p.cid as usize].as_mut() {
430                    // ThPlayerEvent::Join only possible as long as teehistorian doesn't record player_ready messages+
431                    assert!(matches!(player.event, ThPlayerEvent::InGame));
432                    player.event = ThPlayerEvent::InGame;
433
434                    assert_eq!(self.tees[p.cid as usize], None);
435                    self.tees[p.cid as usize] = Some(ReplayerTeeInfo {
436                        team: player.team,
437                        pos: Vec2::new(p.x, p.y),
438                        practice: self.teams[&player.team].practice,
439                    });
440                } else {
441                    panic!(
442                        "player_id {}: PlayerNew event for non-existing player",
443                        p.cid
444                    );
445                }
446                assert_ne!(self.players[p.cid as usize], None);
447            }
448            Chunk::PlayerDiff(ref p) => {
449                assert!(0 <= p.cid && p.cid < self.players.len() as i32);
450                self.implicit_tick(world, p.cid as u32, demo.as_deref_mut());
451                if let Some(player) = self.players[p.cid as usize].as_mut() {
452                    assert_eq!(player.event, ThPlayerEvent::InGame);
453                    if let Some(tee) = self.tees[p.cid as usize].as_mut() {
454                        tee.pos += Vec2::new(p.dx, p.dy);
455                    } else {
456                        panic!(
457                            "player_id {}: PlayerDiff event before PlayerNew event",
458                            p.cid
459                        );
460                    }
461                } else {
462                    panic!(
463                        "player_id {}: PlayerDiff event for non-existing player",
464                        p.cid
465                    );
466                }
467            }
468            Chunk::PlayerOld { cid } => {
469                assert!(0 <= cid && cid < self.players.len() as i32);
470                self.implicit_tick(world, cid as u32, demo.as_deref_mut());
471
472                if let Some(player) = self.players[cid as usize].as_mut() {
473                    // TODO: figure out why I can't do the following two checks
474                    /*
475                    if player.event != ThPlayerEvent::InGame && player.event != ThPlayerEvent::Drop
476                    {
477                        panic!("player_id {}: PlayerOld event in wrong state found {:?}, expected InGame or Drop", cid, player.event)
478                    }
479                    assert_ne!(self.tees[cid as usize], None); // Tee must exist
480                    */
481                    self.tees[cid as usize] = None;
482                    if player.event == ThPlayerEvent::Drop {
483                        self.players[cid as usize] = None;
484                    }
485                } else {
486                    panic!("player_id {}: PlayerOld event for non-existing player", cid)
487                }
488            }
489
490            Chunk::PlayerTeam { cid, team } => {
491                assert!(0 <= cid && cid <= self.players.len() as i32);
492                if let Some(player) = self.players[cid as usize].as_mut() {
493                    player.team = team;
494                }
495                self.teams.entry(team).or_insert_with(ThTeam::new);
496                if let Some(Some(tee)) = self.tees.get_mut(cid as usize).as_mut() {
497                    tee.team = team;
498                    tee.practice = self.teams[&team].practice;
499                }
500            }
501
502            Chunk::TickSkip { dt } => {
503                // explicits n+1 ticks (calling tick n times and setting delayed_tick to true)
504                assert!(dt >= 0);
505                if self.num_player == 0 {
506                    // don't call tick() and validate() function on empty world + empty teehistorian file
507                    let mut skip_ticks = false;
508                    for i in (1..=dt).rev() {
509                        // skip from the point when there are no entities remaining
510                        skip_ticks = skip_ticks || world.is_empty();
511                        if skip_ticks {
512                            self.cur_time = self.cur_time + Duration::from_ticks(i);
513                            self.last_tick_validated = false;
514                            break;
515                        } else {
516                            self.tick(world, demo.as_deref_mut());
517                        }
518                    }
519                } else {
520                    for _ in 0..dt {
521                        self.tick(world, demo.as_deref_mut());
522                    }
523                }
524                self.implicit_tick_player_id = None;
525                self.delayed_tick = true;
526            }
527            Chunk::PlayerSwap { cid1, cid2 } => {
528                let id1: u32 = cid1.try_into().unwrap();
529                let id2: u32 = cid2.try_into().unwrap();
530                world.swap_tees(id1, id2);
531            }
532
533            Chunk::TeamPractice { team, practice } => {
534                let practice = practice != 0;
535                self.teams.get_mut(&team).unwrap().practice = practice;
536                for tee in self.tees.iter_mut().flatten() {
537                    if tee.team == team {
538                        tee.practice = practice;
539                    }
540                }
541            }
542            Chunk::PlayerName(ref p) => {
543                assert!(0 <= p.cid && p.cid <= self.players.len() as i32);
544                let player = self.players[p.cid as usize]
545                    .as_mut()
546                    .expect("PlayerName for non-existing player");
547                player.name = str::from_utf8(p.name).unwrap().to_owned();
548            }
549            Chunk::PlayerFinish { cid, time } => {
550                let finish = Finishes::FinishTee(FinishTee {
551                    name: self.player_name(cid).to_owned(),
552                    time: Duration::from_ticks(time),
553                });
554                world.on_finish(self.cur_time, &finish);
555            }
556            Chunk::TeamFinish { team, time } => {
557                let finish = Finishes::FinishTeam(FinishTeam {
558                    team,
559                    names: self
560                        .team_names(team)
561                        .iter()
562                        .map(|&s| s.to_owned())
563                        .collect(),
564                    time: Duration::from_ticks(time),
565                });
566                world.on_finish(self.cur_time, &finish);
567            }
568            // TBD
569            _ => {}
570        }
571        world.on_teehistorian_chunk(self.cur_time, &chunk);
572    }
573}