mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-13 12:13:09 -06:00
Update source files
This commit is contained in:
17
crates/borders-server/Cargo.toml
Normal file
17
crates/borders-server/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "borders-server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
borders-core = { path = "../borders-core" }
|
||||
flume = "0.11"
|
||||
rkyv = { version = "0.8", features = ["hashbrown-0_15"] }
|
||||
rustls-pemfile = "2.2.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||
tracing = "0.1"
|
||||
tracing-log = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
web-transport = "0.9"
|
||||
233
crates/borders-server/src/connections.rs
Normal file
233
crates/borders-server/src/connections.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
// Platform-specific imports for native server
|
||||
use crate::registry::ServerRegistry;
|
||||
use anyhow::Result;
|
||||
use borders_core::networking::{NetMessage, SourcedIntent};
|
||||
use flume::Sender;
|
||||
use rkyv::{Archived, api::deserialize_using, de::Pool};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{Instrument, error, info, instrument, warn};
|
||||
use web_transport::quinn::{RecvStream, SendStream};
|
||||
|
||||
type ArchivedNetMessage = Archived<NetMessage>;
|
||||
|
||||
/// Handle a single client connection over WebTransport
|
||||
#[instrument(skip_all)]
|
||||
pub async fn handle_client_connection(mut send_stream: SendStream, mut recv_stream: RecvStream, intent_tx: Sender<SourcedIntent>, registry: Arc<RwLock<ServerRegistry>>) -> Result<()> {
|
||||
info!("New client connected, starting message handling");
|
||||
|
||||
// Create a per-client channel for receiving broadcast messages
|
||||
let (client_tx, client_rx) = flume::unbounded::<NetMessage>();
|
||||
|
||||
// Register this client with the server registry and get assigned player ID
|
||||
let player_id = { registry.write().await.add_client(client_tx) };
|
||||
info!(player_id = %player_id, "Client registered");
|
||||
|
||||
// Send initial server config
|
||||
let server_config = NetMessage::ServerConfig { player_id };
|
||||
let config_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&server_config)?;
|
||||
let len_bytes = (config_bytes.len() as u64).to_be_bytes();
|
||||
|
||||
// Send length prefix
|
||||
let mut written = 0;
|
||||
while written < len_bytes.len() {
|
||||
let bytes_written = send_stream.write(&len_bytes[written..]).await?;
|
||||
written += bytes_written;
|
||||
}
|
||||
|
||||
// Send config bytes
|
||||
let mut written = 0;
|
||||
while written < config_bytes.len() {
|
||||
let bytes_written = send_stream.write(&config_bytes[written..]).await?;
|
||||
written += bytes_written;
|
||||
}
|
||||
|
||||
// Spawn task to handle incoming intents from this client
|
||||
let intent_tx_clone = intent_tx.clone();
|
||||
|
||||
tokio::spawn(
|
||||
async move {
|
||||
loop {
|
||||
// Read length prefix (8 bytes)
|
||||
let mut len_bytes = [0u8; 8];
|
||||
let mut read_so_far = 0;
|
||||
while read_so_far < 8 {
|
||||
let buf = &mut len_bytes[read_so_far..];
|
||||
if let Ok(maybe_size) = recv_stream.read(buf).await {
|
||||
if let Some(size) = maybe_size {
|
||||
read_so_far += size;
|
||||
} else {
|
||||
error!("Stream closed before reading length prefix");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
error!("Stream error reading length prefix");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if read_so_far < 8 {
|
||||
break;
|
||||
}
|
||||
let len = u64::from_be_bytes(len_bytes) as usize;
|
||||
|
||||
// Read message data
|
||||
let mut message_bytes = vec![0u8; len];
|
||||
let mut read_so_far = 0;
|
||||
while read_so_far < len {
|
||||
let buf = &mut message_bytes[read_so_far..];
|
||||
if let Ok(maybe_size) = recv_stream.read(buf).await {
|
||||
if let Some(size) = maybe_size {
|
||||
read_so_far += size;
|
||||
} else {
|
||||
error!("Stream closed before reading full message");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
error!("Stream error reading message data");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if read_so_far < len {
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode message
|
||||
let archived = unsafe { rkyv::access_unchecked::<ArchivedNetMessage>(&message_bytes) };
|
||||
match deserialize_using::<_, _, rkyv::rancor::Error>(archived, &mut Pool::new()) {
|
||||
Ok(net_message) => match net_message {
|
||||
NetMessage::Intent { id, intent } => {
|
||||
// Wrap intent with authenticated source player_id
|
||||
let sourced_intent = SourcedIntent { source: player_id, intent_id: id, intent };
|
||||
if let Err(e) = intent_tx_clone.send(sourced_intent) {
|
||||
error!(error = %e, "Failed to forward intent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => warn!("Received unexpected message type from client"),
|
||||
},
|
||||
Err(e) => {
|
||||
error!(error = %e, "Failed to deserialize message");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Client intent receiver task ended");
|
||||
}
|
||||
.instrument(tracing::trace_span!(
|
||||
"client_recv_loop",
|
||||
player_id = %player_id
|
||||
)),
|
||||
);
|
||||
|
||||
// Handle outgoing messages to this client
|
||||
let registry_clone = registry.clone();
|
||||
tokio::spawn(
|
||||
async move {
|
||||
while let Ok(message) = client_rx.recv_async().await {
|
||||
match rkyv::to_bytes::<rkyv::rancor::Error>(&message) {
|
||||
Ok(message_bytes) => {
|
||||
let len_bytes = (message_bytes.len() as u64).to_be_bytes();
|
||||
|
||||
// Send length prefix
|
||||
let mut written = 0;
|
||||
while written < len_bytes.len() {
|
||||
match send_stream.write(&len_bytes[written..]).await {
|
||||
Ok(bytes_written) => written += bytes_written,
|
||||
Err(e) => {
|
||||
error!(
|
||||
player_id = %player_id,
|
||||
error = %e,
|
||||
"Failed to send length prefix"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send message bytes
|
||||
let mut written = 0;
|
||||
while written < message_bytes.len() {
|
||||
match send_stream.write(&message_bytes[written..]).await {
|
||||
Ok(bytes_written) => written += bytes_written,
|
||||
Err(e) => {
|
||||
error!(player_id = %player_id, error = %e, "Failed to send message");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(player_id = %player_id, error = %e, "Failed to encode message");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove client from registry when sender task ends
|
||||
info!(
|
||||
player_id = %player_id,
|
||||
"Client message sender task ended, removing from registry"
|
||||
);
|
||||
registry_clone.write().await.remove_client(player_id);
|
||||
}
|
||||
.instrument(tracing::trace_span!("client_send_loop", player_id = %player_id)),
|
||||
);
|
||||
|
||||
info!(
|
||||
player_id = %player_id,
|
||||
"Client connection handler setup complete"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the WebTransport server and accept connections
|
||||
#[instrument(skip_all, fields(bind_address = %bind_address))]
|
||||
pub async fn start_server(bind_address: &str, intent_tx: Sender<SourcedIntent>, registry: Arc<RwLock<ServerRegistry>>) -> Result<()> {
|
||||
use web_transport::quinn::ServerBuilder;
|
||||
|
||||
info!("Starting WebTransport server");
|
||||
|
||||
// Load development certificate and key
|
||||
let cert_path = "dev-cert.pem";
|
||||
let key_path = "dev-key.pem";
|
||||
|
||||
let cert_data = std::fs::read(cert_path).map_err(|e| anyhow::anyhow!("Failed to read certificate file {}: {}", cert_path, e))?;
|
||||
let key_data = std::fs::read(key_path).map_err(|e| anyhow::anyhow!("Failed to read key file {}: {}", key_path, e))?;
|
||||
|
||||
// Parse certificate and key
|
||||
let certs = rustls_pemfile::certs(&mut &cert_data[..]).collect::<Result<Vec<_>, _>>().map_err(|e| anyhow::anyhow!("Failed to parse certificate: {}", e))?;
|
||||
|
||||
let key = rustls_pemfile::private_key(&mut &key_data[..]).map_err(|e| anyhow::anyhow!("Failed to parse private key: {}", e))?.ok_or_else(|| anyhow::anyhow!("No private key found"))?;
|
||||
|
||||
let mut server = ServerBuilder::new().with_addr(bind_address.parse()?).with_certificate(certs, key)?;
|
||||
info!("WebTransport server listening for connections");
|
||||
|
||||
loop {
|
||||
match server.accept().await {
|
||||
Some(connection) => {
|
||||
info!("New client connected");
|
||||
|
||||
let intent_tx_clone = intent_tx.clone();
|
||||
let registry_clone = registry.clone();
|
||||
|
||||
let session = connection.ok().await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Accept bidirectional stream from client
|
||||
match session.accept_bi().await {
|
||||
Ok((send_stream, recv_stream)) => {
|
||||
if let Err(e) = handle_client_connection(send_stream, recv_stream, intent_tx_clone, registry_clone).await {
|
||||
error!(error = %e, "Error handling client connection");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "Failed to accept bidirectional stream from client");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
None => {
|
||||
error!("Failed to accept connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
crates/borders-server/src/main.rs
Normal file
107
crates/borders-server/src/main.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use borders_core::networking::server::{SharedTurnGenerator, TurnOutput};
|
||||
use borders_core::networking::{NetMessage, SourcedIntent};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::{Duration, interval};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{connections::start_server, registry::ServerRegistry};
|
||||
|
||||
mod connections;
|
||||
mod registry;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Initialize tracing
|
||||
#[cfg(debug_assertions)]
|
||||
let default_filter = "borders_core=debug,borders_server=debug,borders_protocol=debug,info";
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let default_filter = "borders_core=warn,borders_server=warn,borders_protocol=warn,error";
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter))).init();
|
||||
|
||||
// Initialize log-to-tracing bridge for dependencies using log crate
|
||||
tracing_log::LogTracer::init().expect("Failed to set logger");
|
||||
|
||||
// Parse command line arguments
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let bind_address = if args.len() > 1 { args[1].clone() } else { "127.0.0.1:4433".to_string() };
|
||||
|
||||
info!(bind_address = %bind_address, "Starting borders relay server");
|
||||
|
||||
// Create channels for communication
|
||||
let (intent_tx, intent_rx) = flume::unbounded::<SourcedIntent>();
|
||||
|
||||
// Create server registry
|
||||
let registry = Arc::new(RwLock::new(ServerRegistry::new()));
|
||||
|
||||
// Spawn network listener task
|
||||
let intent_tx_clone = intent_tx.clone();
|
||||
let registry_clone = registry.clone();
|
||||
let bind_addr = bind_address.clone();
|
||||
info!("Spawning server listener task...");
|
||||
tokio::spawn(async move {
|
||||
info!("Server listener task started");
|
||||
if let Err(e) = start_server(&bind_addr, intent_tx_clone, registry_clone).await {
|
||||
error!(error = %e, "Server listener failed");
|
||||
}
|
||||
});
|
||||
|
||||
// Create shared turn generator
|
||||
let mut turn_generator = SharedTurnGenerator::new();
|
||||
let mut tick_interval = interval(Duration::from_millis(100)); // 100ms tick rate
|
||||
|
||||
info!("Server running - spawn phase active");
|
||||
|
||||
loop {
|
||||
let _guard = tracing::debug_span!("server_loop").entered();
|
||||
tick_interval.tick().await;
|
||||
|
||||
// Process all pending sourced intents
|
||||
let mut sourced_intents = Vec::new();
|
||||
while let Ok(sourced_intent) = intent_rx.try_recv() {
|
||||
let output = turn_generator.process_intent(sourced_intent.clone());
|
||||
|
||||
// Broadcast spawn updates to all clients
|
||||
if let TurnOutput::SpawnUpdate(spawn_config) = output {
|
||||
let spawn_message = NetMessage::SpawnConfiguration { spawns: spawn_config };
|
||||
registry.write().await.broadcast(spawn_message);
|
||||
}
|
||||
|
||||
// Collect sourced intents for turn generation
|
||||
sourced_intents.push(sourced_intent);
|
||||
}
|
||||
|
||||
// Tick the generator with 100ms delta
|
||||
let output = turn_generator.tick(100.0, sourced_intents);
|
||||
|
||||
// Handle turn output
|
||||
match output {
|
||||
TurnOutput::GameStart(turn) => {
|
||||
info!("Broadcasting Turn(0) to start game (spawns already configured)");
|
||||
let turn_message = NetMessage::Turn { turn: turn.turn_number, intents: turn.intents };
|
||||
registry.write().await.broadcast(turn_message);
|
||||
info!("Spawn phase complete - game started");
|
||||
}
|
||||
TurnOutput::Turn(turn) => {
|
||||
let turn_number = turn.turn_number;
|
||||
let intent_count = turn.intents.len();
|
||||
let client_count = { registry.read().await.client_count() };
|
||||
|
||||
let turn_message = NetMessage::Turn { turn: turn.turn_number, intents: turn.intents.clone() };
|
||||
|
||||
if !turn.intents.is_empty() || turn_number.is_multiple_of(100) {
|
||||
let _guard = tracing::trace_span!("turn_broadcast", turn_number = turn_number, intent_count = intent_count, client_count = client_count).entered();
|
||||
|
||||
info!(turn_number = turn_number, intent_count = intent_count, client_count = client_count, "Broadcasting turn");
|
||||
}
|
||||
|
||||
registry.write().await.broadcast(turn_message);
|
||||
}
|
||||
TurnOutput::None | TurnOutput::SpawnUpdate(_) => {
|
||||
// No turn to broadcast this tick
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
crates/borders-server/src/registry.rs
Normal file
66
crates/borders-server/src/registry.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use borders_core::game::NationId;
|
||||
use borders_core::networking::NetMessage;
|
||||
use flume::Sender;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tracing::error;
|
||||
|
||||
/// Connection information for a client
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientConnection {
|
||||
pub id: NationId,
|
||||
pub tx: Sender<NetMessage>,
|
||||
}
|
||||
|
||||
/// Registry for managing client connections and broadcasting messages
|
||||
pub struct ServerRegistry {
|
||||
connections: Arc<RwLock<HashMap<NationId, ClientConnection>>>,
|
||||
next_player_id: Arc<RwLock<u16>>,
|
||||
}
|
||||
|
||||
impl Default for ServerRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connections: Arc::new(RwLock::new(HashMap::new())),
|
||||
next_player_id: Arc::new(RwLock::new(1)), // Start from 1, 0 reserved
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new client connection and return assigned player ID
|
||||
pub fn add_client(&self, tx: Sender<NetMessage>) -> NationId {
|
||||
let mut next_id = self.next_player_id.write().unwrap();
|
||||
let player_id = NationId::new_unchecked(*next_id);
|
||||
*next_id += 1;
|
||||
|
||||
let connection = ClientConnection { id: player_id, tx };
|
||||
self.connections.write().unwrap().insert(player_id, connection);
|
||||
|
||||
player_id
|
||||
}
|
||||
|
||||
/// Remove a client connection
|
||||
pub fn remove_client(&self, player_id: NationId) {
|
||||
self.connections.write().unwrap().remove(&player_id);
|
||||
}
|
||||
|
||||
/// Broadcast a message to all connected clients
|
||||
pub fn broadcast(&self, message: NetMessage) {
|
||||
let connections = self.connections.read().unwrap();
|
||||
for connection in connections.values() {
|
||||
if let Err(e) = connection.tx.send(message.clone()) {
|
||||
error!("Failed to send message to client {}: {}", connection.id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of connected clients
|
||||
pub fn client_count(&self) -> usize {
|
||||
self.connections.read().unwrap().len()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user