Add playing replays from file

This commit is contained in:
Sofia 2026-07-26 21:54:07 +03:00
parent 5687adbf8a
commit 8c0d5b99ff
7 changed files with 115 additions and 6 deletions

3
.gitignore vendored
View File

@ -11,4 +11,5 @@ godot/maps/entity_models
rust/target rust/target
target target
dist dist
*.replay

View File

@ -23,6 +23,7 @@ GameManagerGlobal="*uid://bii6018k3ip3t"
CliParserGlobal="*uid://c52o7xa0fnr53" CliParserGlobal="*uid://c52o7xa0fnr53"
GameSettingsGlobal="*uid://bjr15hjiojecw" GameSettingsGlobal="*uid://bjr15hjiojecw"
AnnouncerGlobal="*uid://5gey4i80mmsa" AnnouncerGlobal="*uid://5gey4i80mmsa"
StandaloneReplayGlobal="*uid://djmb2jqcpkuep"
[display] [display]

View File

@ -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://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"] [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") camera = NodePath("camera_3d")
main_position = NodePath("main_pos") main_position = NodePath("main_pos")
host_position = NodePath("host_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_text = NodePath("popup_panel/v_box_container/text")
popup_button = NodePath("popup_panel/v_box_container/ok") popup_button = NodePath("popup_panel/v_box_container/ok")
credits = ExtResource("1_sugp2") credits = ExtResource("1_sugp2")
open_replay_dialog = NodePath("open_replay_dialog")
[node name="camera_3d" type="Camera3D" parent="." unique_id=1513321931] [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) 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" text = "Join Game"
metadata/_edit_lock_ = true 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] [node name="credits" type="Button" parent="main_panel/v_box_container" unique_id=953921422]
layout_mode = 2 layout_mode = 2
text = "Credits" text = "Credits"
@ -245,8 +251,14 @@ autowrap_mode = 3
layout_mode = 2 layout_mode = 2
text = "Ok" 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/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/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/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/settings" to="settings" method="open"]
[connection signal="pressed" from="main_panel/v_box_container/quit" to="." method="quit_game"] [connection signal="pressed" from="main_panel/v_box_container/quit" to="." method="quit_game"]

View File

@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://djmb2jqcpkuep"]
[node name="standalone_replay_global" type="StandaloneReplayManager" unique_id=730657724]

View File

@ -6,7 +6,7 @@ use crate::{
player::{ player::{
IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer,
}, },
replay::{ReplayPlayer, ReplayPlayerState}, replay::{ReplayPlayer, ReplayPlayerState, StandaloneReplayManager},
}; };
#[derive(GodotClass)] #[derive(GodotClass)]
@ -25,6 +25,10 @@ impl INode3D for Map {
fn ready(&mut self) { fn ready(&mut self) {
if let Some(game) = &mut Game::singleton() { if let Some(game) = &mut Game::singleton() {
game.bind_mut().on_map_loaded(self.to_gd()); game.bind_mut().on_map_loaded(self.to_gd());
} else {
StandaloneReplayManager::singleton()
.bind_mut()
.on_map_loaded(self.to_gd());
} }
} }
} }

View File

@ -3,11 +3,12 @@ use std::{collections::HashMap, io::Cursor};
use godot::{ use godot::{
classes::{FileAccess, file_access::ModeFlags}, classes::{FileAccess, file_access::ModeFlags},
prelude::*, prelude::*,
tools::get_autoload_by_name,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
game_manager::{Game, Player}, game_manager::{Game, GameManager, Player},
map::Map, map::Map,
net::{network_manager::BallSync, util::NetVector3}, net::{network_manager::BallSync, util::NetVector3},
player::{ 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<Gd<Map>>,
playback: Option<Gd<ReplayPlayback>>,
replay: Option<Replay>,
base: Base<Node>,
}
impl StandaloneReplayManager {
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.push(file.get_8());
}
let bytes = Cursor::new(&mut bytes);
if let Ok(res) = ciborium::from_reader::<Replay, _>(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<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.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)] #[derive(GodotClass)]
#[class(base=Node, init)] #[class(base=Node, init)]
pub struct ReplayPlayback { pub struct ReplayPlayback {
@ -292,7 +351,7 @@ impl ReplayRecorder {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Replay { pub struct Replay {
map_idx: u8, pub map_idx: u8,
players: HashMap<u16, ReplayPlayer>, players: HashMap<u16, ReplayPlayer>,
frames: Vec<ReplayFrame>, frames: Vec<ReplayFrame>,
} }

View File

@ -1,13 +1,20 @@
use std::io::Cursor;
use godot::{ use godot::{
classes::{Button, Camera3D, Label, LineEdit, OptionButton, Panel, SpinBox}, classes::{
Button, Camera3D, FileAccess, FileDialog, Label, LineEdit, OptionButton, Panel, SpinBox,
file_access::ModeFlags,
},
prelude::*, prelude::*,
register::property::SimpleVar, register::property::SimpleVar,
}; };
use crate::{ use crate::{
game_manager::GameManager,
game_settings::{GameSettingsManager, RecentServer}, game_settings::{GameSettingsManager, RecentServer},
net::network_manager::NetworkManager, net::network_manager::NetworkManager,
popup_queue::{Popup, PopupQueue}, popup_queue::{Popup, PopupQueue},
replay::{Replay, StandaloneReplayManager},
ui::credits::CreditsMenu, ui::credits::CreditsMenu,
}; };
@ -61,6 +68,9 @@ pub struct MainMenu {
#[export] #[export]
credits: Option<Gd<PackedScene>>, credits: Option<Gd<PackedScene>>,
#[export]
open_replay_dialog: Option<Gd<FileDialog>>,
prev_panel: Option<Gd<Panel>>, prev_panel: Option<Gd<Panel>>,
recent_servers: Vec<RecentServer>, recent_servers: Vec<RecentServer>,
@ -87,6 +97,12 @@ impl INode3D for MainMenu {
.item_selected() .item_selected()
.connect_other(self, |s, idx| s.on_recent_server_selected(idx)); .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(); self.hide_popup();
} }
@ -162,6 +178,19 @@ impl INode3D for MainMenu {
#[godot_api] #[godot_api]
impl MainMenu { 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] #[func]
pub fn host_game(&mut self) { pub fn host_game(&mut self) {
if let Some(host_port) = &self.host_port { if let Some(host_port) = &self.host_port {