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"
version = "0.1.0"
dependencies = [
"ciborium",
"godot",
"serde",
"teanet",

View File

@ -2,9 +2,11 @@
[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")
back_button = NodePath("button")
player_listings = NodePath("player_listings")
save_file_dialog = NodePath("save_dialog")
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
@ -39,6 +41,22 @@ grow_vertical = 0
theme_override_font_sizes/font_size = 32
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]
player_listing_scene = ExtResource("1_ou354")
layout_mode = 1
@ -62,3 +80,8 @@ offset_top = 93.0
offset_right = 20.0
offset_bottom = 133.0
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.
[dependencies]
ciborium = "0.2.2"
godot = "0.5.4"
serde = "1.0.228"
teanet = { path = "../teanet-rust" }

View File

@ -1008,7 +1008,7 @@ impl Game {
if let Some(recorder) = &mut self.replay_recorder {
recorder
.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);
@ -1035,7 +1035,7 @@ impl Game {
if let Some(recorder) = &mut self.replay_recorder {
recorder
.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);

View File

@ -112,7 +112,7 @@ impl Map {
let mut node: Gd<RemotePlayer> = replay_player.instantiate_as();
node.bind_mut().player_id = Some(player_id);
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);
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::{
game_manager::{Game, Player},
@ -97,7 +101,7 @@ impl INode for ReplayPlayback {
}
ReplayEvent::Shoot(player_id, to) => {
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) => {
@ -124,7 +128,7 @@ impl INode for ReplayPlayback {
}
ReplayEvent::Punch(player_id, to) => {
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) => {
@ -213,11 +217,12 @@ impl INode for ReplayRecorder {
ReplayPlayer {
id: player.id(),
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 {
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) {
self.current_frame.events.push(event);
}
@ -274,8 +290,9 @@ impl ReplayRecorder {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Replay {
map_idx: u8,
players: HashMap<u16, ReplayPlayer>,
frames: Vec<ReplayFrame>,
}
@ -283,20 +300,51 @@ pub struct Replay {
impl Default for Replay {
fn default() -> Self {
Self {
map_idx: Default::default(),
players: Default::default(),
frames: Default::default(),
}
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayPlayer {
pub id: u16,
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 {
time: f64,
player_data: HashMap<u16, ReplayPlayerState>,
@ -304,24 +352,24 @@ pub struct ReplayFrame {
events: Vec<ReplayEvent>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReplayEvent {
Death(DamageSource, u16),
Shoot(u16, Vector3),
Shoot(u16, NetVector3),
Respawn(u16, ReplayPlayerState),
Jump(u16),
Punch(u16, Vector3),
Punch(u16, NetVector3),
TakeDamage(u16, DamageSource, i32),
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum ReplayBall {
World(ReplayBallData),
#[default]
Held,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayPlayerState {
pub transform: NetTransform,
pub movement_dir: NetVector3,
@ -350,7 +398,7 @@ impl Player {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayBallData {
transform: NetTransform,
velocity: NetVector3,

View File

@ -1,5 +1,5 @@
use godot::{
classes::{Button, HBoxContainer, IPanel, Label, Panel},
classes::{Button, FileDialog, HBoxContainer, IPanel, Label, Panel},
prelude::*,
};
@ -18,6 +18,9 @@ pub struct GameFinishedScreen {
#[export]
player_listings: Option<Gd<PlayerListings>>,
#[export]
save_file_dialog: Option<Gd<FileDialog>>,
base: Base<Panel>,
}
@ -36,10 +39,24 @@ impl IPanel for GameFinishedScreen {
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 {
#[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) {
if let Some(game) = Game::singleton() {
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);
}
}
}