skullball/rust/src/replay.rs
2026-07-27 23:54:35 +03:00

652 lines
21 KiB
Rust

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<Gd<PackedScene>>,
#[export]
main_menu: Option<Gd<PackedScene>>,
#[export]
hud_scene: Option<Gd<PackedScene>>,
loading_replay: bool,
map: Option<Gd<Map>>,
pub playback: Option<Gd<ReplayPlayback>>,
replay: Option<Replay>,
hud: Option<Gd<Hud>>,
base: Base<Node>,
}
#[godot_api]
impl StandaloneReplayManager {
#[signal]
pub fn on_map_loaded(map: Gd<Map>);
pub fn singleton() -> Gd<StandaloneReplayManager> {
get_autoload_by_name::<StandaloneReplayManager>(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::<Replay, _>(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<Map>) {
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>();
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<u16, ReplayPlayerState>,
pub current_map: Option<Gd<Map>>,
current_frame_idx: usize,
player_characters: HashMap<u16, Gd<RemotePlayer>>,
ball: Option<Gd<Ball>>,
pub current_time: f64,
pub spectated_player_id: u16,
pub fast_forwarding: bool,
pub finished: bool,
pub running_goals: HashMap<u8, u16>,
base: Base<Node>,
}
#[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<Node>,
}
#[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<u16, ReplayPlayer>,
pub teams: Vec<ReplayTeam>,
pub goals: HashMap<u8, u16>,
frames: Vec<ReplayFrame>,
}
impl Default for Replay {
fn default() -> Self {
Self {
map_idx: Default::default(),
players: Default::default(),
goals: Default::default(),
teams: Default::default(),
frames: Default::default(),
}
}
}
impl Replay {
pub fn find_frame(&self, elapsed: f64) -> usize {
self.frames
.iter()
.enumerate()
.skip_while(|(_, f)| f.time < elapsed)
.next()
.map(|(i, _)| i)
.unwrap_or(0)
}
pub fn clone_since(&self, frame_idx: usize) -> Replay {
Replay {
map_idx: self.map_idx,
players: self.players.clone(),
teams: self.teams.clone(),
goals: self.goals.clone(),
frames: self
.frames
.iter()
.skip(frame_idx)
.cloned()
.collect::<Vec<_>>(),
}
}
}
#[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<Color> for ReplayColor {
fn from(value: Color) -> Self {
ReplayColor {
r: value.r,
g: value.g,
b: value.b,
a: value.a,
}
}
}
impl From<ReplayColor> 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<u16, ReplayPlayerState>,
ball_data: ReplayBall,
pickups_visible: HashMap<u8, bool>,
events: Vec<ReplayEvent>,
goals: HashMap<u8, u16>,
}
#[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<ReplayPlayerState> {
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,
}