Store replay to file
This commit is contained in:
parent
b3ecbfccd8
commit
5687adbf8a
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -200,6 +200,7 @@ dependencies = [
|
||||
name = "quakeball"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ciborium",
|
||||
"godot",
|
||||
"serde",
|
||||
"teanet",
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user