From 3dfeae14f75bccdc3a7b975e70c34bf1639e36bb Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Sun, 26 Oct 2025 00:44:30 -0400 Subject: [PATCH] Initial change propagation work --- .vscode/settings.json | 33 +++++++-------- Cargo.lock | 1 + Cargo.toml | 2 +- src/game/plugin.rs | 19 +++++---- src/game/runtime.rs | 9 ++++- src/game/seed.rs | 3 ++ src/game/setup.rs | 6 +-- src/net/distribution.rs | 90 ++++++++++++++++++++++++++++++++--------- src/net/heartbeat.rs | 9 ++--- src/net/io.rs | 7 ++-- src/net/mod.rs | 2 +- src/net/packet.rs | 35 +++++++++++----- src/net/peer.rs | 2 +- 13 files changed, 146 insertions(+), 72 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ebe79c..2d7649e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,20 +23,21 @@ // ], "rust-analyzer.cargo.targetDir": true, "cSpell.words": [ - "Backquote", - "Cheatbook", - "codegen", - "despawn", - "Despawns", - "Iyes", - "lerp", - "PRNG", - "recip", - "respawns", - "Seedable", - "timestep", - "timesteps", - "winit", - "wyrand" - ] + "Backquote", + "Cheatbook", + "codegen", + "despawn", + "Despawns", + "Iyes", + "lerp", + "PRNG", + "recip", + "respawns", + "Seedable", + "timestep", + "timesteps", + "winit", + "wyrand", + "zerocopy" +] } diff --git a/Cargo.lock b/Cargo.lock index 19fe817..1b049ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4599,6 +4599,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom", "js-sys", + "rand", "serde", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index b4c9950..f30754f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ rand = { version = "0.9.2", default-features = false, features = [ "std", "thread_rng", ] } -uuid = { version = "1.18.1", features = ["v4"] } +uuid = { version = "1.18.1", features = ["v4", "fast-rng"] } wyrand = "0.3.2" [features] diff --git a/src/game/plugin.rs b/src/game/plugin.rs index 586dd48..13ea24f 100644 --- a/src/game/plugin.rs +++ b/src/game/plugin.rs @@ -1,13 +1,16 @@ use std::net::SocketAddr; use avian2d::PhysicsPlugins; -use bevy::{input::common_conditions::input_pressed, prelude::*}; +use bevy::{ + input::common_conditions::{input_just_pressed, input_pressed}, + prelude::*, +}; use crate::net::prelude::*; use super::{ net::{handle_deleted_peer, handle_incoming_packets, handle_new_peer}, - runtime::{move_camera, move_player, quit, zoom_camera}, + runtime::{move_camera, move_player, quit, reset_seed, zoom_camera}, seed::Seed, setup::{ check_for_seed, setup_balls, setup_camera, setup_from_seed, setup_player, setup_walls, @@ -76,13 +79,7 @@ impl Plugin for GamePlugin { OnEnter(AppState::InGame), (setup_from_seed, (setup_player, setup_balls, setup_walls)).chain(), ) - .add_systems( - FixedUpdate, - ( - check_for_seed.run_if(in_state(AppState::Loading)), - handle_incoming_packets, - ), - ) + .add_systems(FixedUpdate, handle_incoming_packets) .add_systems( Update, ( @@ -94,11 +91,13 @@ impl Plugin for GamePlugin { update_peer_ui_timings, update_potential_peer_ui, ), + reset_seed.run_if(input_just_pressed(KeyCode::KeyR)), quit.run_if(input_pressed(KeyCode::KeyQ)), ), ) .add_observer(handle_new_peer) - .add_observer(handle_deleted_peer); + .add_observer(handle_deleted_peer) + .add_observer(check_for_seed); match self.source { DataSource::Address(peer) => { diff --git a/src/game/runtime.rs b/src/game/runtime.rs index f5273f3..405a7e9 100644 --- a/src/game/runtime.rs +++ b/src/game/runtime.rs @@ -7,7 +7,10 @@ use bevy::{ prelude::*, }; -use super::objects::{Player, Radius}; +use super::{ + objects::{Player, Radius}, + seed::Seed, +}; /// Move the player character based on the keyboard input pub fn move_player( @@ -81,3 +84,7 @@ pub fn zoom_camera( projection.scale = (projection.scale - scroll.delta.y * scroll_type_multiplier).clamp(0.1, 4.0); Ok(()) } + +pub fn reset_seed(mut seed: Single<&mut Seed>) { + **seed = Seed::random() +} diff --git a/src/game/seed.rs b/src/game/seed.rs index d95a957..8c470ce 100644 --- a/src/game/seed.rs +++ b/src/game/seed.rs @@ -6,8 +6,11 @@ use std::{ use bevy::prelude::*; use rand::random; +use crate::net::prelude::{EntityNetworkID, Networked}; + /// Value with which to initialize the PRNG #[derive(Clone, Component, Copy, Debug)] +#[require(EntityNetworkID)] pub struct Seed(u64); impl Seed { diff --git a/src/game/setup.rs b/src/game/setup.rs index 464b9db..7ab9251 100644 --- a/src/game/setup.rs +++ b/src/game/setup.rs @@ -23,10 +23,8 @@ const BALL_COUNT: u8 = 32; const BALL_SIZES: Range = 10.0..25.0; const DIMENSION_SIZES: Range = 500.0..2000.0; -pub fn check_for_seed(seed: Option>, mut next_state: ResMut>) { - if seed.is_some() { - next_state.set(AppState::InGame); - } +pub fn check_for_seed(_add: On, mut next_state: ResMut>) { + next_state.set(AppState::InGame); } /// The size of the playable area (x, y) diff --git a/src/net/distribution.rs b/src/net/distribution.rs index d991399..6c47c61 100644 --- a/src/net/distribution.rs +++ b/src/net/distribution.rs @@ -1,17 +1,27 @@ -use bevy::prelude::*; +use bevy::{ + ecs::{component::Mutable, query::QueryFilter}, + prelude::*, +}; +use uuid::Uuid; use super::{ packet::{InboundPacket, OutboundPacket, Packet}, peer::PeerID, }; -#[derive(Component)] +/// Entities wishing to be networked must have this in their bundle +#[derive(Component, Debug)] +pub struct EntityNetworkID(Uuid); + +impl Default for EntityNetworkID { + fn default() -> Self { + Self(Uuid::new_v4()) + } +} + +#[derive(Component, Debug)] pub struct PeerOwned; -pub trait Networked: Component + NetworkEncodable + NetworkDecodable {} - -impl Networked for T where T: Component + NetworkEncodable + NetworkDecodable {} - pub trait NetworkEncodable { fn encode(&self) -> Vec; } @@ -40,45 +50,87 @@ where } } -fn incoming_network_entity( +/// Components wishing to be networked must implement this type +pub trait Networked: Component + NetworkEncodable + NetworkDecodable { + type LocalFilter: QueryFilter; + type RemoteFilter: QueryFilter; +} + +impl Networked for T +where + T: Component + NetworkEncodable + NetworkDecodable, +{ + type LocalFilter = Without; + type RemoteFilter = With; +} + +fn incoming_network_entity< + T: NetworkDecodable + Component, + F: QueryFilter, +>( mut inbound: MessageReader, + mut components: Query<(&mut T, &EntityNetworkID), F>, mut commands: Commands, ) { - for InboundPacket(packet) in inbound.read() { + 'packets: for InboundPacket(packet) in inbound.read() { if let Ok(component) = T::decode(packet.message.clone()) { - commands.spawn((component, PeerOwned)); + for (mut existing_component, id) in &mut components { + if id.0 == packet.entity { + *existing_component = component; + continue 'packets; + } + } + commands.spawn((component, EntityNetworkID(packet.entity), PeerOwned)); } } } fn new_peer( add: On, - components: Query<&T, Without>, + components: Query<(&T, &EntityNetworkID), Without>, peers: Query<&PeerID>, mut outbound: MessageWriter, ) -> Result { let peer = peers.get(add.entity)?; - for component in components { - outbound.write(Packet::create(peer.id, component.encode())); + for (component, id) in components { + outbound.write(Packet::create(peer.id, id.0, component.encode())); } Ok(()) } -fn new_entity( +fn new_local_entity( add: On, - components: Query<&T, Without>, + components: Query<(&T, &EntityNetworkID), Without>, peers: Query<&PeerID>, mut outbound: MessageWriter, ) { - if let Ok(component) = components.get(add.entity) { + if let Ok((component, id)) = components.get(add.entity) { for peer in peers { - outbound.write(Packet::create(peer.id, component.encode())); + outbound.write(Packet::create(peer.id, id.0, component.encode())); + } + } +} + +fn changed_local_entity( + components: Query<(&T, &EntityNetworkID), (F, Changed)>, + peers: Query<&PeerID>, + mut outbound: MessageWriter, +) { + for (component, id) in components { + for peer in peers { + outbound.write(Packet::create(peer.id, id.0, component.encode())); } } } pub fn distribution_plugin(app: &mut App) { - app.add_systems(FixedUpdate, incoming_network_entity::) - .add_observer(new_peer::) - .add_observer(new_entity::); + app.add_systems( + FixedUpdate, + ( + changed_local_entity::, + incoming_network_entity::, + ), + ) + .add_observer(new_peer::) + .add_observer(new_local_entity::); } diff --git a/src/net/heartbeat.rs b/src/net/heartbeat.rs index 4408862..3fc87bb 100644 --- a/src/net/heartbeat.rs +++ b/src/net/heartbeat.rs @@ -1,13 +1,12 @@ use std::time::Duration; use bevy::prelude::*; - -use crate::net::peer::PotentialPeers; +use uuid::Uuid; use super::{ io::{Config, format_message}, packet::{OutboundPacket, Packet}, - peer::{Peer, PeerChangeMessage, PeerID, PeerReceiveTiming, PeerSendTiming}, + peer::{Peer, PeerChangeMessage, PeerID, PeerReceiveTiming, PeerSendTiming, PotentialPeers}, queues::NetworkSend, }; @@ -24,7 +23,7 @@ pub fn heartbeat( if last.time() + PING_FREQUENCY > time.elapsed() { continue; } - outbound.write(Packet::create(peer.id, Vec::new())); + outbound.write(Packet::create(peer.id, Uuid::nil(), Vec::new())); } Ok(()) } @@ -48,7 +47,7 @@ pub fn ping_potential_peers( config: Res, ) -> Result { for peer in &peers.addresses { - to_socket.send(format_message(config.id, &Vec::new()), *peer)?; + to_socket.send(format_message(config.id, Uuid::nil(), &Vec::new()), *peer)?; } Ok(()) } diff --git a/src/net/io.rs b/src/net/io.rs index 8cb6cd7..6aa12d5 100644 --- a/src/net/io.rs +++ b/src/net/io.rs @@ -18,8 +18,8 @@ impl Default for Config { } } -pub fn format_message(id: Uuid, data: &Vec) -> Vec { - [id.as_bytes(), data.as_slice()].concat() +pub fn format_message(peer: Uuid, entity: Uuid, data: &Vec) -> Vec { + [peer.as_bytes(), entity.as_bytes(), data.as_slice()].concat() } pub fn handle_network_input( @@ -33,7 +33,6 @@ pub fn handle_network_input( for (message, address) in from_socket.iter() { match Packet::try_from(message) { Ok(packet) => { - // TODO: Handle packet variant if !packet.message.is_empty() { to_app.write(packet.clone().into()); } @@ -63,7 +62,7 @@ pub fn handle_network_output( for OutboundPacket(packet) in from_app.read() { let peer_id = peer_map.try_get(&packet.peer)?; let (peer, mut last) = peers.get_mut(*peer_id)?; - let message = format_message(config.id, &packet.message); + let message = format_message(config.id, packet.entity, &packet.message); to_socket.send(message, peer.addr.into())?; last.update(&time); } diff --git a/src/net/mod.rs b/src/net/mod.rs index 1a5834c..bcbb70f 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -11,7 +11,7 @@ mod thread; #[allow(unused_imports)] pub mod prelude { pub use super::distribution::{ - NetworkDecodable, NetworkEncodable, Networked, distribution_plugin, + EntityNetworkID, NetworkDecodable, NetworkEncodable, Networked, distribution_plugin, }; pub use super::packet::{InboundPacket, OutboundPacket, Packet}; pub use super::peer::{Peer, PeerID, PeerReceiveTiming, PeerSendTiming, PotentialPeers}; diff --git a/src/net/packet.rs b/src/net/packet.rs index 8177f51..275dad0 100644 --- a/src/net/packet.rs +++ b/src/net/packet.rs @@ -7,30 +7,45 @@ pub enum TryFromBytesError { NotUUID, } -pub const UUID_SIZE: usize = 16; - #[derive(Clone, Debug)] pub struct Packet { pub peer: Uuid, + pub entity: Uuid, pub message: Vec, } impl Packet { - pub fn create>(peer: Uuid, message: Vec) -> T { - Self { peer, message }.into() + pub fn create>(peer: Uuid, entity: Uuid, message: Vec) -> T { + Self { + peer, + entity, + message, + } + .into() } } +pub const UUID_SIZE: usize = 16; + +fn extract_uuid(buffer: &mut Vec) -> std::result::Result { + if buffer.len() < UUID_SIZE { + return Err(TryFromBytesError::InsufficientLength); + } + let og_not_uuid = &mut buffer.split_off(UUID_SIZE); + // Return the rest of the vector through the argument + // TODO: Check if this has a performance penalty + std::mem::swap(og_not_uuid, buffer); + // TODO: The Uuid crate has support for zerocopy (although I'm copying in a ton of other place regardless) + Uuid::from_slice(og_not_uuid.as_slice()).map_err(|_| TryFromBytesError::NotUUID) +} + impl TryFrom> for Packet { type Error = TryFromBytesError; fn try_from(mut value: Vec) -> std::result::Result { - if value.len() < UUID_SIZE { - return Err(TryFromBytesError::InsufficientLength); - } - let message = value.split_off(UUID_SIZE); - let uuid = Uuid::from_slice(value.as_slice()).map_err(|_| TryFromBytesError::NotUUID)?; - Ok(Packet::create(uuid, message)) + let peer = extract_uuid(&mut value)?; + let entity = extract_uuid(&mut value)?; + Ok(Packet::create(peer, entity, value)) } } diff --git a/src/net/peer.rs b/src/net/peer.rs index a59ae39..1e7a974 100644 --- a/src/net/peer.rs +++ b/src/net/peer.rs @@ -280,7 +280,7 @@ pub fn handle_new_peer( if change.is_added() { for (_, other, data) in peers { if peer.id != other.id { - outbound.write(Packet::create(peer.id, data.addr.into())); + outbound.write(Packet::create(peer.id, other.id, data.addr.into())); } } }