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
15pub use twgame_core;
17pub use twgame_core::teehistorian;
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21enum ThPlayerEvent {
22 JoinVer,
24 Join,
25 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 num_player: u32,
67 teams: HashMap<i32, ThTeam>,
68 last_input: Vec<Option<Input>>,
70 implicit_tick_player_id: Option<u32>,
72 last_tick_validated: bool,
75 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, 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 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 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 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 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 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 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 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 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 Chunk::JoinVer6 { cid } => self.join_ver(cid, NetVersion::V06),
306 Chunk::JoinVer7 { cid } => self.join_ver(cid, NetVersion::V07),
308 Chunk::RejoinVer6 { cid: _ } => {}
310 Chunk::Join { cid } => self.join(world, cid),
311 Chunk::Drop(ref p) => {
312 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 assert_ne!(player.event, ThPlayerEvent::Drop);
318 player.event = ThPlayerEvent::Drop;
319 world.player_leave(p.cid as u32);
320 } else {
321 }
323 }
324
325 Chunk::InputNew(ref inp) => {
326 assert!(0 <= inp.cid && inp.cid < self.players.len() as i32);
327 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 } 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 } 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 if matches!(cmd, Command::Team(_))
394 && !self
395 .cur_time
396 .duration_passed_since(Instant::zero(), Duration::from_secs(3))
397 {
398 } else {
400 world.on_command(cid, &cmd);
401 }
402 }
403 }
404 }
405 }
406
407 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 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 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 assert!(dt >= 0);
505 if self.num_player == 0 {
506 let mut skip_ticks = false;
508 for i in (1..=dt).rev() {
509 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 _ => {}
570 }
571 world.on_teehistorian_chunk(self.cur_time, &chunk);
572 }
573}