792 lines
26 KiB
Rust
792 lines
26 KiB
Rust
use std::{collections::HashMap, fs, io::Cursor, thread};
|
|
|
|
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::{Game, GameManager, Player},
|
|
game_settings::GameSettingsManager,
|
|
map::Map,
|
|
net::{network_manager::BallSync, util::NetVector3},
|
|
pickup::{PickupKind, ReplayVisibility},
|
|
player::{
|
|
DamageSource, IPlayer, NetTransform, ball::Ball, buffs::Buffs, remote_player::RemotePlayer,
|
|
telegrenade::Telegrenade, weapon::WeaponType,
|
|
},
|
|
ui::{hud::Hud, replay_hud::ReplayHud},
|
|
};
|
|
|
|
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>>,
|
|
#[export]
|
|
replay_hud_scene: Option<Gd<PackedScene>>,
|
|
|
|
end_time: f64,
|
|
loading_replay: bool,
|
|
map: Option<Gd<Map>>,
|
|
pub playback: Option<Gd<ReplayPlayback>>,
|
|
replay: Option<Replay>,
|
|
hud: Option<Gd<Hud>>,
|
|
replay_hud: Option<Gd<ReplayHud>>,
|
|
since_last_update: f64,
|
|
|
|
base: Base<Node>,
|
|
}
|
|
|
|
#[godot_api]
|
|
impl INode for StandaloneReplayManager {
|
|
fn process(&mut self, delta: f64) {
|
|
self.since_last_update += delta;
|
|
if self.since_last_update >= 1.
|
|
&& let Some(hud) = &mut self.replay_hud
|
|
&& let Some(playback) = &self.playback
|
|
{
|
|
self.since_last_update = 0.;
|
|
hud.bind_mut()
|
|
.update_times(playback.bind().current_time, self.end_time, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
|
|
self.end_time = replay.find_ending();
|
|
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 mut 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();
|
|
});
|
|
|
|
hud.signals().on_next_spectator().connect_other(self, |s| {
|
|
if let Some(playback) = &mut s.playback {
|
|
playback.bind_mut().next_spectator();
|
|
}
|
|
});
|
|
|
|
hud.signals().on_prev_spectator().connect_other(self, |s| {
|
|
if let Some(playback) = &mut s.playback {
|
|
playback.bind_mut().prev_spectator();
|
|
}
|
|
});
|
|
|
|
self.hud = Some(hud.clone());
|
|
playback.add_child(&hud);
|
|
|
|
if let Some(scene) = &self.replay_hud_scene {
|
|
let mut replay_hud = scene.instantiate_as::<ReplayHud>();
|
|
|
|
replay_hud.bind_mut().update_times(0., self.end_time, true);
|
|
|
|
replay_hud
|
|
.signals()
|
|
.on_time_changed()
|
|
.connect_other(self, |s, new_time| s.fast_forward_to(new_time));
|
|
|
|
self.replay_hud = Some(replay_hud.clone());
|
|
hud.add_child(&replay_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 hud) = self.replay_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);
|
|
}
|
|
}
|
|
|
|
pub fn fast_forward_to(&mut self, new_time: f64) {
|
|
self.run_deferred(move |s| {
|
|
if let Some(playback) = &mut s.playback {
|
|
playback.bind_mut().current_time = new_time;
|
|
playback.bind_mut().current_frame_idx = 0;
|
|
playback.bind_mut().fast_forwarding = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
#[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>>,
|
|
telegrenades: HashMap<u16, Gd<Telegrenade>>,
|
|
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);
|
|
}
|
|
}
|
|
ReplayEvent::TelegrenadeSpawn(player_id, location, velocity) => {
|
|
if let Some(prev_grenade) = self.telegrenades.get_mut(player_id) {
|
|
prev_grenade.queue_free();
|
|
}
|
|
self.telegrenades.remove(player_id);
|
|
|
|
if let Some(map) = &mut self.current_map {
|
|
if let Some(telegrenade) = map.bind_mut().spawn_replay_telegrenade(
|
|
(*location).into(),
|
|
(*velocity).into(),
|
|
) {
|
|
self.telegrenades.insert(*player_id, telegrenade);
|
|
}
|
|
}
|
|
}
|
|
ReplayEvent::TelegrenadeDespawn(player_id) => {
|
|
if let Some(prev_grenade) = self.telegrenades.get_mut(player_id) {
|
|
prev_grenade.queue_free();
|
|
}
|
|
self.telegrenades.remove(player_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
character.bind_mut().set_buffs(state.buffs.clone());
|
|
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();
|
|
for (_, telegrenade) in &mut self.telegrenades {
|
|
telegrenade.queue_free();
|
|
}
|
|
self.telegrenades.clear();
|
|
if let Some(mut ball) = self.ball.take() {
|
|
ball.queue_free();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ReplayPlayback {
|
|
pub fn next_spectator(&mut self) {
|
|
let mut ids = self.player_characters.keys().collect::<Vec<_>>();
|
|
ids.sort();
|
|
if let Some((current_idx, _)) = ids
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_, id)| ***id == self.spectated_player_id)
|
|
{
|
|
let idx = (current_idx + 1) % ids.len();
|
|
if let Some(id) = ids.get(idx) {
|
|
self.spectated_player_id = **id;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn prev_spectator(&mut self) {
|
|
let mut ids = self.player_characters.keys().collect::<Vec<_>>();
|
|
ids.sort();
|
|
if let Some((current_idx, _)) = ids
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_, id)| ***id == self.spectated_player_id)
|
|
{
|
|
let mut idx = current_idx as i32 - 1;
|
|
if idx == -1 {
|
|
idx = ids.len() as i32 - 1;
|
|
}
|
|
let idx = (idx % ids.len() as i32) as usize;
|
|
if let Some(id) = ids.get(idx) {
|
|
self.spectated_player_id = **id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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;
|
|
|
|
let fps = GameSettingsManager::singleton().bind().settings.replay_fps;
|
|
|
|
if self.time_elapsed > (self.current_frame.time + (1. / fps as f64)) {
|
|
self.replay.frames.push(self.current_frame.clone());
|
|
self.current_frame = self.record_frame();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ReplayRecorder {
|
|
pub fn save_to(&self, path: &GString) {
|
|
let path = path.to_string();
|
|
let replay = self.replay.clone();
|
|
thread::spawn(move || {
|
|
let mut bytes = Vec::new();
|
|
let encoder = GzEncoder::new(&mut bytes, Compression::default());
|
|
ciborium::into_writer(&replay, encoder).ok();
|
|
fs::write(path, bytes).ok();
|
|
});
|
|
}
|
|
|
|
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<_>>(),
|
|
}
|
|
}
|
|
|
|
pub fn find_ending(&self) -> f64 {
|
|
if let Some(frame) = self.frames.last() {
|
|
frame.time
|
|
} else {
|
|
0.
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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),
|
|
TelegrenadeSpawn(u16, NetVector3, NetVector3),
|
|
TelegrenadeDespawn(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,
|
|
pub buffs: Buffs,
|
|
}
|
|
|
|
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(),
|
|
buffs: character.dyn_bind().get_buffs().clone(),
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ReplayBallData {
|
|
transform: NetTransform,
|
|
velocity: NetVector3,
|
|
}
|