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"
|
name = "quakeball"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ciborium",
|
||||||
"godot",
|
"godot",
|
||||||
"serde",
|
"serde",
|
||||||
"teanet",
|
"teanet",
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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" }
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user