Compare commits

...

10 Commits

Author SHA1 Message Date
d251973dd3 Refactor a bit 2026-07-17 23:28:28 +03:00
11b6691a4e Deny new connections after game has started 2026-07-17 23:15:46 +03:00
73c7c5b850 Add map selection and entering game 2026-07-17 23:09:11 +03:00
c594af3bd1 Add readiness 2026-07-17 21:36:29 +03:00
b01ecbfe13 Censor player ip when joining 2026-07-17 21:02:11 +03:00
936de8fd0b Fix player creation for 3 players 2026-07-17 20:32:43 +03:00
6ffdd201a7 Fix player disconnecting 2026-07-17 20:13:44 +03:00
4c58824840 Add more proper player listings 2026-07-17 19:59:06 +03:00
8893a2370d Update keybinds 2026-07-17 19:09:51 +03:00
eaac940b98 Allow toggling cli 2026-07-17 19:09:14 +03:00
16 changed files with 585 additions and 49 deletions

View File

@ -0,0 +1,7 @@
[gd_resource type="Map" format=3 uid="uid://b4rw48yjf7q3q"]
[ext_resource type="PackedScene" uid="uid://lahirbqfppvw" path="res://scenes/main.tscn" id="1_k5804"]
[resource]
name = "Default Map"
scene = ExtResource("1_k5804")

View File

@ -0,0 +1,7 @@
[gd_resource type="Map" format=3 uid="uid://crti664evgfmj"]
[ext_resource type="PackedScene" uid="uid://lahirbqfppvw" path="res://scenes/main.tscn" id="1_gr7ev"]
[resource]
name = "Default Map 2"
scene = ExtResource("1_gr7ev")

View File

@ -34,6 +34,15 @@ project/assembly_name="QuakeBall"
naming/node_name_casing=2 naming/node_name_casing=2
[input]
toggle_cli={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":167,"key_label":0,"unicode":167,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[physics] [physics]
3d/physics_engine="Jolt Physics" 3d/physics_engine="Jolt Physics"

View File

@ -1,6 +1,7 @@
[gd_scene format=3 uid="uid://bsb6asp5o35w3"] [gd_scene format=3 uid="uid://bsb6asp5o35w3"]
[node name="canvas_layer" type="CanvasLayer" unique_id=1517655401] [node name="canvas_layer" type="CanvasLayer" unique_id=1517655401]
layer = 10
[node name="command_line_panel" type="CommandLinePanel" parent="." unique_id=2077938969 node_paths=PackedStringArray("input_field", "text_field", "scroll_container")] [node name="command_line_panel" type="CommandLinePanel" parent="." unique_id=2077938969 node_paths=PackedStringArray("input_field", "text_field", "scroll_container")]
input_field = NodePath("input_field") input_field = NodePath("input_field")
@ -8,7 +9,10 @@ text_field = NodePath("scroll_container/text_field")
scroll_container = NodePath("scroll_container") scroll_container = NodePath("scroll_container")
anchors_preset = 10 anchors_preset = 10
anchor_right = 1.0 anchor_right = 1.0
offset_bottom = 307.0 offset_left = 1.0
offset_top = -344.0
offset_right = 1.0
offset_bottom = -37.0
grow_horizontal = 2 grow_horizontal = 2
[node name="input_field" type="TextEdit" parent="command_line_panel" unique_id=2095748573] [node name="input_field" type="TextEdit" parent="command_line_panel" unique_id=2095748573]
@ -18,6 +22,8 @@ anchor_right = 1.0
offset_top = 307.0 offset_top = 307.0
offset_bottom = 342.0 offset_bottom = 342.0
grow_horizontal = 2 grow_horizontal = 2
editable = false
metadata/_edit_lock_ = true
[node name="scroll_container" type="ScrollContainer" parent="command_line_panel" unique_id=1922272461] [node name="scroll_container" type="ScrollContainer" parent="command_line_panel" unique_id=1922272461]
custom_minimum_size = Vector2(-1, 0) custom_minimum_size = Vector2(-1, 0)
@ -28,6 +34,7 @@ offset_left = 1.0
offset_right = -1.0 offset_right = -1.0
offset_bottom = 292.0 offset_bottom = 292.0
grow_horizontal = 2 grow_horizontal = 2
metadata/_edit_lock_ = true
[node name="text_field" type="RichTextLabel" parent="command_line_panel/scroll_container" unique_id=591332585] [node name="text_field" type="RichTextLabel" parent="command_line_panel/scroll_container" unique_id=591332585]
custom_minimum_size = Vector2(-1, -1) custom_minimum_size = Vector2(-1, -1)
@ -37,3 +44,4 @@ fit_content = true
scroll_active = false scroll_active = false
autowrap_mode = 0 autowrap_mode = 0
shortcut_keys_enabled = false shortcut_keys_enabled = false
metadata/_edit_lock_ = true

View File

@ -1,3 +1,7 @@
[gd_scene format=3 uid="uid://bii6018k3ip3t"] [gd_scene format=3 uid="uid://bii6018k3ip3t"]
[ext_resource type="Map" uid="uid://b4rw48yjf7q3q" path="res://maps/default_map.tres" id="1_glms7"]
[ext_resource type="Map" uid="uid://crti664evgfmj" path="res://maps/default_map_2.tres" id="2_udu3i"]
[node name="game_manager" type="GameManager" unique_id=1442044769] [node name="game_manager" type="GameManager" unique_id=1442044769]
maps = Array[Map]([ExtResource("1_glms7"), ExtResource("2_udu3i")])

View File

@ -1,11 +1,16 @@
[gd_scene format=3 uid="uid://bwd6bj21x310s"] [gd_scene format=3 uid="uid://bwd6bj21x310s"]
[ext_resource type="PackedScene" uid="uid://ccd0wj2usx5os" path="res://scenes/player_listing.tscn" id="1_o1atq"]
[sub_resource type="LabelSettings" id="LabelSettings_1cgct"] [sub_resource type="LabelSettings" id="LabelSettings_1cgct"]
font_size = 32 font_size = 32
[node name="lobby" type="LobbyPanel" unique_id=915192272 node_paths=PackedStringArray("players_list", "name_field")] [node name="lobby" type="LobbyPanel" unique_id=915192272 node_paths=PackedStringArray("players_list", "name_field", "ready_button", "map_selection")]
players_list = NodePath("v_box_container/players_list") players_list = NodePath("v_box_container/players_list")
name_field = NodePath("h_box_container/text_edit") name_field = NodePath("v_box_container/h_box_container/text_edit")
player_listing_prefab = ExtResource("1_o1atq")
ready_button = NodePath("ready_button")
map_selection = NodePath("v_box_container2/option_button")
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
@ -25,29 +30,57 @@ horizontal_alignment = 1
[node name="v_box_container" type="VBoxContainer" parent="." unique_id=336946640] [node name="v_box_container" type="VBoxContainer" parent="." unique_id=336946640]
layout_mode = 0 layout_mode = 0
offset_left = 178.0 offset_left = 129.0
offset_top = 189.0 offset_top = 146.0
offset_right = 239.0 offset_right = 334.0
offset_bottom = 229.0 offset_bottom = 200.0
[node name="h_box_container" type="HBoxContainer" parent="v_box_container" unique_id=1789302658]
layout_mode = 2
metadata/_edit_lock_ = true
[node name="label" type="Label" parent="v_box_container/h_box_container" unique_id=626633696]
layout_mode = 2
text = "Name:"
[node name="text_edit" type="TextEdit" parent="v_box_container/h_box_container" unique_id=683714912]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
[node name="players_label" type="Label" parent="v_box_container" unique_id=1537542117] [node name="players_label" type="Label" parent="v_box_container" unique_id=1537542117]
layout_mode = 2 layout_mode = 2
text = "Players:" text = "Players:"
metadata/_edit_lock_ = true
[node name="players_list" type="VBoxContainer" parent="v_box_container" unique_id=1834737379] [node name="players_list" type="VBoxContainer" parent="v_box_container" unique_id=1834737379]
layout_mode = 2 layout_mode = 2
metadata/_edit_lock_ = true
[node name="h_box_container" type="HBoxContainer" parent="." unique_id=1789302658] [node name="netstats" type="NetStatsLabel" parent="." unique_id=1335814511]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -144.0
grow_horizontal = 2
grow_vertical = 0
vertical_alignment = 2
[node name="ready_button" type="Button" parent="." unique_id=311553419]
layout_mode = 0 layout_mode = 0
offset_left = 221.0 offset_left = 129.0
offset_top = 512.0 offset_top = 504.0
offset_right = 261.0 offset_right = 278.0
offset_bottom = 552.0 offset_bottom = 555.0
text = "Not Ready"
[node name="label" type="Label" parent="h_box_container" unique_id=626633696] [node name="v_box_container2" type="VBoxContainer" parent="." unique_id=653902328]
layout_mode = 2 layout_mode = 0
text = "Name:" offset_left = 716.0
offset_top = 146.0
offset_right = 824.0
offset_bottom = 204.0
[node name="text_edit" type="TextEdit" parent="h_box_container" unique_id=683714912] [node name="option_button" type="OptionButton" parent="v_box_container2" unique_id=426342653]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2 layout_mode = 2

View File

@ -0,0 +1,26 @@
[gd_scene format=3 uid="uid://ccd0wj2usx5os"]
[node name="player_listing" type="PlayerListing" unique_id=772938280 node_paths=PackedStringArray("name_label", "ping_label", "ready_label")]
name_label = NodePath("name")
ping_label = NodePath("ping")
ready_label = NodePath("ready")
offset_right = 319.0
offset_bottom = 23.0
columns = 3
[node name="name" type="Label" parent="." unique_id=635700663]
custom_minimum_size = Vector2(200, 0)
custom_maximum_size = Vector2(200, -1)
layout_mode = 2
text = "Name"
[node name="ready" type="Label" parent="." unique_id=1064828735]
custom_minimum_size = Vector2(100, 0)
custom_maximum_size = Vector2(100, -1)
layout_mode = 2
text = "Ready"
[node name="ping" type="Label" parent="." unique_id=1935412601]
layout_mode = 2
text = "(200ms)"
horizontal_alignment = 2

View File

@ -3,6 +3,11 @@ use std::net::SocketAddr;
use godot::{prelude::*, tools::get_autoload_by_name}; use godot::{prelude::*, tools::get_autoload_by_name};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{
map_resource::Map,
net::network_manager::{NetworkManager, Package, PeerKind},
};
pub const GAME_MANAGER_GLOBAL: &str = "GameManagerGlobal"; pub const GAME_MANAGER_GLOBAL: &str = "GameManagerGlobal";
#[derive(GodotClass)] #[derive(GodotClass)]
@ -10,6 +15,9 @@ pub const GAME_MANAGER_GLOBAL: &str = "GameManagerGlobal";
pub struct GameManager { pub struct GameManager {
pub game: Option<Gd<Game>>, pub game: Option<Gd<Game>>,
base: Base<Node>, base: Base<Node>,
#[export]
pub maps: Array<Gd<Map>>,
} }
#[godot_api] #[godot_api]
@ -20,6 +28,8 @@ impl GameManager {
pub fn on_new_player(id: u16); pub fn on_new_player(id: u16);
#[signal] #[signal]
pub fn on_player_disconnected(id: u16); pub fn on_player_disconnected(id: u16);
#[signal]
pub fn on_map_changed(id: u8);
pub fn singleton() -> Gd<GameManager> { pub fn singleton() -> Gd<GameManager> {
get_autoload_by_name::<GameManager>(GAME_MANAGER_GLOBAL) get_autoload_by_name::<GameManager>(GAME_MANAGER_GLOBAL)
@ -31,6 +41,7 @@ impl GameManager {
} }
let mut game = Game::new_alloc(); let mut game = Game::new_alloc();
game.bind_mut().is_host = is_host; game.bind_mut().is_host = is_host;
game.bind_mut().maps = self.maps.clone();
self.run_deferred(|s| { self.run_deferred(|s| {
if let Some(game) = &s.game { if let Some(game) = &s.game {
@ -43,6 +54,9 @@ impl GameManager {
game.signals() game.signals()
.on_players_updated() .on_players_updated()
.connect_other(s, |s| s.signals().on_players_updated().emit()); .connect_other(s, |s| s.signals().on_players_updated().emit());
game.signals()
.on_map_changed()
.connect_other(s, |s, id| s.signals().on_map_changed().emit(id));
} }
}); });
@ -55,10 +69,15 @@ impl GameManager {
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Node, init)] #[class(base=Node, init)]
pub struct Game { pub struct Game {
pub selected_map_idx: u8,
pub selected_map: Option<Gd<Map>>,
pub maps: Array<Gd<Map>>,
is_host: bool, is_host: bool,
player_counter: u16, player_counter: u16,
pub self_id: Option<u16>, pub self_id: Option<u16>,
pub players: Vec<Player>, pub players: Vec<Player>,
game_started: bool,
base: Base<Node>, base: Base<Node>,
} }
@ -71,6 +90,8 @@ impl Game {
pub fn on_new_player(id: u16); pub fn on_new_player(id: u16);
#[signal] #[signal]
pub fn on_player_disconnected(id: u16); pub fn on_player_disconnected(id: u16);
#[signal]
pub fn on_map_changed(id: u8);
pub fn singleton() -> Option<Gd<Game>> { pub fn singleton() -> Option<Gd<Game>> {
GameManager::singleton() GameManager::singleton()
@ -78,34 +99,75 @@ impl Game {
.map(|g| g.cast::<Game>()) .map(|g| g.cast::<Game>())
} }
pub fn change_map_selection(&mut self, map_idx: u8, emit: bool) {
self.selected_map_idx = map_idx;
if let Some(map) = self.maps.get(map_idx as usize) {
self.selected_map = Some(map);
if emit {
self.run_deferred(move |s| s.signals().on_map_changed().emit(map_idx));
}
}
}
pub fn start_game(&mut self) {
self.game_started = true;
if let Some(map) = &self.selected_map {
if let Some(scene) = &map.bind().scene {
self.base().get_tree().change_scene_to_packed(scene);
}
}
self.run_deferred(|_| {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer {
if let PeerKind::Server(peer, _) = peer {
peer.broadcast_reliable(Package::GameStarted);
peer.set_accepting_connections(false);
}
}
});
}
pub fn set_self(&mut self, id: u16) { pub fn set_self(&mut self, id: u16) {
self.self_id = Some(id); self.self_id = Some(id);
} }
pub fn new_player(&mut self, addr: SocketAddr, name: String) -> u16 { pub fn new_player(&mut self, addr: SocketAddr, name: String, id: Option<u16>) -> u16 {
let id = self.player_counter; let id = if let Some(id) = id {
self.player_counter += 1; id
} else {
let id = self.player_counter;
self.player_counter += 1;
id
};
self.players.push(Player { self.players.push(Player {
id, id,
connection_addr: addr, connection_addr: addr,
name, name,
ping: 0,
ready: false,
}); });
self.signals().on_new_player().emit(id); self.run_deferred(move |s| s.signals().on_new_player().emit(id));
id id
} }
pub fn set_players(&mut self, players: Vec<Player>) { pub fn set_players(&mut self, players: Vec<Player>) {
self.players = players; self.players = players;
self.signals().on_players_updated().emit(); for player in self.players.clone() {
self.run_deferred(move |s| s.signals().on_new_player().emit(player.id));
}
} }
pub fn find_player_by_addr(&self, addr: &SocketAddr) -> Option<&Player> { pub fn find_player_by_addr(&self, addr: &SocketAddr) -> Option<&Player> {
self.players.iter().find(|p| p.connection_addr == *addr) self.players.iter().find(|p| p.connection_addr == *addr)
} }
pub fn find_player(&self, player_id: u16) -> Option<&Player> {
self.players.iter().find(|p| p.id == player_id)
}
pub fn remove_player(&mut self, id: u16) { pub fn remove_player(&mut self, id: u16) {
self.signals().on_player_disconnected().emit(id);
self.players.retain(|p| p.id != id); self.players.retain(|p| p.id != id);
self.run_deferred(move |s| s.signals().on_player_disconnected().emit(id));
} }
pub fn update_player_nick(&mut self, player_id: u16, nick: String) { pub fn update_player_nick(&mut self, player_id: u16, nick: String) {
@ -114,6 +176,20 @@ impl Game {
} }
self.run_deferred(|s| s.signals().on_players_updated().emit()); self.run_deferred(|s| s.signals().on_players_updated().emit());
} }
pub fn update_player_ping(&mut self, player_id: u16, ping: u128) {
if let Some(player) = self.players.iter_mut().find(|p| p.id == player_id) {
player.ping = ping;
}
self.run_deferred(|s| s.signals().on_players_updated().emit());
}
pub fn update_player_ready(&mut self, player_id: u16, ready: bool) {
if let Some(player) = self.players.iter_mut().find(|p| p.id == player_id) {
player.ready = ready;
}
self.run_deferred(|s| s.signals().on_players_updated().emit());
}
} }
#[derive(Clone, Deserialize, Serialize, Debug)] #[derive(Clone, Deserialize, Serialize, Debug)]
@ -121,4 +197,6 @@ pub struct Player {
pub id: u16, pub id: u16,
pub connection_addr: SocketAddr, pub connection_addr: SocketAddr,
pub name: String, pub name: String,
pub ping: u128,
pub ready: bool,
} }

View File

@ -3,6 +3,7 @@ use std::net::SocketAddr;
use godot::prelude::*; use godot::prelude::*;
pub mod game_manager; pub mod game_manager;
pub mod map_resource;
pub mod net; pub mod net;
pub mod ui; pub mod ui;

27
rust/src/map_resource.rs Normal file
View File

@ -0,0 +1,27 @@
use godot::prelude::*;
#[derive(GodotClass)]
#[class(base=Resource, init, tool)]
pub struct Map {
#[export]
pub name: GString,
#[export]
pub scene: Option<Gd<PackedScene>>,
base: Base<Resource>,
}
#[godot_api]
impl IResource for Map {
fn on_notification(&mut self, what: godot::classes::notify::ObjectNotification) {}
fn to_string(&self) -> GString {
"Hello there".to_gstring()
}
fn setup_local_to_scene(&mut self) {}
fn reset_state(&mut self) {}
fn set_path_cache(&self, path: GString) {}
}

View File

@ -1,11 +1,12 @@
use std::{ use std::{
collections::HashMap,
net::SocketAddr, net::SocketAddr,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use godot::{prelude::*, tools::get_autoload_by_name}; use godot::{prelude::*, tools::get_autoload_by_name};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use teanet::{Peer, PeerConfig, PeerMessage}; use teanet::{Peer, PeerConfig, PeerMessage, connections::Connection};
use crate::{ use crate::{
game_manager::{Game, GameManager, Player}, game_manager::{Game, GameManager, Player},
@ -23,6 +24,7 @@ pub struct NetworkManager {
last_hello: Instant, last_hello: Instant,
last_netstat_update: Instant, last_netstat_update: Instant,
last_player_update: Instant,
base: Base<Node>, base: Base<Node>,
} }
@ -39,6 +41,7 @@ impl INode for NetworkManager {
last_hello: Instant::now(), last_hello: Instant::now(),
last_netstat_update: Instant::now(), last_netstat_update: Instant::now(),
last_player_update: Instant::now(),
base, base,
} }
@ -74,6 +77,29 @@ impl INode for NetworkManager {
self.last_netstat_update = now; self.last_netstat_update = now;
} }
if now - self.last_player_update > Duration::from_millis(500) {
self.last_player_update = now;
if let PeerKind::Server(peer, _) = peer {
let mut pings = HashMap::new();
for connection in peer.connections() {
if let Some(mut game) = Game::singleton() {
let player = game
.bind()
.find_player_by_addr(&connection.address)
.cloned();
if let Some(player) = player {
game.bind_mut()
.update_player_ping(player.id, connection.ping.as_millis());
pings.insert(player.id, connection.ping.as_millis());
}
}
}
peer.broadcast_reliable(Package::UpdatePings(pings));
}
}
match peer { match peer {
PeerKind::Client(..) => {} PeerKind::Client(..) => {}
PeerKind::Server(peer, _) => { PeerKind::Server(peer, _) => {
@ -137,6 +163,49 @@ impl INode for NetworkManager {
} }
} }
} }
Package::UpdatePings(pings) => {
if let Some(game) = &mut Game::singleton() {
for (player_id, ping) in pings {
game.bind_mut().update_player_ping(player_id, ping);
}
}
}
Package::NewPlayer(player) => {
if let Some(game) = &mut Game::singleton() {
if player.id != game.bind().self_id.unwrap_or(u16::MAX)
{
game.bind_mut().new_player(
SocketAddr::from(([0, 0, 0, 0], 0)),
player.name,
Some(player.id),
);
}
}
}
Package::PlayerLeft(player) => {
if let Some(game) = &mut Game::singleton() {
if player.id != game.bind().self_id.unwrap_or(u16::MAX)
{
game.bind_mut().remove_player(player.id);
}
}
}
Package::SetReady(player_id, is_ready) => {
if let Some(game) = &mut Game::singleton() {
game.bind_mut()
.update_player_ready(player_id, is_ready);
}
}
Package::SelectMap(map_id) => {
if let Some(game) = &mut Game::singleton() {
game.bind_mut().change_map_selection(map_id, true);
}
}
Package::GameStarted => {
if let Some(game) = &mut Game::singleton() {
game.bind_mut().start_game();
}
}
_ => {} _ => {}
}, },
} }
@ -158,12 +227,29 @@ impl INode for NetworkManager {
); );
if let Some(game) = &mut Game::singleton() { if let Some(game) = &mut Game::singleton() {
let self_id = game let self_id = game.bind_mut().new_player(
.bind_mut() connection.address,
.new_player(connection.address, String::new()); String::new(),
None,
);
let mut sent_players = game.bind().players.clone();
for player in &mut sent_players {
player.connection_addr =
SocketAddr::from(([0, 0, 0, 0], 0))
}
peer.send_reliable( peer.send_reliable(
&connection.address, &connection.address,
Package::Players(game.bind().players.clone(), self_id), Package::Players(sent_players, self_id),
);
let mut sent_player =
game.bind().find_player(self_id).unwrap().clone();
sent_player.connection_addr =
SocketAddr::from(([0, 0, 0, 0], 0));
peer.broadcast_reliable(Package::NewPlayer(sent_player));
peer.send_reliable(
&connection.address,
Package::SelectMap(game.bind().selected_map_idx),
); );
} }
} }
@ -184,6 +270,9 @@ impl INode for NetworkManager {
.find_player_by_addr(&connection.address) .find_player_by_addr(&connection.address)
.map(|p| p.id); .map(|p| p.id);
if let Some(player_id) = player_id { if let Some(player_id) = player_id {
peer.broadcast_reliable(Package::PlayerLeft(
game.bind().find_player(player_id).unwrap().clone(),
));
game.bind_mut().remove_player(player_id); game.bind_mut().remove_player(player_id);
} }
} }
@ -210,6 +299,21 @@ impl INode for NetworkManager {
} }
} }
} }
Package::SetReady(_, is_ready) => {
if let Some(game) = &mut Game::singleton() {
let player = game
.bind()
.find_player_by_addr(&conn.address)
.cloned();
if let Some(player) = player {
game.bind_mut()
.update_player_ready(player.id, is_ready);
peer.broadcast_reliable(Package::SetReady(
player.id, is_ready,
));
}
}
}
_ => {} _ => {}
}, },
} }
@ -217,7 +321,7 @@ impl INode for NetworkManager {
} }
Err(err) => { Err(err) => {
cli.bind_mut() cli.bind_mut()
.publish_message(format!("Error: {}", err), CliColor::Info); .publish_message(format!("Error: {}", err), CliColor::Error);
} }
}, },
} }
@ -246,9 +350,12 @@ impl NetworkManager {
self.swap_to_lobby(true); self.swap_to_lobby(true);
if let Some(game) = &mut Game::singleton() { if let Some(game) = &mut Game::singleton() {
let id = game let id = game.bind_mut().new_player(
.bind_mut() SocketAddr::from(([0, 0, 0, 0], 0)),
.new_player(SocketAddr::from(([0, 0, 0, 0], 0)), "Host".to_owned()); "Host".to_owned(),
None,
);
game.bind_mut().update_player_ready(id, true);
game.bind_mut().set_self(id); game.bind_mut().set_self(id);
} }
} }
@ -320,9 +427,24 @@ pub enum Package {
Players(Vec<Player>, u16), Players(Vec<Player>, u16),
SetNick(u16, String), SetNick(u16, String),
SetSelfNick(String), SetSelfNick(String),
UpdatePings(HashMap<u16, u128>),
NewPlayer(Player),
PlayerLeft(Player),
SetReady(u16, bool),
SelectMap(u8),
GameStarted,
} }
pub enum PeerKind { pub enum PeerKind {
Client(Peer<Package>, SocketAddr), Client(Peer<Package>, SocketAddr),
Server(Peer<Package>, u16), Server(Peer<Package>, u16),
} }
impl PeerKind {
pub fn is_host(&self) -> bool {
match self {
PeerKind::Client(_, _) => false,
PeerKind::Server(_, _) => true,
}
}
}

View File

@ -4,7 +4,7 @@ use std::ops::DerefMut;
use std::rc::Rc; use std::rc::Rc;
use godot::classes::{ use godot::classes::{
CanvasLayer, Engine, IPanel, Panel, RichTextLabel, ScrollContainer, TextEdit, CanvasLayer, Engine, IPanel, InputEvent, Panel, RichTextLabel, ScrollContainer, TextEdit,
}; };
use godot::prelude::*; use godot::prelude::*;
use godot::tools::get_autoload_by_name; use godot::tools::get_autoload_by_name;
@ -39,9 +39,14 @@ pub struct CommandLinePanel {
#[export] #[export]
pub scroll_container: Option<Gd<ScrollContainer>>, pub scroll_container: Option<Gd<ScrollContainer>>,
#[export]
pub opened: bool,
scroll_next_update: bool, scroll_next_update: bool,
commands: HashMap<String, Rc<Box<dyn Command>>>, commands: HashMap<String, Rc<Box<dyn Command>>>,
idle_y: f32,
base: Base<Panel>, base: Base<Panel>,
} }
@ -61,12 +66,18 @@ impl IPanel for CommandLinePanel {
input_field: None, input_field: None,
text_field: None, text_field: None,
scroll_container: None, scroll_container: None,
opened: false,
scroll_next_update: false, scroll_next_update: false,
idle_y: 0.,
commands, commands,
base, base,
} }
} }
fn ready(&mut self) {
self.idle_y = self.base().get_position().y;
}
fn process(&mut self, _delta: f64) { fn process(&mut self, _delta: f64) {
if let Some(scroll) = &mut self.scroll_container if let Some(scroll) = &mut self.scroll_container
&& self.scroll_next_update && self.scroll_next_update
@ -83,6 +94,32 @@ impl IPanel for CommandLinePanel {
self.input_command(first.to_string()); self.input_command(first.to_string());
} }
} }
let old_pos = self.base().get_position();
let mut target_pos = self.base().get_position();
if self.opened {
target_pos.y = 0.;
} else {
target_pos.y = self.idle_y;
}
let new_pos = old_pos.lerp(target_pos, 0.3);
self.base_mut().set_position(new_pos);
}
fn input(&mut self, event: Gd<InputEvent>) {
if event.is_action_pressed("toggle_cli") {
self.opened = !self.opened;
if let Some(input_field) = &mut self.input_field {
if self.opened {
input_field.grab_focus();
input_field.set_editable(true);
self.run_deferred(|s| s.input_field.as_mut().unwrap().clear());
} else {
input_field.release_focus();
input_field.set_editable(false);
}
}
}
} }
} }

View File

@ -1,11 +1,13 @@
use godot::{ use godot::{
classes::{IPanel, Label, Panel, TextEdit, VBoxContainer}, classes::{Button, IPanel, Label, OptionButton, Panel, TextEdit, VBoxContainer},
prelude::*, prelude::*,
}; };
use crate::{ use crate::{
game_manager::{Game, GameManager}, game_manager::{Game, GameManager},
net::network_manager::NetworkManager, map_resource::Map,
net::network_manager::{NetworkManager, Package, PeerKind},
ui::player_listing::PlayerListing,
}; };
#[derive(GodotClass)] #[derive(GodotClass)]
@ -15,6 +17,14 @@ pub struct LobbyPanel {
players_list: Option<Gd<VBoxContainer>>, players_list: Option<Gd<VBoxContainer>>,
#[export] #[export]
name_field: Option<Gd<TextEdit>>, name_field: Option<Gd<TextEdit>>,
#[export]
player_listing_prefab: Option<Gd<PackedScene>>,
#[export]
ready_button: Option<Gd<Button>>,
#[export]
map_selection: Option<Gd<OptionButton>>,
is_ready: bool,
base: Base<Panel>, base: Base<Panel>,
} }
@ -25,15 +35,50 @@ impl IPanel for LobbyPanel {
GameManager::singleton() GameManager::singleton()
.signals() .signals()
.on_new_player() .on_new_player()
.connect_other(self, |s, _| s.update_players_list()); .connect_other(self, |s, _| s.update_players(true));
GameManager::singleton() GameManager::singleton()
.signals() .signals()
.on_player_disconnected() .on_player_disconnected()
.connect_other(self, |s, _| s.update_players_list()); .connect_other(self, |s, _| s.update_players(true));
GameManager::singleton() GameManager::singleton()
.signals() .signals()
.on_players_updated() .on_players_updated()
.connect_other(self, |s| s.update_players_list()); .connect_other(self, |s| s.update_players(false));
GameManager::singleton()
.signals()
.on_map_changed()
.connect_other(self, |s, id| s.update_map_selection(id));
if let Some(ready_button) = &self.ready_button {
ready_button
.signals()
.pressed()
.connect_other(self, |s| s.on_ready());
}
if let Some(map_selection) = &self.map_selection {
map_selection
.signals()
.item_selected()
.connect_other(self, |s, id| {
s.update_map_selection(id as u8);
});
}
if let Some(game) = Game::singleton() {
if let Some(map_selection) = &mut self.map_selection {
for map in game.bind().maps.iter_shared() {
map_selection.add_item(&map.bind().name);
}
let is_host = if let Some(peer) = &NetworkManager::singleton().bind().peer {
peer.is_host()
} else {
false
};
map_selection.set_disabled(!is_host);
map_selection.select(0);
}
}
self.update_map_selection(0);
if let Some(name_field) = &self.name_field { if let Some(name_field) = &self.name_field {
name_field name_field
@ -58,21 +103,103 @@ impl IPanel for LobbyPanel {
}); });
} }
self.update_players_list(); self.update_players(true);
if let Some(peer) = &NetworkManager::singleton().bind().peer
&& peer.is_host()
{
if let Some(button) = &mut self.ready_button {
button.set_text("Start Game");
button.set_disabled(true);
}
}
self.update_ready_button();
} }
} }
impl LobbyPanel { impl LobbyPanel {
pub fn update_players_list(&mut self) { fn update_ready_button(&mut self) {
if let Some(list) = &mut self.players_list { if let Some(game) = Game::singleton()
for child in list.get_children().iter_shared() { && let Some(peer) = &NetworkManager::singleton().bind().peer
list.remove_child(&child); {
if let PeerKind::Server(_, _) = peer
&& let Some(button) = &mut self.ready_button
{
let all_ready = game.bind().players.iter().all(|p| p.ready);
button.set_disabled(!all_ready);
} }
if let Some(game) = &GameManager::singleton().bind().game { }
for player in &game.bind().players { }
let mut label = Label::new_alloc();
label.set_text(&format!("{} ({})", player.name, player.connection_addr)); pub fn update_players(&mut self, replace: bool) {
list.add_child(&label); self.update_ready_button();
if let Some(list) = &mut self.players_list {
if replace {
for child in list.get_children().iter_shared() {
list.remove_child(&child);
}
if let Some(game) = &GameManager::singleton().bind().game {
if let Some(prefab) = &self.player_listing_prefab {
for player in &game.bind().players {
if let Some(node) = prefab.instantiate() {
let mut listing = node.cast::<PlayerListing>();
listing.bind_mut().player_id = Some(player.id);
listing.bind_mut().update();
list.add_child(&listing);
}
}
}
}
} else {
for child in list.get_children().iter_shared() {
child.cast::<PlayerListing>().bind_mut().update();
}
}
}
}
pub fn on_ready(&mut self) {
let is_host = if let Some(peer) = &NetworkManager::singleton().bind().peer {
peer.is_host()
} else {
false
};
if !is_host {
self.is_ready = !self.is_ready;
if let Some(button) = &mut self.ready_button {
if self.is_ready {
button.set_text("Ready");
} else {
button.set_text("Not Ready");
}
}
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer {
if let PeerKind::Client(peer, socket_addr) = peer {
peer.send_reliable(&socket_addr, Package::SetReady(0, self.is_ready));
}
}
} else {
if let Some(game) = &mut Game::singleton() {
game.bind_mut().start_game();
}
}
}
pub fn update_map_selection(&mut self, idx: u8) {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer {
match peer {
PeerKind::Client(..) => {
if let Some(map_selection) = &mut self.map_selection {
map_selection.select(idx as i32);
}
}
PeerKind::Server(peer, _) => {
if let Some(game) = &mut Game::singleton() {
game.bind_mut().change_map_selection(idx, false);
}
peer.broadcast_reliable(Package::SelectMap(idx));
} }
} }
} }

View File

@ -1,3 +1,4 @@
pub mod cli; pub mod cli;
pub mod lobby; pub mod lobby;
pub mod net_stats; pub mod net_stats;
pub mod player_listing;

View File

@ -0,0 +1,49 @@
use godot::{
classes::{GridContainer, IGridContainer, Label, Panel, TextEdit, VBoxContainer},
prelude::*,
};
use crate::{
game_manager::{Game, Player},
net::network_manager::NetworkManager,
};
#[derive(GodotClass)]
#[class(base=GridContainer, init)]
pub struct PlayerListing {
#[export]
pub name_label: Option<Gd<Label>>,
#[export]
pub ping_label: Option<Gd<Label>>,
#[export]
pub ready_label: Option<Gd<Label>>,
pub player_id: Option<u16>,
base: Base<GridContainer>,
}
impl PlayerListing {
pub fn update(&mut self) {
if let Some(player_id) = &self.player_id
&& let Some(game) = Game::singleton()
&& let Some(player) = game.bind().find_player(*player_id)
{
if let Some(player_label) = &mut self.name_label {
player_label.set_text(&format!("{} ({})", player.name, player.connection_addr));
}
if let Some(ready_label) = &mut self.ready_label {
if player.ready {
ready_label.set_text("Ready");
} else {
ready_label.set_text("Not Ready");
}
}
if let Some(ping_label) = &mut self.ping_label {
ping_label.set_text(&format!("({}ms)", player.ping));
}
}
}
}

@ -1 +1 @@
Subproject commit 2721021f7b20831388d9ea7b3ea0797e3efdfdac Subproject commit a947f3a8cbad03b939dedfc1486de75226d90c0a