Store replay to file

This commit is contained in:
Sofia 2026-07-26 21:30:57 +03:00
parent b3ecbfccd8
commit 5687adbf8a
7 changed files with 118 additions and 20 deletions

1
Cargo.lock generated
View File

@ -200,6 +200,7 @@ dependencies = [
name = "quakeball" name = "quakeball"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ciborium",
"godot", "godot",
"serde", "serde",
"teanet", "teanet",

View File

@ -2,9 +2,11 @@
[ext_resource type="PackedScene" uid="uid://2sfy0vq0fqt3" path="res://scenes/ui/player_listing.tscn" id="1_ou354"] [ext_resource type="PackedScene" uid="uid://2sfy0vq0fqt3" path="res://scenes/ui/player_listing.tscn" id="1_ou354"]
[node name="game_finished" type="GameFinishedScreen" unique_id=700430082 node_paths=PackedStringArray("title_container", "back_button")] [node name="game_finished" type="GameFinishedScreen" unique_id=700430082 node_paths=PackedStringArray("title_container", "back_button", "player_listings", "save_file_dialog")]
title_container = NodePath("title_container") title_container = NodePath("title_container")
back_button = NodePath("button") back_button = NodePath("button")
player_listings = NodePath("player_listings")
save_file_dialog = NodePath("save_dialog")
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
@ -39,6 +41,22 @@ grow_vertical = 0
theme_override_font_sizes/font_size = 32 theme_override_font_sizes/font_size = 32
text = "Back" text = "Back"
[node name="save_replay" type="Button" parent="." unique_id=1199584793]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -534.0
offset_top = -87.0
offset_right = -342.0
offset_bottom = -34.0
grow_horizontal = 2
grow_vertical = 0
theme_override_font_sizes/font_size = 32
text = "Save Replay"
[node name="player_listings" type="PlayerListings" parent="." unique_id=602626392] [node name="player_listings" type="PlayerListings" parent="." unique_id=602626392]
player_listing_scene = ExtResource("1_ou354") player_listing_scene = ExtResource("1_ou354")
layout_mode = 1 layout_mode = 1
@ -62,3 +80,8 @@ offset_top = 93.0
offset_right = 20.0 offset_right = 20.0
offset_bottom = 133.0 offset_bottom = 133.0
grow_horizontal = 2 grow_horizontal = 2
[node name="save_dialog" type="FileDialog" parent="." unique_id=1637714468]
filters = PackedStringArray("*.replay")
[connection signal="pressed" from="save_replay" to="." method="open_file_dialog"]

View File

@ -7,6 +7,7 @@ edition = "2024"
crate-type = ["cdylib"] # Compile this crate to a dynamic C library. crate-type = ["cdylib"] # Compile this crate to a dynamic C library.
[dependencies] [dependencies]
ciborium = "0.2.2"
godot = "0.5.4" godot = "0.5.4"
serde = "1.0.228" serde = "1.0.228"
teanet = { path = "../teanet-rust" } teanet = { path = "../teanet-rust" }

View File

@ -1008,7 +1008,7 @@ impl Game {
if let Some(recorder) = &mut self.replay_recorder { if let Some(recorder) = &mut self.replay_recorder {
recorder recorder
.bind_mut() .bind_mut()
.record_event(ReplayEvent::Shoot(player_id, to)); .record_event(ReplayEvent::Shoot(player_id, to.into()));
} }
let self_id = self.self_id.clone().unwrap_or(0); let self_id = self.self_id.clone().unwrap_or(0);
@ -1035,7 +1035,7 @@ impl Game {
if let Some(recorder) = &mut self.replay_recorder { if let Some(recorder) = &mut self.replay_recorder {
recorder recorder
.bind_mut() .bind_mut()
.record_event(ReplayEvent::Punch(player_id, to)); .record_event(ReplayEvent::Punch(player_id, to.into()));
} }
let self_id = self.self_id.clone().unwrap_or(0); let self_id = self.self_id.clone().unwrap_or(0);

View File

@ -112,7 +112,7 @@ impl Map {
let mut node: Gd<RemotePlayer> = replay_player.instantiate_as(); let mut node: Gd<RemotePlayer> = replay_player.instantiate_as();
node.bind_mut().player_id = Some(player_id); node.bind_mut().player_id = Some(player_id);
node.bind_mut().player_name = player.name.clone(); node.bind_mut().player_name = player.name.clone();
node.bind_mut().player_color = player.color; node.bind_mut().player_color = player.color.clone().into();
self.base_mut().add_child(&node); self.base_mut().add_child(&node);
node.bind_mut() node.bind_mut()

View File

@ -1,6 +1,10 @@
use std::collections::HashMap; use std::{collections::HashMap, io::Cursor};
use godot::prelude::*; use godot::{
classes::{FileAccess, file_access::ModeFlags},
prelude::*,
};
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
game_manager::{Game, Player}, game_manager::{Game, Player},
@ -97,7 +101,7 @@ impl INode for ReplayPlayback {
} }
ReplayEvent::Shoot(player_id, to) => { ReplayEvent::Shoot(player_id, to) => {
if let Some(character) = self.player_characters.get_mut(player_id) { if let Some(character) = self.player_characters.get_mut(player_id) {
character.bind_mut().try_shoot(*to, false); character.bind_mut().try_shoot((*to).into(), false);
} }
} }
ReplayEvent::Respawn(player_id, data) => { ReplayEvent::Respawn(player_id, data) => {
@ -124,7 +128,7 @@ impl INode for ReplayPlayback {
} }
ReplayEvent::Punch(player_id, to) => { ReplayEvent::Punch(player_id, to) => {
if let Some(character) = self.player_characters.get_mut(player_id) { if let Some(character) = self.player_characters.get_mut(player_id) {
character.bind_mut().try_punch(*to, false); character.bind_mut().try_punch((*to).into(), false);
} }
} }
ReplayEvent::TakeDamage(player_id, damage_source, damage) => { ReplayEvent::TakeDamage(player_id, damage_source, damage) => {
@ -213,11 +217,12 @@ impl INode for ReplayRecorder {
ReplayPlayer { ReplayPlayer {
id: player.id(), id: player.id(),
name: player.data.name.clone(), name: player.data.name.clone(),
color: team.bind().color, color: team.bind().color.into(),
}, },
); );
} }
} }
s.replay.map_idx = game.bind().selected_map_idx;
} }
}); });
} }
@ -236,6 +241,17 @@ impl INode for ReplayRecorder {
} }
impl ReplayRecorder { impl ReplayRecorder {
pub fn save_to(&self, path: &GString) {
if let Some(mut file) = FileAccess::open(path, ModeFlags::WRITE) {
let mut buf = Vec::new();
let cursor = Cursor::new(&mut buf);
ciborium::into_writer(&self.replay, cursor).ok();
for byte in buf {
file.store_8(byte);
}
}
}
pub fn record_event(&mut self, event: ReplayEvent) { pub fn record_event(&mut self, event: ReplayEvent) {
self.current_frame.events.push(event); self.current_frame.events.push(event);
} }
@ -274,8 +290,9 @@ impl ReplayRecorder {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Replay { pub struct Replay {
map_idx: u8,
players: HashMap<u16, ReplayPlayer>, players: HashMap<u16, ReplayPlayer>,
frames: Vec<ReplayFrame>, frames: Vec<ReplayFrame>,
} }
@ -283,20 +300,51 @@ pub struct Replay {
impl Default for Replay { impl Default for Replay {
fn default() -> Self { fn default() -> Self {
Self { Self {
map_idx: Default::default(),
players: Default::default(), players: Default::default(),
frames: Default::default(), frames: Default::default(),
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayPlayer { pub struct ReplayPlayer {
pub id: u16, pub id: u16,
pub name: String, pub name: String,
pub color: Color, pub color: ReplayColor,
} }
#[derive(Debug, Default, Clone)] #[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 { pub struct ReplayFrame {
time: f64, time: f64,
player_data: HashMap<u16, ReplayPlayerState>, player_data: HashMap<u16, ReplayPlayerState>,
@ -304,24 +352,24 @@ pub struct ReplayFrame {
events: Vec<ReplayEvent>, events: Vec<ReplayEvent>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReplayEvent { pub enum ReplayEvent {
Death(DamageSource, u16), Death(DamageSource, u16),
Shoot(u16, Vector3), Shoot(u16, NetVector3),
Respawn(u16, ReplayPlayerState), Respawn(u16, ReplayPlayerState),
Jump(u16), Jump(u16),
Punch(u16, Vector3), Punch(u16, NetVector3),
TakeDamage(u16, DamageSource, i32), TakeDamage(u16, DamageSource, i32),
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum ReplayBall { pub enum ReplayBall {
World(ReplayBallData), World(ReplayBallData),
#[default] #[default]
Held, Held,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayPlayerState { pub struct ReplayPlayerState {
pub transform: NetTransform, pub transform: NetTransform,
pub movement_dir: NetVector3, pub movement_dir: NetVector3,
@ -350,7 +398,7 @@ impl Player {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayBallData { pub struct ReplayBallData {
transform: NetTransform, transform: NetTransform,
velocity: NetVector3, velocity: NetVector3,

View File

@ -1,5 +1,5 @@
use godot::{ use godot::{
classes::{Button, HBoxContainer, IPanel, Label, Panel}, classes::{Button, FileDialog, HBoxContainer, IPanel, Label, Panel},
prelude::*, prelude::*,
}; };
@ -18,6 +18,9 @@ pub struct GameFinishedScreen {
#[export] #[export]
player_listings: Option<Gd<PlayerListings>>, player_listings: Option<Gd<PlayerListings>>,
#[export]
save_file_dialog: Option<Gd<FileDialog>>,
base: Base<Panel>, base: Base<Panel>,
} }
@ -36,10 +39,24 @@ impl IPanel for GameFinishedScreen {
GameManager::singleton().bind().go_to_lobby(); GameManager::singleton().bind().go_to_lobby();
}); });
} }
if let Some(file_dialog) = self.save_file_dialog.clone() {
file_dialog
.signals()
.file_selected()
.connect_other(self, Self::save_replay);
}
} }
} }
#[godot_api]
impl GameFinishedScreen { impl GameFinishedScreen {
#[func]
pub fn open_file_dialog(&mut self) {
if let Some(file_dialog) = &mut self.save_file_dialog {
file_dialog.popup_file_dialog();
}
}
fn produce_title(&mut self) { fn produce_title(&mut self) {
if let Some(game) = Game::singleton() { if let Some(game) = Game::singleton() {
let max_goals = game let max_goals = game
@ -84,4 +101,12 @@ impl GameFinishedScreen {
} }
} }
} }
pub fn save_replay(&mut self, path: GString) {
if let Some(game) = &Game::singleton()
&& let Some(recorder) = &game.bind().replay_recorder
{
recorder.bind().save_to(&path);
}
}
} }