use std::{collections::HashMap, io::Cursor}; use flate2::{Compression, bufread::GzDecoder, write::GzEncoder}; use godot::{ classes::{FileAccess, file_access::ModeFlags}, prelude::*, tools::get_autoload_by_name, }; use serde::{Deserialize, Serialize}; use crate::{ game_manager::{Game, GameManager, Player}, map::Map, net::{network_manager::BallSync, util::NetVector3}, pickup::{PickupKind, ReplayVisibility}, player::{ DamageSource, IPlayer, NetTransform, ball::Ball, remote_player::RemotePlayer, weapon::WeaponType, }, ui::hud::Hud, }; pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal"; #[derive(GodotClass)] #[class(base=Node, init)] pub struct StandaloneReplayManager { #[export] replay_finished_screen: Option>, #[export] main_menu: Option>, #[export] hud_scene: Option>, loading_replay: bool, map: Option>, pub playback: Option>, replay: Option, hud: Option>, base: Base, } #[godot_api] impl StandaloneReplayManager { #[signal] pub fn on_map_loaded(map: Gd); pub fn singleton() -> Gd { get_autoload_by_name::(STANDALONE_REPLAY_GLOBAL) } pub fn open_replay(&mut self, path: &GString) { if let Some(mut file) = FileAccess::open(path, ModeFlags::READ) { let mut bytes = Vec::new(); while !file.eof_reached() { bytes.extend(file.get_buffer(1_000_000).to_vec()); } let cursor = Cursor::new(&mut bytes); let decoder = GzDecoder::new(cursor); if let Ok(res) = ciborium::from_reader::(decoder) { self.loading_replay = true; let map_idx = res.map_idx; self.replay = Some(res); if let Some(map) = GameManager::singleton().bind().maps.get(map_idx as usize) { if let Some(scene) = &map.bind().scene { self.base().get_tree().change_scene_to_packed(scene); } } } } } pub fn on_map_loaded(&mut self, map: Gd) { if self.loading_replay { self.loading_replay = false; self.map = Some(map.clone()); if let Some(replay) = self.replay.take() { let mut playback = ReplayPlayback::new_alloc(); playback .signals() .on_finished() .connect_other(self, Self::on_done); playback.bind_mut().replay = replay; playback.bind_mut().current_map = Some(map.clone()); playback.bind_mut().current_time = 0.; playback.bind_mut().fast_forwarding = false; playback.bind_mut().spectated_player_id = 0; self.playback = Some(playback.clone()); self.base_mut().add_child(&playback); if let Some(scene) = &self.hud_scene { let hud = scene.instantiate_as::(); hud.signals().on_exit_to_lobby().connect_other(self, |s| { s.on_done(); }); hud.signals().on_exit().connect_other(self, |s| { s.clear(); s.go_to_menu(); }); self.hud = Some(hud.clone()); playback.add_child(&hud); } } } self.signals().on_map_loaded().emit(&map); } pub fn on_done(&mut self) { if let Some(scene) = &self.replay_finished_screen { self.base().get_tree().change_scene_to_packed(scene); } } pub fn clear(&mut self) { self.replay.take(); self.map.take(); if let Some(mut hud) = self.hud.take() { hud.queue_free(); } if let Some(mut playback) = self.playback.take() { playback.queue_free(); } } pub fn go_to_menu(&mut self) { if let Some(scene) = &self.main_menu { self.base().get_tree().change_scene_to_packed(scene); } } } #[derive(GodotClass)] #[class(base=Node, init)] pub struct ReplayPlayback { pub replay: Replay, pub player_data: HashMap, pub current_map: Option>, current_frame_idx: usize, player_characters: HashMap>, ball: Option>, pub current_time: f64, pub spectated_player_id: u16, pub fast_forwarding: bool, pub finished: bool, pub running_goals: HashMap, base: Base, } #[godot_api] impl ReplayPlayback { #[signal] pub fn on_finished(); #[signal] pub fn on_data_changed(); } #[godot_api] impl INode for ReplayPlayback { fn ready(&mut self) { self.finished = false; } fn process(&mut self, _: f64) { if let Some(character) = self.player_characters.get_mut(&self.spectated_player_id) { if let Some(camera) = &mut character.bind_mut().spectator_camera { if !camera.is_current() { camera.make_current(); } } } } fn physics_process(&mut self, delta: f64) { self.current_time += delta; while let Some(frame) = self.replay.frames.get(self.current_frame_idx).cloned() && frame.time <= self.current_time { self.player_data = frame.player_data.clone(); self.running_goals = frame.goals.clone(); if let Some(map) = &mut self.current_map { for (id, visible) in frame.pickups_visible { if let Some(pickup) = map.bind_mut().pickups.get_mut(&id) { pickup.dyn_bind_mut().set_replay(match visible { true => ReplayVisibility::Visible, false => ReplayVisibility::Hidden, }); } } } if let ReplayBall::World(ball_state) = &frame.ball_data { if let Some(ball) = &mut self.ball { ball.bind_mut().apply_sync(BallSync { transform: ball_state.transform, velocity: ball_state.velocity, }); } else if let Some(map) = &mut self.current_map { if let Some(ball) = map.bind_mut().spawn_replay_ball( ball_state.transform.into(), ball_state.velocity.into(), None, ) { self.ball = Some(ball); } } } else { if let Some(mut ball) = self.ball.take() { ball.queue_free(); } } if !self.fast_forwarding { if let Some(map) = &mut self.current_map { for (id, data) in &frame.player_data { if !self.player_characters.contains_key(id) && !data.is_dead && let Some(player) = self.replay.players.get(id) { if let Some(player) = map.bind_mut() .spawn_replay_player(*id, player, data.clone()) { self.player_characters.insert(*id, player); } } } } for event in &frame.events { match event { ReplayEvent::Death(damage_source, player_id) => { if let Some(character) = self.player_characters.get_mut(player_id) { character.bind_mut().kill(*damage_source); } } ReplayEvent::Shoot(player_id, to) => { if let Some(character) = self.player_characters.get_mut(player_id) { character.bind_mut().try_shoot((*to).into(), false); } } ReplayEvent::Respawn(player_id, data) => { if let Some(mut character) = self.player_characters.remove(player_id) { character.queue_free(); } if let Some(map) = &mut self.current_map && let Some(player) = self.replay.players.get(player_id) { if let Some(player) = map.bind_mut().spawn_replay_player( *player_id, player, data.clone(), ) { self.player_characters.insert(*player_id, player); } } } ReplayEvent::Jump(player_id) => { if let Some(character) = self.player_characters.get_mut(player_id) { character.bind_mut().jump(); } } ReplayEvent::Punch(player_id, to) => { if let Some(character) = self.player_characters.get_mut(player_id) { character.bind_mut().try_punch((*to).into(), false); } } ReplayEvent::TakeDamage(player_id, damage_source, damage) => { if let Some(character) = self.player_characters.get_mut(player_id) { character .bind_mut() .take_damage(*damage_source, *damage, false); } } ReplayEvent::Pickup(kind, player_id) => { if let Some(character) = self.player_characters.get_mut(player_id) { character.bind_mut().on_pickup(*kind); } } } } } for (id, mut character) in self.player_characters.clone() { if let Some(state) = frame.player_data.get(&id) { character.bind_mut().apply_transform(state.transform); character.bind_mut().apply_look_up(state.look_up); character .bind_mut() .set_movement_dir(state.movement_dir.into()); character.bind_mut().set_hp(state.health); let weapon = character.bind().get_weapon().clone(); if let Some(weapon) = weapon && weapon.dyn_bind().get_type() != state.current_weapon { character .bind_mut() .swap_weapon(state.current_weapon, false); } } else { character.queue_free(); self.player_characters.remove(&id); } } self.current_frame_idx += 1; self.signals().on_data_changed().emit(); } self.fast_forwarding = false; if !self.finished && self.current_frame_idx >= self.replay.frames.len() { self.finished = true; for (id, mut character) in self.player_characters.clone() { character.queue_free(); self.player_characters.remove(&id); } if let Some(mut ball) = self.ball.take() { ball.queue_free(); } self.signals().on_finished().emit(); } } fn exit_tree(&mut self) { if let Some(map) = &mut self.current_map { for (_, pickup) in &mut map.bind_mut().pickups { pickup.dyn_bind_mut().set_replay(ReplayVisibility::NoReplay); } } for (_, character) in &mut self.player_characters { character.queue_free(); } self.player_characters.clear(); if let Some(mut ball) = self.ball.take() { ball.queue_free(); } } } #[derive(GodotClass)] #[class(base=Node, init)] pub struct ReplayRecorder { pub time_elapsed: f64, pub replay: Replay, current_frame: ReplayFrame, recording: bool, base: Base, } #[godot_api] impl INode for ReplayRecorder { fn ready(&mut self) { self.recording = true; self.run_deferred(|s| { s.current_frame = s.record_frame(); if let Some(game) = &Game::singleton() { for player in &game.bind().players { if let Some(team) = game.bind().get_team(player.data.team) { s.replay.players.insert( player.id(), ReplayPlayer { id: player.id(), name: player.data.name.clone(), color: team.bind().color.into(), team: player.data.team, kills: 0, deaths: 0, }, ); } } s.replay.map_idx = game.bind().selected_map_idx; s.replay.teams = game .bind() .get_teams() .iter_shared() .map(|t| ReplayTeam { name: t.bind().name.to_string(), color: t.bind().color.into(), }) .collect(); } }); } fn physics_process(&mut self, delta: f64) { if !self.recording { return; } self.time_elapsed += delta; if self.time_elapsed > (self.current_frame.time + (1. / 60.)) { self.replay.frames.push(self.current_frame.clone()); self.current_frame = self.record_frame(); } } } impl ReplayRecorder { pub fn save_to(&self, path: &GString) { if let Some(mut file) = FileAccess::open(path, ModeFlags::WRITE) { let mut bytes = Vec::new(); let encoder = GzEncoder::new(&mut bytes, Compression::default()); ciborium::into_writer(&self.replay, encoder).ok(); file.store_buffer(&PackedArray::from_iter(bytes)); } } pub fn record_event(&mut self, event: ReplayEvent) { self.current_frame.events.push(event); } pub fn stop_recording(&mut self) { self.replay.frames.push(self.current_frame.clone()); self.recording = false; } fn record_frame(&mut self) -> ReplayFrame { let mut player_data = HashMap::new(); let mut ball_data = ReplayBall::Held; let mut goals = HashMap::new(); let mut pickups_visible = HashMap::new(); if let Some(game) = Game::singleton() { for player in &game.bind().players { if let Some(state) = player.get_state(player.clone()) { player_data.insert(player.id(), state); } if let Some(replay_player) = self.replay.players.get_mut(&player.id()) { replay_player.kills = player.data.kills; replay_player.deaths = player.data.deaths; } } ball_data = if let Some(ball) = &game.bind().world_ball { ReplayBall::World(ReplayBallData { transform: ball.bind().get_transform(), velocity: ball.bind().get_velocity(), }) } else { ReplayBall::Held }; if let Some(map) = &game.bind().current_map { for (id, pickup) in &map.bind().pickups { pickups_visible.insert(*id, pickup.dyn_bind().is_active()); } } goals = game.bind().goals.clone(); } self.replay.goals = goals.clone(); ReplayFrame { time: self.time_elapsed, goals, pickups_visible, player_data, ball_data, events: Vec::new(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Replay { pub map_idx: u8, pub players: HashMap, pub teams: Vec, pub goals: HashMap, frames: Vec, } impl Default for Replay { fn default() -> Self { Self { map_idx: Default::default(), players: Default::default(), goals: Default::default(), teams: Default::default(), frames: Default::default(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplayTeam { pub name: String, pub color: ReplayColor, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplayPlayer { pub id: u16, pub name: String, pub color: ReplayColor, pub team: u8, pub kills: u16, pub deaths: u16, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplayColor { r: f32, g: f32, b: f32, a: f32, } impl From for ReplayColor { fn from(value: Color) -> Self { ReplayColor { r: value.r, g: value.g, b: value.b, a: value.a, } } } impl From for Color { fn from(value: ReplayColor) -> Self { Color { r: value.r, g: value.g, b: value.b, a: value.a, } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ReplayFrame { time: f64, player_data: HashMap, ball_data: ReplayBall, pickups_visible: HashMap, events: Vec, goals: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ReplayEvent { Death(DamageSource, u16), Shoot(u16, NetVector3), Respawn(u16, ReplayPlayerState), Jump(u16), Punch(u16, NetVector3), TakeDamage(u16, DamageSource, i32), Pickup(PickupKind, u16), } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub enum ReplayBall { World(ReplayBallData), #[default] Held, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplayPlayerState { pub transform: NetTransform, pub movement_dir: NetVector3, pub look_up: f32, pub current_weapon: WeaponType, pub is_dead: bool, pub kills: u16, pub deaths: u16, pub health: i32, } impl Player { pub fn get_state(&self, player: Player) -> Option { if let Some(character) = &self.character { Some(ReplayPlayerState { transform: character.dyn_bind().get_transform(), movement_dir: character.dyn_bind().get_movement_dir().into(), look_up: character.dyn_bind().get_look_up(), current_weapon: character .dyn_bind() .get_weapon() .map(|w| w.dyn_bind().get_type()) .unwrap_or(WeaponType::Raygun), is_dead: character.dyn_bind().is_dead(), kills: player.data.kills, deaths: player.data.deaths, health: character.dyn_bind().get_health(), }) } else { None } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplayBallData { transform: NetTransform, velocity: NetVector3, }