Initial change propagation work
All checks were successful
CI / Formatting (push) Successful in 42s

This commit is contained in:
Michael Bradley 2025-10-26 00:44:30 -04:00
parent fe967d70b9
commit 3dfeae14f7
Signed by: MichaelBradley
SSH key fingerprint: SHA256:BKO2eI2LPsCbQS3n3i5SdwZTAIV3F1lHezR07qP+Ob0
13 changed files with 146 additions and 72 deletions

33
.vscode/settings.json vendored
View file

@ -23,20 +23,21 @@
// ], // ],
"rust-analyzer.cargo.targetDir": true, "rust-analyzer.cargo.targetDir": true,
"cSpell.words": [ "cSpell.words": [
"Backquote", "Backquote",
"Cheatbook", "Cheatbook",
"codegen", "codegen",
"despawn", "despawn",
"Despawns", "Despawns",
"Iyes", "Iyes",
"lerp", "lerp",
"PRNG", "PRNG",
"recip", "recip",
"respawns", "respawns",
"Seedable", "Seedable",
"timestep", "timestep",
"timesteps", "timesteps",
"winit", "winit",
"wyrand" "wyrand",
] "zerocopy"
]
} }

1
Cargo.lock generated
View file

@ -4599,6 +4599,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"js-sys", "js-sys",
"rand",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
] ]

View file

@ -63,7 +63,7 @@ rand = { version = "0.9.2", default-features = false, features = [
"std", "std",
"thread_rng", "thread_rng",
] } ] }
uuid = { version = "1.18.1", features = ["v4"] } uuid = { version = "1.18.1", features = ["v4", "fast-rng"] }
wyrand = "0.3.2" wyrand = "0.3.2"
[features] [features]

View file

@ -1,13 +1,16 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use avian2d::PhysicsPlugins; 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 crate::net::prelude::*;
use super::{ use super::{
net::{handle_deleted_peer, handle_incoming_packets, handle_new_peer}, 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, seed::Seed,
setup::{ setup::{
check_for_seed, setup_balls, setup_camera, setup_from_seed, setup_player, setup_walls, 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), OnEnter(AppState::InGame),
(setup_from_seed, (setup_player, setup_balls, setup_walls)).chain(), (setup_from_seed, (setup_player, setup_balls, setup_walls)).chain(),
) )
.add_systems( .add_systems(FixedUpdate, handle_incoming_packets)
FixedUpdate,
(
check_for_seed.run_if(in_state(AppState::Loading)),
handle_incoming_packets,
),
)
.add_systems( .add_systems(
Update, Update,
( (
@ -94,11 +91,13 @@ impl Plugin for GamePlugin {
update_peer_ui_timings, update_peer_ui_timings,
update_potential_peer_ui, update_potential_peer_ui,
), ),
reset_seed.run_if(input_just_pressed(KeyCode::KeyR)),
quit.run_if(input_pressed(KeyCode::KeyQ)), quit.run_if(input_pressed(KeyCode::KeyQ)),
), ),
) )
.add_observer(handle_new_peer) .add_observer(handle_new_peer)
.add_observer(handle_deleted_peer); .add_observer(handle_deleted_peer)
.add_observer(check_for_seed);
match self.source { match self.source {
DataSource::Address(peer) => { DataSource::Address(peer) => {

View file

@ -7,7 +7,10 @@ use bevy::{
prelude::*, prelude::*,
}; };
use super::objects::{Player, Radius}; use super::{
objects::{Player, Radius},
seed::Seed,
};
/// Move the player character based on the keyboard input /// Move the player character based on the keyboard input
pub fn move_player( 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); projection.scale = (projection.scale - scroll.delta.y * scroll_type_multiplier).clamp(0.1, 4.0);
Ok(()) Ok(())
} }
pub fn reset_seed(mut seed: Single<&mut Seed>) {
**seed = Seed::random()
}

View file

@ -6,8 +6,11 @@ use std::{
use bevy::prelude::*; use bevy::prelude::*;
use rand::random; use rand::random;
use crate::net::prelude::{EntityNetworkID, Networked};
/// Value with which to initialize the PRNG /// Value with which to initialize the PRNG
#[derive(Clone, Component, Copy, Debug)] #[derive(Clone, Component, Copy, Debug)]
#[require(EntityNetworkID)]
pub struct Seed(u64); pub struct Seed(u64);
impl Seed { impl Seed {

View file

@ -23,10 +23,8 @@ const BALL_COUNT: u8 = 32;
const BALL_SIZES: Range<f32> = 10.0..25.0; const BALL_SIZES: Range<f32> = 10.0..25.0;
const DIMENSION_SIZES: Range<f32> = 500.0..2000.0; const DIMENSION_SIZES: Range<f32> = 500.0..2000.0;
pub fn check_for_seed(seed: Option<Single<&Seed>>, mut next_state: ResMut<NextState<AppState>>) { pub fn check_for_seed(_add: On<Insert, Seed>, mut next_state: ResMut<NextState<AppState>>) {
if seed.is_some() { next_state.set(AppState::InGame);
next_state.set(AppState::InGame);
}
} }
/// The size of the playable area (x, y) /// The size of the playable area (x, y)

View file

@ -1,17 +1,27 @@
use bevy::prelude::*; use bevy::{
ecs::{component::Mutable, query::QueryFilter},
prelude::*,
};
use uuid::Uuid;
use super::{ use super::{
packet::{InboundPacket, OutboundPacket, Packet}, packet::{InboundPacket, OutboundPacket, Packet},
peer::PeerID, 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 struct PeerOwned;
pub trait Networked: Component + NetworkEncodable + NetworkDecodable {}
impl<T> Networked for T where T: Component + NetworkEncodable + NetworkDecodable {}
pub trait NetworkEncodable { pub trait NetworkEncodable {
fn encode(&self) -> Vec<u8>; fn encode(&self) -> Vec<u8>;
} }
@ -40,45 +50,87 @@ where
} }
} }
fn incoming_network_entity<T: NetworkDecodable + Component>( /// Components wishing to be networked must implement this type
pub trait Networked: Component<Mutability = Mutable> + NetworkEncodable + NetworkDecodable {
type LocalFilter: QueryFilter;
type RemoteFilter: QueryFilter;
}
impl<T> Networked for T
where
T: Component<Mutability = Mutable> + NetworkEncodable + NetworkDecodable,
{
type LocalFilter = Without<PeerOwned>;
type RemoteFilter = With<PeerOwned>;
}
fn incoming_network_entity<
T: NetworkDecodable + Component<Mutability = Mutable>,
F: QueryFilter,
>(
mut inbound: MessageReader<InboundPacket>, mut inbound: MessageReader<InboundPacket>,
mut components: Query<(&mut T, &EntityNetworkID), F>,
mut commands: Commands, 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()) { 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<T: NetworkEncodable + Component>( fn new_peer<T: NetworkEncodable + Component>(
add: On<Add, PeerID>, add: On<Add, PeerID>,
components: Query<&T, Without<PeerOwned>>, components: Query<(&T, &EntityNetworkID), Without<PeerOwned>>,
peers: Query<&PeerID>, peers: Query<&PeerID>,
mut outbound: MessageWriter<OutboundPacket>, mut outbound: MessageWriter<OutboundPacket>,
) -> Result { ) -> Result {
let peer = peers.get(add.entity)?; let peer = peers.get(add.entity)?;
for component in components { for (component, id) in components {
outbound.write(Packet::create(peer.id, component.encode())); outbound.write(Packet::create(peer.id, id.0, component.encode()));
} }
Ok(()) Ok(())
} }
fn new_entity<T: NetworkEncodable + Component>( fn new_local_entity<T: NetworkEncodable + Component>(
add: On<Add, T>, add: On<Add, T>,
components: Query<&T, Without<PeerOwned>>, components: Query<(&T, &EntityNetworkID), Without<PeerOwned>>,
peers: Query<&PeerID>, peers: Query<&PeerID>,
mut outbound: MessageWriter<OutboundPacket>, mut outbound: MessageWriter<OutboundPacket>,
) { ) {
if let Ok(component) = components.get(add.entity) { if let Ok((component, id)) = components.get(add.entity) {
for peer in peers { 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<T: NetworkEncodable + Component, F: QueryFilter>(
components: Query<(&T, &EntityNetworkID), (F, Changed<T>)>,
peers: Query<&PeerID>,
mut outbound: MessageWriter<OutboundPacket>,
) {
for (component, id) in components {
for peer in peers {
outbound.write(Packet::create(peer.id, id.0, component.encode()));
} }
} }
} }
pub fn distribution_plugin<T: Networked>(app: &mut App) { pub fn distribution_plugin<T: Networked>(app: &mut App) {
app.add_systems(FixedUpdate, incoming_network_entity::<T>) app.add_systems(
.add_observer(new_peer::<T>) FixedUpdate,
.add_observer(new_entity::<T>); (
changed_local_entity::<T, T::LocalFilter>,
incoming_network_entity::<T, T::RemoteFilter>,
),
)
.add_observer(new_peer::<T>)
.add_observer(new_local_entity::<T>);
} }

View file

@ -1,13 +1,12 @@
use std::time::Duration; use std::time::Duration;
use bevy::prelude::*; use bevy::prelude::*;
use uuid::Uuid;
use crate::net::peer::PotentialPeers;
use super::{ use super::{
io::{Config, format_message}, io::{Config, format_message},
packet::{OutboundPacket, Packet}, packet::{OutboundPacket, Packet},
peer::{Peer, PeerChangeMessage, PeerID, PeerReceiveTiming, PeerSendTiming}, peer::{Peer, PeerChangeMessage, PeerID, PeerReceiveTiming, PeerSendTiming, PotentialPeers},
queues::NetworkSend, queues::NetworkSend,
}; };
@ -24,7 +23,7 @@ pub fn heartbeat(
if last.time() + PING_FREQUENCY > time.elapsed() { if last.time() + PING_FREQUENCY > time.elapsed() {
continue; continue;
} }
outbound.write(Packet::create(peer.id, Vec::new())); outbound.write(Packet::create(peer.id, Uuid::nil(), Vec::new()));
} }
Ok(()) Ok(())
} }
@ -48,7 +47,7 @@ pub fn ping_potential_peers(
config: Res<Config>, config: Res<Config>,
) -> Result { ) -> Result {
for peer in &peers.addresses { 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(()) Ok(())
} }

View file

@ -18,8 +18,8 @@ impl Default for Config {
} }
} }
pub fn format_message(id: Uuid, data: &Vec<u8>) -> Vec<u8> { pub fn format_message(peer: Uuid, entity: Uuid, data: &Vec<u8>) -> Vec<u8> {
[id.as_bytes(), data.as_slice()].concat() [peer.as_bytes(), entity.as_bytes(), data.as_slice()].concat()
} }
pub fn handle_network_input( pub fn handle_network_input(
@ -33,7 +33,6 @@ pub fn handle_network_input(
for (message, address) in from_socket.iter() { for (message, address) in from_socket.iter() {
match Packet::try_from(message) { match Packet::try_from(message) {
Ok(packet) => { Ok(packet) => {
// TODO: Handle packet variant
if !packet.message.is_empty() { if !packet.message.is_empty() {
to_app.write(packet.clone().into()); to_app.write(packet.clone().into());
} }
@ -63,7 +62,7 @@ pub fn handle_network_output(
for OutboundPacket(packet) in from_app.read() { for OutboundPacket(packet) in from_app.read() {
let peer_id = peer_map.try_get(&packet.peer)?; let peer_id = peer_map.try_get(&packet.peer)?;
let (peer, mut last) = peers.get_mut(*peer_id)?; 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())?; to_socket.send(message, peer.addr.into())?;
last.update(&time); last.update(&time);
} }

View file

@ -11,7 +11,7 @@ mod thread;
#[allow(unused_imports)] #[allow(unused_imports)]
pub mod prelude { pub mod prelude {
pub use super::distribution::{ 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::packet::{InboundPacket, OutboundPacket, Packet};
pub use super::peer::{Peer, PeerID, PeerReceiveTiming, PeerSendTiming, PotentialPeers}; pub use super::peer::{Peer, PeerID, PeerReceiveTiming, PeerSendTiming, PotentialPeers};

View file

@ -7,30 +7,45 @@ pub enum TryFromBytesError {
NotUUID, NotUUID,
} }
pub const UUID_SIZE: usize = 16;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Packet { pub struct Packet {
pub peer: Uuid, pub peer: Uuid,
pub entity: Uuid,
pub message: Vec<u8>, pub message: Vec<u8>,
} }
impl Packet { impl Packet {
pub fn create<T: From<Packet>>(peer: Uuid, message: Vec<u8>) -> T { pub fn create<T: From<Packet>>(peer: Uuid, entity: Uuid, message: Vec<u8>) -> T {
Self { peer, message }.into() Self {
peer,
entity,
message,
}
.into()
} }
} }
pub const UUID_SIZE: usize = 16;
fn extract_uuid(buffer: &mut Vec<u8>) -> std::result::Result<Uuid, TryFromBytesError> {
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<Vec<u8>> for Packet { impl TryFrom<Vec<u8>> for Packet {
type Error = TryFromBytesError; type Error = TryFromBytesError;
fn try_from(mut value: Vec<u8>) -> std::result::Result<Self, Self::Error> { fn try_from(mut value: Vec<u8>) -> std::result::Result<Self, Self::Error> {
if value.len() < UUID_SIZE { let peer = extract_uuid(&mut value)?;
return Err(TryFromBytesError::InsufficientLength); let entity = extract_uuid(&mut value)?;
} Ok(Packet::create(peer, entity, value))
let message = value.split_off(UUID_SIZE);
let uuid = Uuid::from_slice(value.as_slice()).map_err(|_| TryFromBytesError::NotUUID)?;
Ok(Packet::create(uuid, message))
} }
} }

View file

@ -280,7 +280,7 @@ pub fn handle_new_peer(
if change.is_added() { if change.is_added() {
for (_, other, data) in peers { for (_, other, data) in peers {
if peer.id != other.id { 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()));
} }
} }
} }