From 8c0d5b99ff41a922e58613b6b224d51b1cf23356 Mon Sep 17 00:00:00 2001 From: Sofia Date: Sun, 26 Jul 2026 21:54:07 +0300 Subject: [PATCH] Add playing replays from file --- .gitignore | 3 +- godot/project.godot | 1 + godot/scenes/main.tscn | 14 ++++- .../scenes/misc/standalone_replay_global.tscn | 3 + rust/src/map.rs | 6 +- rust/src/replay.rs | 63 ++++++++++++++++++- rust/src/ui/main_menu.rs | 31 ++++++++- 7 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 godot/scenes/misc/standalone_replay_global.tscn diff --git a/.gitignore b/.gitignore index 508a3a6..a21886c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ godot/maps/entity_models rust/target target -dist \ No newline at end of file +dist +*.replay \ No newline at end of file diff --git a/godot/project.godot b/godot/project.godot index 3ab3985..b6e48fc 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -23,6 +23,7 @@ GameManagerGlobal="*uid://bii6018k3ip3t" CliParserGlobal="*uid://c52o7xa0fnr53" GameSettingsGlobal="*uid://bjr15hjiojecw" AnnouncerGlobal="*uid://5gey4i80mmsa" +StandaloneReplayGlobal="*uid://djmb2jqcpkuep" [display] diff --git a/godot/scenes/main.tscn b/godot/scenes/main.tscn index 1a7a83e..c939377 100644 --- a/godot/scenes/main.tscn +++ b/godot/scenes/main.tscn @@ -4,7 +4,7 @@ [ext_resource type="PackedScene" uid="uid://dsxu0y8pjbw3k" path="res://scenes/ui/credits.tscn" id="1_sugp2"] [ext_resource type="PackedScene" uid="uid://n3u0jr4wiib1" path="res://scenes/ui/settings.tscn" id="2_0wfyh"] -[node name="menu" type="MainMenu" unique_id=2036763876 node_paths=PackedStringArray("camera", "main_position", "host_position", "main_panel", "host_panel", "join_panel", "host_port", "join_addr", "recent_servers_button", "popup_panel", "popup_title", "popup_text", "popup_button")] +[node name="menu" type="MainMenu" unique_id=2036763876 node_paths=PackedStringArray("camera", "main_position", "host_position", "main_panel", "host_panel", "join_panel", "host_port", "join_addr", "recent_servers_button", "popup_panel", "popup_title", "popup_text", "popup_button", "open_replay_dialog")] camera = NodePath("camera_3d") main_position = NodePath("main_pos") host_position = NodePath("host_pos") @@ -19,6 +19,7 @@ popup_title = NodePath("popup_panel/v_box_container/title") popup_text = NodePath("popup_panel/v_box_container/text") popup_button = NodePath("popup_panel/v_box_container/ok") credits = ExtResource("1_sugp2") +open_replay_dialog = NodePath("open_replay_dialog") [node name="camera_3d" type="Camera3D" parent="." unique_id=1513321931] transform = Transform3D(0.98629165, -0.031186381, 0.16203775, -1.4054481e-08, 0.981978, 0.1889952, -0.16501158, -0.18640438, 0.9685167, 1.0974052, 1.7166122, 1.4909106) @@ -74,6 +75,11 @@ layout_mode = 2 text = "Join Game" metadata/_edit_lock_ = true +[node name="play_replay" type="Button" parent="main_panel/v_box_container" unique_id=275552647] +layout_mode = 2 +text = "Play Replay" +metadata/_edit_lock_ = true + [node name="credits" type="Button" parent="main_panel/v_box_container" unique_id=953921422] layout_mode = 2 text = "Credits" @@ -245,8 +251,14 @@ autowrap_mode = 3 layout_mode = 2 text = "Ok" +[node name="open_replay_dialog" type="FileDialog" parent="." unique_id=925465558] +title = "Open a File" +file_mode = 0 +filters = PackedStringArray("*.replay") + [connection signal="pressed" from="main_panel/v_box_container/host" to="." method="go_to_host_game"] [connection signal="pressed" from="main_panel/v_box_container/join" to="." method="go_to_join_game"] +[connection signal="pressed" from="main_panel/v_box_container/play_replay" to="." method="open_replay_dialog"] [connection signal="pressed" from="main_panel/v_box_container/credits" to="." method="open_credits"] [connection signal="pressed" from="main_panel/v_box_container/settings" to="settings" method="open"] [connection signal="pressed" from="main_panel/v_box_container/quit" to="." method="quit_game"] diff --git a/godot/scenes/misc/standalone_replay_global.tscn b/godot/scenes/misc/standalone_replay_global.tscn new file mode 100644 index 0000000..1fbcbab --- /dev/null +++ b/godot/scenes/misc/standalone_replay_global.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://djmb2jqcpkuep"] + +[node name="standalone_replay_global" type="StandaloneReplayManager" unique_id=730657724] diff --git a/rust/src/map.rs b/rust/src/map.rs index 34792a6..75dbcb4 100644 --- a/rust/src/map.rs +++ b/rust/src/map.rs @@ -6,7 +6,7 @@ use crate::{ player::{ IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, }, - replay::{ReplayPlayer, ReplayPlayerState}, + replay::{ReplayPlayer, ReplayPlayerState, StandaloneReplayManager}, }; #[derive(GodotClass)] @@ -25,6 +25,10 @@ impl INode3D for Map { fn ready(&mut self) { if let Some(game) = &mut Game::singleton() { game.bind_mut().on_map_loaded(self.to_gd()); + } else { + StandaloneReplayManager::singleton() + .bind_mut() + .on_map_loaded(self.to_gd()); } } } diff --git a/rust/src/replay.rs b/rust/src/replay.rs index 4dc794b..c17e021 100644 --- a/rust/src/replay.rs +++ b/rust/src/replay.rs @@ -3,11 +3,12 @@ use std::{collections::HashMap, io::Cursor}; use godot::{ classes::{FileAccess, file_access::ModeFlags}, prelude::*, + tools::get_autoload_by_name, }; use serde::{Deserialize, Serialize}; use crate::{ - game_manager::{Game, Player}, + game_manager::{Game, GameManager, Player}, map::Map, net::{network_manager::BallSync, util::NetVector3}, player::{ @@ -16,6 +17,64 @@ use crate::{ }, }; +pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal"; + +#[derive(GodotClass)] +#[class(base=Node, init)] +pub struct StandaloneReplayManager { + loading_replay: bool, + map: Option>, + playback: Option>, + replay: Option, + + base: Base, +} + +impl StandaloneReplayManager { + 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.push(file.get_8()); + } + + let bytes = Cursor::new(&mut bytes); + if let Ok(res) = ciborium::from_reader::(bytes) { + 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.bind_mut().replay = replay; + playback.bind_mut().current_map = Some(map); + 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); + } + } + } +} + #[derive(GodotClass)] #[class(base=Node, init)] pub struct ReplayPlayback { @@ -292,7 +351,7 @@ impl ReplayRecorder { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Replay { - map_idx: u8, + pub map_idx: u8, players: HashMap, frames: Vec, } diff --git a/rust/src/ui/main_menu.rs b/rust/src/ui/main_menu.rs index b77878c..105b143 100644 --- a/rust/src/ui/main_menu.rs +++ b/rust/src/ui/main_menu.rs @@ -1,13 +1,20 @@ +use std::io::Cursor; + use godot::{ - classes::{Button, Camera3D, Label, LineEdit, OptionButton, Panel, SpinBox}, + classes::{ + Button, Camera3D, FileAccess, FileDialog, Label, LineEdit, OptionButton, Panel, SpinBox, + file_access::ModeFlags, + }, prelude::*, register::property::SimpleVar, }; use crate::{ + game_manager::GameManager, game_settings::{GameSettingsManager, RecentServer}, net::network_manager::NetworkManager, popup_queue::{Popup, PopupQueue}, + replay::{Replay, StandaloneReplayManager}, ui::credits::CreditsMenu, }; @@ -61,6 +68,9 @@ pub struct MainMenu { #[export] credits: Option>, + #[export] + open_replay_dialog: Option>, + prev_panel: Option>, recent_servers: Vec, @@ -87,6 +97,12 @@ impl INode3D for MainMenu { .item_selected() .connect_other(self, |s, idx| s.on_recent_server_selected(idx)); } + if let Some(dialog) = &self.open_replay_dialog { + dialog + .signals() + .file_selected() + .connect_other(self, Self::on_open_replay); + } self.hide_popup(); } @@ -162,6 +178,19 @@ impl INode3D for MainMenu { #[godot_api] impl MainMenu { + #[func] + pub fn open_replay_dialog(&mut self) { + if let Some(dialog) = &mut self.open_replay_dialog { + dialog.popup_file_dialog(); + } + } + + pub fn on_open_replay(&mut self, path: GString) { + StandaloneReplayManager::singleton() + .bind_mut() + .open_replay(&path); + } + #[func] pub fn host_game(&mut self) { if let Some(host_port) = &self.host_port {