From bfb6ab471df81f99726321c415b0f70512834918 Mon Sep 17 00:00:00 2001 From: Sofia Date: Thu, 23 Jul 2026 21:39:51 +0300 Subject: [PATCH] Add settings that persist --- Cargo.lock | 77 +++++++++++++++++++ godot/scenes/main.tscn | 5 ++ godot/scenes/ui/settings.tscn | 54 ++++++++++++++ godot/scenes/ui/slider.tscn | 19 +++++ rust/Cargo.toml | 1 + rust/src/game_settings.rs | 47 ++++++++++++ rust/src/lib.rs | 1 + rust/src/ui/mod.rs | 2 + rust/src/ui/settings.rs | 137 ++++++++++++++++++++++++++++++++++ rust/src/ui/slider.rs | 56 ++++++++++++++ 10 files changed, 399 insertions(+) create mode 100644 godot/scenes/ui/settings.tscn create mode 100644 godot/scenes/ui/slider.tscn create mode 100644 rust/src/game_settings.rs create mode 100644 rust/src/ui/settings.rs create mode 100644 rust/src/ui/slider.rs diff --git a/Cargo.lock b/Cargo.lock index 3763ca7..80841ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "gdextension-api" version = "0.5.1" @@ -138,12 +144,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "libc" version = "0.2.186" @@ -181,6 +203,7 @@ dependencies = [ "godot", "serde", "teanet", + "toml", ] [[package]] @@ -222,6 +245,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "syn" version = "2.0.119" @@ -262,6 +294,45 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -278,6 +349,12 @@ dependencies = [ "quote", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "zerocopy" version = "0.8.54" diff --git a/godot/scenes/main.tscn b/godot/scenes/main.tscn index 285a4c9..5ee48f3 100644 --- a/godot/scenes/main.tscn +++ b/godot/scenes/main.tscn @@ -1,6 +1,7 @@ [gd_scene format=3 uid="uid://lahirbqfppvw"] [ext_resource type="PackedScene" uid="uid://bbmc2gmy7g8ag" path="res://scenes/misc/menu_player.tscn" id="1_o5qli"] +[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", "popup_panel", "popup_title", "popup_text", "popup_button")] camera = NodePath("camera_3d") @@ -175,6 +176,9 @@ text = "Join" layout_mode = 2 text = "Back" +[node name="settings" parent="." unique_id=999912383 instance=ExtResource("2_0wfyh")] +visible = false + [node name="main_pos" type="Node3D" parent="." unique_id=98241074] transform = Transform3D(0.98629165, -0.031186387, 0.16203767, -1.4054484e-08, 0.981978, 0.1889952, -0.16501155, -0.18640435, 0.9685167, 1.0974054, 1.7166122, 1.4909104) @@ -232,6 +236,7 @@ text = "Ok" [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/settings" to="settings" method="open"] [connection signal="pressed" from="main_panel/v_box_container/quit" to="." method="quit_game"] [connection signal="pressed" from="host_panel/v_box_container/host" to="." method="host_game"] [connection signal="pressed" from="host_panel/v_box_container/back" to="." method="go_to_main_menu"] diff --git a/godot/scenes/ui/settings.tscn b/godot/scenes/ui/settings.tscn new file mode 100644 index 0000000..fb28c66 --- /dev/null +++ b/godot/scenes/ui/settings.tscn @@ -0,0 +1,54 @@ +[gd_scene format=3 uid="uid://n3u0jr4wiib1"] + +[ext_resource type="PackedScene" uid="uid://cqiwdajl71ujv" path="res://scenes/ui/slider.tscn" id="1_8wfqe"] + +[node name="settings" type="SettingsPanel" unique_id=999912383 node_paths=PackedStringArray("settings_grid")] +settings_grid = NodePath("grid") +slider_scene = ExtResource("1_8wfqe") +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -207.0 +offset_top = -112.0 +offset_right = 207.0 +offset_bottom = 112.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="label" type="Label" parent="." unique_id=2036746921] +layout_mode = 0 +offset_left = 57.0 +offset_right = 357.0 +offset_bottom = 23.0 +text = "Settings" +horizontal_alignment = 1 + +[node name="grid" type="GridContainer" parent="." unique_id=833231169] +custom_minimum_size = Vector2(400, 0) +custom_maximum_size = Vector2(400, -1) +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -145.0 +offset_top = -74.0 +offset_right = 155.0 +offset_bottom = 67.0 +grow_horizontal = 2 +grow_vertical = 2 +columns = 2 + +[node name="button" type="Button" parent="." unique_id=1301469030] +layout_mode = 0 +offset_left = 182.0 +offset_top = 192.0 +offset_right = 232.0 +offset_bottom = 223.0 +size_flags_horizontal = 4 +text = "Close" + +[connection signal="pressed" from="button" to="." method="close"] diff --git a/godot/scenes/ui/slider.tscn b/godot/scenes/ui/slider.tscn new file mode 100644 index 0000000..ec397c3 --- /dev/null +++ b/godot/scenes/ui/slider.tscn @@ -0,0 +1,19 @@ +[gd_scene format=3 uid="uid://cqiwdajl71ujv"] + +[node name="slider" type="SliderContainer" unique_id=2000698366 node_paths=PackedStringArray("label", "slider")] +label = NodePath("label") +slider = NodePath("h_slider") +value = 50.0 +max = 100.0 +offset_right = 214.0 +offset_bottom = 23.0 + +[node name="label" type="Label" parent="." unique_id=2059967949] +layout_mode = 2 +text = "50" + +[node name="h_slider" type="HSlider" parent="." unique_id=1564648919] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +size_flags_vertical = 4 +value = 50.0 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c906dfd..310363c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,3 +10,4 @@ crate-type = ["cdylib"] # Compile this crate to a dynamic C library. godot = "0.5.4" serde = "1.0.228" teanet = { path = "../teanet-rust" } +toml = "1.1.3" diff --git a/rust/src/game_settings.rs b/rust/src/game_settings.rs new file mode 100644 index 0000000..84aeaa5 --- /dev/null +++ b/rust/src/game_settings.rs @@ -0,0 +1,47 @@ +use godot::{ + classes::{FileAccess, file_access::ModeFlags}, + prelude::*, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GameSettings { + pub fov: f64, +} + +impl Default for GameSettings { + fn default() -> Self { + Self { fov: 90. } + } +} + +#[derive(GodotClass)] +#[class(base=Node, singleton)] +pub struct GameSettingsManager { + pub settings: GameSettings, + base: Base, +} + +#[godot_api] +impl INode for GameSettingsManager { + fn init(base: Base) -> GameSettingsManager { + let mut settings = GameSettings::default(); + if let Some(file) = FileAccess::open("user://config.toml", ModeFlags::READ) { + if let Ok(deserialized) = toml::from_str(&file.get_as_text().to_string()) { + settings = deserialized; + } + } + GameSettingsManager { settings, base } + } +} + +impl GameSettingsManager { + pub fn update_settings(&mut self, settings: GameSettings) { + self.settings = settings; + if let Some(mut file) = FileAccess::open("user://config.toml", ModeFlags::WRITE) { + if let Ok(serialized) = toml::to_string(&self.settings) { + file.store_string(&serialized); + } + } + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 300247c..3d35069 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,6 +3,7 @@ use godot::prelude::*; pub mod ball; pub mod cli_parser; pub mod game_manager; +pub mod game_settings; pub mod goal; pub mod killbox; pub mod map; diff --git a/rust/src/ui/mod.rs b/rust/src/ui/mod.rs index cad9fe1..401c73e 100644 --- a/rust/src/ui/mod.rs +++ b/rust/src/ui/mod.rs @@ -6,4 +6,6 @@ pub mod main_menu; pub mod menu_player; pub mod net_stats; pub mod player_listing; +pub mod settings; pub mod simple_score_view; +pub mod slider; diff --git a/rust/src/ui/settings.rs b/rust/src/ui/settings.rs new file mode 100644 index 0000000..9fb0a29 --- /dev/null +++ b/rust/src/ui/settings.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; + +use godot::{ + classes::{Control, GridContainer, IPanel, Label, Panel, control::SizeFlags}, + prelude::*, +}; + +use crate::{ + game_settings::{GameSettings, GameSettingsManager}, + ui::slider::SliderContainer, +}; + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Hash)] +pub enum Setting { + FoV, +} + +impl ToString for Setting { + fn to_string(&self) -> String { + match self { + Setting::FoV => "Field of View", + } + .to_string() + } +} + +#[derive(Clone, Copy)] +pub enum SettingValue { + SliderNumber(SliderValue), +} + +#[derive(Clone, Copy)] +pub struct SliderValue { + min: f64, + max: f64, + value: f64, +} + +#[derive(GodotClass)] +#[class(base=Panel, init)] +pub struct SettingsPanel { + #[export] + settings_grid: Option>, + #[export] + slider_scene: Option>, + + settings: HashMap, + + base: Base, +} + +#[godot_api] +impl IPanel for SettingsPanel { + fn ready(&mut self) { + let singleton = GameSettingsManager::singleton(); + let settings = &singleton.bind().settings; + self.settings.insert( + Setting::FoV, + SettingValue::SliderNumber(SliderValue { + min: 60., + max: 120., + value: settings.fov, + }), + ); + + if let Some(mut grid) = self.settings_grid.clone() { + for (setting, value) in &self.settings { + let mut label = Label::new_alloc(); + label.set_text(&setting.to_string()); + label.set_h_size_flags(SizeFlags::EXPAND | SizeFlags::SHRINK_BEGIN); + grid.add_child(&label); + + if let Some(scene) = self.slider_scene.clone() { + let node = match value { + SettingValue::SliderNumber(slider_value) => { + let mut slider = scene.instantiate_as::(); + + slider.bind_mut().min = slider_value.min; + slider.bind_mut().max = slider_value.max; + slider.bind_mut().value = slider_value.value; + + let setting_clone = setting.clone(); + let value_clone = slider_value.clone(); + slider.bind_mut().signals().on_change().connect_other( + self, + move |s, new_value| { + s.run_deferred(move |s| { + s.on_change( + setting_clone, + SettingValue::SliderNumber(SliderValue { + value: new_value, + ..value_clone + }), + ) + }) + }, + ); + + slider.upcast::() + } + }; + + grid.add_child(&node); + } + } + } + } +} + +#[godot_api] +impl SettingsPanel { + pub fn on_change(&mut self, setting: Setting, new_value: SettingValue) { + self.settings.insert(setting, new_value); + + let mut new_settings = GameSettingsManager::singleton().bind().settings.clone(); + for (setting, value) in &self.settings { + match (setting, value) { + (Setting::FoV, SettingValue::SliderNumber(value)) => { + new_settings.fov = value.value; + } + } + } + GameSettingsManager::singleton() + .bind_mut() + .update_settings(new_settings); + } + + #[func] + pub fn open(&mut self) { + self.base_mut().set_visible(true); + } + + #[func] + pub fn close(&mut self) { + self.base_mut().set_visible(false); + } +} diff --git a/rust/src/ui/slider.rs b/rust/src/ui/slider.rs new file mode 100644 index 0000000..a2d6b02 --- /dev/null +++ b/rust/src/ui/slider.rs @@ -0,0 +1,56 @@ +use godot::{ + classes::{HBoxContainer, IHBoxContainer, Label, Slider}, + prelude::*, +}; + +#[derive(GodotClass)] +#[class(base=HBoxContainer, init, tool)] +pub struct SliderContainer { + #[export] + label: Option>, + #[export] + slider: Option>, + #[export] + pub value: f64, + #[export] + pub min: f64, + #[export] + pub max: f64, + + last_value: f64, + + base: Base, +} + +#[godot_api] +impl IHBoxContainer for SliderContainer { + fn ready(&mut self) {} + + fn process(&mut self, _delta: f64) { + if let Some(slider) = &mut self.slider { + slider.set_min(self.min); + slider.set_max(self.max); + if self.last_value != self.value { + slider.set_value(self.value); + } + self.value = slider.get_value(); + if self.last_value != self.value { + let value = self.value; + self.signals().on_change().emit(value); + } + self.last_value = self.value; + } + + if let Some(label) = &mut self.label + && let Some(slider) = &self.slider + { + label.set_text(&format!("{}", slider.get_value())); + } + } +} + +#[godot_api] +impl SliderContainer { + #[signal] + pub fn on_change(value: f64); +}