~starkingdoms/starkingdoms

13ff574f3067094b9c252b6353fa0df5d6317624 — core 6 days ago 1bcde11
netcode: unreliable updates
M Cargo.lock => Cargo.lock +7 -0
@@ 5570,6 5570,12 @@ dependencies = [
]

[[package]]
name = "smolvec"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b43855ed42c16647e6f7d19fdef4b020529c822f77a5ab417eb2278e11756da7"

[[package]]
name = "socket2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 5653,6 5659,7 @@ dependencies = [
 "postcard",
 "rand 0.10.1",
 "serde",
 "smolvec",
 "tracing-subscriber",
 "tracing-web",
 "wasm-bindgen",

M Cargo.toml => Cargo.toml +1 -0
@@ 68,6 68,7 @@ aeronet_transport = { version = "0.20" }
aeronet_replicon = { version = "0.20" }
bevy_replicon = { version = "0.39"}
postcard = { version = "1.1.3", features = ["alloc"] }
smolvec = { version = "1" }

[profile.dev]
opt-level = 1

M crates/unified/Cargo.toml => crates/unified/Cargo.toml +1 -0
@@ 20,6 20,7 @@ web-time = { workspace = true }
aeronet = { workspace = true }
aeronet_transport = { workspace = true }
postcard = { workspace = true }
smolvec = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ctrlc = { workspace = true, optional = true }

M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +5 -11
@@ 22,7 22,7 @@ use crate::client::components::Me;
use crate::client::ship::attachment::client_attachment_plugin;
use crate::shared::ecs::{GameplayState, TimeOffset};
use crate::shared::gravity::update_gravity;
use crate::shared::net::{Hi, ClientMessageRegistry, ServerMessageRegistry, ServerEntityMap};
use crate::shared::net::{Hi, ClientMessageRegistry, ServerMessageRegistry, ServerEntityMap, TransportLanes};
use crate::shared::orbit::OrbitPlugin;

pub mod colors;


@@ 104,8 104,7 @@ pub fn on_connected(
    trigger: On<Add, Session>,
    names: Query<&Name>,
    sessions: Query<&Session>,
    server_message_registry: Res<ServerMessageRegistry>,
    client_message_registry: Res<ClientMessageRegistry>,
    lanes: Res<TransportLanes>,
    mut commands: Commands,
) {
    let entity = trigger.event_target();


@@ 114,15 113,10 @@ pub fn on_connected(
        return;
    };

    let server_message_count = server_message_registry.message_count();
    let client_message_count = client_message_registry.message_count();
    let message_count = server_message_count + client_message_count;
    debug!("message count: {}", message_count);
    let lanes = [LaneKind::ReliableOrdered].repeat(message_count);
    let transport = Transport::new(
        session,
        lanes.clone(),
        lanes,
        lanes.lanes.iter().map(|u| u.clone()),
        lanes.lanes.iter().map(|u| u.clone()),
        bevy::platform::time::Instant::now(),
    ).expect("packet MTU too small to support transport");
    commands.entity(entity).insert(transport);


@@ 147,7 141,7 @@ pub fn on_disconnected(trigger: On<Disconnected>, names: Query<&Name>) {
}

#[derive(Resource)]
struct HeartyEntityId(Option<Entity>);
pub struct HeartyEntityId(Option<Entity>);

pub fn handle_hi(
    mut msgs: MessageReader<Hi>,

M crates/unified/src/client/net/incoming_parts.rs => crates/unified/src/client/net/incoming_parts.rs +22 -25
@@ 2,39 2,36 @@ use bevy::log::{debug, warn};
use bevy::prelude::{Commands, MessageReader, ResMut, Transform};
use crate::prelude::Query;
use crate::shared::ecs::{Part, Temperature};
use crate::shared::net::part::PartUpdatePacket;
use crate::shared::net::part::{PartDto};
use crate::shared::net::ServerEntityMap;

pub fn handle_incoming_parts(
    mut msgs: MessageReader<PartUpdatePacket>,
    mut msgs: MessageReader<PartDto>,

    mut q_parts: Query<(&mut Part, &mut Transform, &mut Temperature)>,
    mut entity_map: ResMut<ServerEntityMap>,
    mut commands: Commands
) {
    for msg in msgs.read() {
        'to_next_part: for part in &msg.updated_parts {
            if let Some(local_entity) = entity_map.server_to_client.get(&part.server_entity) {
                let Ok((mut part_data, mut transform, mut temperature)) = q_parts.get_mut(*local_entity) else {
                    warn!("local part entity {:?} for part srv:{:?} doesn't exist? skipping update, this is a bug", local_entity, part.server_entity);
                    continue 'to_next_part
                };
                if part.part_data_changed {
                    *part_data = part.part.clone();
                }
                *transform = part.transform;
                *temperature = part.temperature;
            } else {
                // Spawn new part
                let e = commands.spawn((
                    part.part.clone(),
                    part.transform,
                    part.temperature,
                )).id();
                entity_map.server_to_client.insert(part.server_entity, e.clone());
                entity_map.client_to_server.insert(e.clone(), part.server_entity);
                debug!(?part.part, "spawned new part");
            }
    for part in msgs.read() {
        if let Some(local_entity) = entity_map.server_to_client.get(&part.server_entity) {
            let Ok((mut part_data, mut transform, mut temperature)) = q_parts.get_mut(*local_entity) else {
                warn!("local part entity {:?} for part srv:{:?} doesn't exist? skipping update, this is a bug", local_entity, part.server_entity);
                continue;
            };
            part.part.update(part_data);
            part.transform.update(transform);
            part.temperature.update(temperature);
        } else {
            // Spawn new part
            let e = commands.spawn((
                part.part.data().clone(),
                *part.transform.data(),
                *part.temperature.data(),
            )).id();
            entity_map.server_to_client.insert(part.server_entity, e.clone());
            entity_map.client_to_server.insert(e.clone(), part.server_entity);
            debug!(?part.part, "spawned new part");
        }

    }
}
\ No newline at end of file

M crates/unified/src/client/net/incoming_planets.rs => crates/unified/src/client/net/incoming_planets.rs +20 -23
@@ 2,37 2,34 @@ use bevy::log::{debug, warn};
use bevy::prelude::{Commands, MessageReader, ResMut, Transform};
use crate::prelude::Query;
use crate::shared::config::planet::Planet;
use crate::shared::net::planet::PlanetUpdatePacket;
use crate::shared::net::planet::{PlanetDto};
use crate::shared::net::ServerEntityMap;

pub fn handle_incoming_planets(
    mut msgs: MessageReader<PlanetUpdatePacket>,
    mut msgs: MessageReader<PlanetDto>,

    mut q_planets: Query<(&mut Planet, &mut Transform)>,
    mut entity_map: ResMut<ServerEntityMap>,
    mut commands: Commands
) {
    for msg in msgs.read() {
        'to_next_planet: for planet in &msg.updated_planets {
            if let Some(local_entity) = entity_map.server_to_client.get(&planet.server_entity) {
                let Ok((mut planet_data, mut transform)) = q_planets.get_mut(*local_entity) else {
                    warn!("local planet entity {:?} for planet srv:{:?} doesn't exist? skipping update, this is a bug", local_entity, planet.server_entity);
                    continue 'to_next_planet
                };
                if planet.planet_data_changed {
                    *planet_data = planet.planet.clone();
                }
                *transform = planet.transform;
            } else {
                // Spawn new planet
                let e = commands.spawn((
                    planet.planet.clone(),
                    planet.transform
                )).id();
                entity_map.server_to_client.insert(planet.server_entity, e.clone());
                entity_map.client_to_server.insert(e.clone(), planet.server_entity);
                debug!(?planet.planet, "spawned new planet");
            }
    for planet in msgs.read() {
        if let Some(local_entity) = entity_map.server_to_client.get(&planet.server_entity) {
            let Ok((mut planet_data, mut transform)) = q_planets.get_mut(*local_entity) else {
                warn!("local planet entity {:?} for planet srv:{:?} doesn't exist? skipping update, this is a bug", local_entity, planet.server_entity);
                continue;
            };
            planet.planet.update(planet_data);
            planet.transform.update(transform);
        } else {
            // Spawn new planet
            let e = commands.spawn((
                planet.planet.data().clone(),
                *planet.transform.data(),
            )).id();
            entity_map.server_to_client.insert(planet.server_entity, e.clone());
            entity_map.client_to_server.insert(e.clone(), planet.server_entity);
            debug!(?planet.planet, "spawned new planet");
        }

    }
}
\ No newline at end of file

M crates/unified/src/server/mod.rs => crates/unified/src/server/mod.rs +4 -11
@@ 37,7 37,7 @@ use crate::server::system_sets::{PlayerInputSet, WorldUpdateSet};
use crate::prelude::*;
use crate::server::orbit::OrbitPlugin;
use crate::server::player::thrust::server_thrust_plugin;
use crate::shared::net::{ClientMessageRegistry, ServerMessageRegistry};
use crate::shared::net::{ClientMessageRegistry, ServerMessageRegistry, TransportLanes};

pub struct ServerPlugin {
    pub bind: SocketAddr


@@ 95,8 95,7 @@ fn on_connected(
    trigger: On<Add, Session>,
    clients: Query<&ChildOf>,
    sessions: Query<&Session>,
    server_message_registry: Res<ServerMessageRegistry>,
    client_message_registry: Res<ClientMessageRegistry>,
    lanes: Res<TransportLanes>,
    mut commands: Commands,
) {
    let client = trigger.event_target();


@@ 112,16 111,10 @@ fn on_connected(
            network_entity: client,
        }))
        .id();

    let server_message_count = server_message_registry.message_count();
    let client_message_count = client_message_registry.message_count();
    let message_count = server_message_count + client_message_count;
    debug!("message count: {}", message_count);
    let lanes = [LaneKind::ReliableOrdered].repeat(message_count);
    let transport = Transport::new(
        session,
        lanes.clone(),
        lanes,
        lanes.lanes.iter().map(|u| u.clone()),
        lanes.lanes.iter().map(|u| u.clone()),
        bevy::platform::time::Instant::now(),
    ).expect("packet MTU too small to support transport");
    commands.entity(client).insert((

M crates/unified/src/server/net/update_parts.rs => crates/unified/src/server/net/update_parts.rs +13 -65
@@ 1,75 1,23 @@
use std::collections::BTreeSet;
use bevy::prelude::{Added, Changed, Commands, MessageWriter, Query};
use bevy::prelude::{Added, Changed, Commands, MessageWriter, Query, Ref};
use crate::prelude::{debug, Entity, ParamSet, Transform};
use crate::shared::ecs::{Part, Temperature};
use crate::shared::net::part::{PartUpdatePacket, PartDto};
use crate::shared::net::part::{PartDto};
use crate::shared::net::staged_transform::LastStagedTransform;
use crate::shared::net::{SendTargets, ToClients};

pub fn send_updated_parts(
    mut set: ParamSet<(
        // If you need to send more data, don't add it here...
        Query<(Entity, &Transform), Added<Part>>,
        Query<(Entity, &Transform, &LastStagedTransform), Changed<Transform>>,
        Query<Entity, Changed<Part>>,

        // add it here.
        Query<(&Part, &Transform, &Temperature, &mut LastStagedTransform)>,
    )>,

    mut update_packets_out: MessageWriter<ToClients<PartUpdatePacket>>,
    mut commands: Commands
    parts: Query<(Entity, Ref<Part>, Ref<Transform>, Ref<Temperature>)>,
    mut messages: MessageWriter<ToClients<PartDto>>
) {
    let start = bevy::platform::time::Instant::now();
    let mut parts_to_send: BTreeSet<Entity> = BTreeSet::new();

    // Updating the part behavior triggers sprite loading on the client. We don't want to send it every time the Transform changes
    let mut parts_with_part_change: BTreeSet<Entity> = BTreeSet::new();

    for (new_part, transform) in set.p0().iter() {
        parts_to_send.insert(new_part); // Always send newly created parts...
        // ...and add LastStagedTransform...
        commands.entity(new_part).insert(LastStagedTransform(*transform));
        // and put a marker, we need to copy part data across
        parts_with_part_change.insert(new_part);
    }
    for (moved_part, current_transform, maybe_staged_transform) in set.p1().iter() {
        // have we had a significant transform change?
        if !maybe_staged_transform.should_update(current_transform) { continue; }
        // we have, resend
        parts_to_send.insert(moved_part);
    }
    for changed_part in set.p2().iter() {
        // if part changed, always resend
        parts_to_send.insert(changed_part);
        // and put a marker, we need to copy part data across
        parts_with_part_change.insert(changed_part);
    }

    // process all parts to send
    let mut packet = PartUpdatePacket {
        updated_parts: vec![]
    };

    for part_to_send in parts_to_send.iter() {
        let mut q = set.p3();
        let Ok(mut part_info) = q.get_mut(*part_to_send) else { continue };

        *part_info.3 = LastStagedTransform(*part_info.1);

        packet.updated_parts.push(PartDto {
            server_entity: *part_to_send,
            part: part_info.0.clone(),
            part_data_changed: parts_with_part_change.contains(part_to_send),
            transform: *part_info.1,
            temperature: *part_info.2
    for part in parts {
        messages.write(ToClients {
            message: PartDto {
                server_entity: part.0,
                part: part.1.into(),
                transform: part.2.into(),
                temperature: part.3.into()
            },
            targets: SendTargets::All
        });

    }

    update_packets_out.write(ToClients {
        message: packet,
        targets: SendTargets::All,
    });

}
\ No newline at end of file

M crates/unified/src/server/net/update_planets.rs => crates/unified/src/server/net/update_planets.rs +14 -60
@@ 1,71 1,25 @@
use std::collections::BTreeSet;
use bevy::prelude::{Added, Changed, Commands, MessageWriter, Query};
use crate::prelude::{Entity, ParamSet, Transform};
use crate::prelude::{Entity, ParamSet, Ref, Transform};
use crate::shared::config::planet::Planet;
use crate::shared::net::planet::{PlanetUpdatePacket, PlanetDto};
use crate::shared::ecs::{Part, Temperature};
use crate::shared::net::planet::PlanetDto;
use crate::shared::net::staged_transform::LastStagedTransform;
use crate::shared::net::{SendTargets, ToClients};
use crate::shared::net::part::PartDto;

pub fn send_updated_planets(
    mut set: ParamSet<(
        // If you need to send more data, don't add it here...
        Query<(Entity, &Transform), Added<Planet>>,
        Query<(Entity, &Transform, &LastStagedTransform), Changed<Transform>>,
        Query<Entity, Changed<Planet>>,

        // add it here.
        Query<(&Planet, &Transform, &mut LastStagedTransform)>,
    )>,

    mut update_packets_out: MessageWriter<ToClients<PlanetUpdatePacket>>,
    mut commands: Commands
    planets: Query<(Entity, Ref<Planet>, Ref<Transform>, Ref<Temperature>)>,
    mut messages: MessageWriter<ToClients<PlanetDto>>
) {
    let mut planets_to_send: BTreeSet<Entity> = BTreeSet::new();

    // Updating the Planet behavior triggers sprite loading on the client. We don't want to send it every time the Transform changes
    let mut planets_with_planet_change: BTreeSet<Entity> = BTreeSet::new();

    for (new_planet, transform) in set.p0().iter() {
        planets_to_send.insert(new_planet); // Always send newly created planets...
        // ...and add LastStagedTransform...
        commands.entity(new_planet).insert(LastStagedTransform(*transform));
        // and put a marker, we need to copy planet data across
        planets_with_planet_change.insert(new_planet);
    }
    for (moved_planet, current_transform, maybe_staged_transform) in set.p1().iter() {
        // have we had a significant transform change?
        if !maybe_staged_transform.should_update(current_transform) { continue; }
        // we have, resend
        planets_to_send.insert(moved_planet);
    }
    for changed_planet in set.p2().iter() {
        // if Planet changed, always resend
        planets_to_send.insert(changed_planet);
        // and put a marker, we need to copy planet data across
        planets_with_planet_change.insert(changed_planet);
    }

    // process all planets to send
    let mut packet = PlanetUpdatePacket {
        updated_planets: vec![]
    };

    for planet_to_send in planets_to_send.iter() {
        let mut p3 = set.p3();
        let Ok(mut planet_info) = p3.get_mut(*planet_to_send) else { continue };

        *planet_info.2 = LastStagedTransform(*planet_info.1);

        packet.updated_planets.push(PlanetDto {
            server_entity: *planet_to_send,
            planet: planet_info.0.clone(),
            planet_data_changed: planets_with_planet_change.contains(planet_to_send),
            transform: *planet_info.1,
    for planet in planets {
        messages.write(ToClients {
            message: PlanetDto {
                server_entity: planet.0,
                planet: planet.1.into(),
                transform: planet.2.into(),
            },
            targets: SendTargets::All
        });
    }

    update_packets_out.write(ToClients {
        message: packet,
        targets: SendTargets::All,
    });
}
\ No newline at end of file

M crates/unified/src/shared/net.rs => crates/unified/src/shared/net.rs +32 -30
@@ 1,6 1,7 @@
pub mod planet;
pub mod staged_transform;
pub mod part;
pub mod was_updated;

use std::any::TypeId;
use std::collections::HashMap;


@@ 9,7 10,7 @@ use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::time::Instant;

use aeronet_transport::Transport;
use aeronet_transport::lane::LaneIndex;
use aeronet_transport::lane::{LaneIndex, LaneKind};
use aeronet_websocket::tungstenite::Bytes;
use avian2d::prelude::{AngularInertia, AngularVelocity, Collider, LinearVelocity, Mass, Position, Rotation};
use bevy::ecs::entity::{EntityHashMap, MapEntities};


@@ 20,24 21,25 @@ use postcard::{from_bytes, to_allocvec, to_slice, to_vec};
use crate::prelude::{App, Message};
use crate::shared::thrust::ThrustSolution;
use serde::{Deserialize, Serialize};
use smolvec::SmolVec;
use crate::shared::attachment::{Joint, JointOf, PartInShip, Peer, Ship, SnapOf, SnapOfJoint};
use crate::shared::config::planet::{Planet, PlanetSpring, PlanetSpringJoint};
use crate::shared::ecs::{CanCraft, CraftPartRequest, DragRequestEvent, Drill, Part, Player, PlayerStorage, SingleStorage, Temperature, ToggleDrillEvent};
use crate::shared::ecs::thruster::{Thruster, ThrusterOfPart};
use crate::shared::net::part::PartUpdatePacket;
use crate::shared::net::planet::PlanetUpdatePacket;
use crate::shared::net::part::PartDto;
use crate::shared::net::planet::PlanetDto;

pub fn register_net(app: &mut App) {
    app
        .add_server_message::<Hi>()
        .add_server_message::<Hi>(LaneKind::ReliableOrdered)

        .add_server_message::<PlanetUpdatePacket>()
        .add_server_message::<PartUpdatePacket>()
        .add_server_message::<PlanetDto>(LaneKind::UnreliableSequenced)
        .add_server_message::<PartDto>(LaneKind::UnreliableSequenced)

        .add_client_message::<DragRequestEvent>()
        .add_client_message::<ToggleDrillEvent>()
        .add_client_message::<CraftPartRequest>()
        .add_client_message::<ThrustSolution>()
        .add_client_message::<DragRequestEvent>(LaneKind::ReliableOrdered)
        .add_client_message::<ToggleDrillEvent>(LaneKind::ReliableOrdered)
        .add_client_message::<CraftPartRequest>(LaneKind::ReliableOrdered)
        .add_client_message::<ThrustSolution>(LaneKind::ReliableOrdered)

        .add_systems(PostStartup, |srv_msg_reg: Res<ServerMessageRegistry>, client_msg_reg: Res<ClientMessageRegistry>| {
            debug!("network registration complete");


@@ 56,6 58,7 @@ pub struct Hi {
pub fn setup_net(app: &mut App) {
    app.insert_resource(ServerMessageRegistry::default());
    app.insert_resource(ClientMessageRegistry::default());
    app.insert_resource(TransportLanes::default());
    app.add_systems(Update, recv_from_server);
    app.add_systems(Update, recv_from_client);
}


@@ 85,6 88,11 @@ pub enum ClientId {
}

#[derive(Resource, Default)]
pub struct TransportLanes {
    pub lanes: Vec<LaneKind>
}

#[derive(Resource, Default)]
pub struct ServerEntityMap {
    pub server_to_client: EntityHashMap<Entity>,
    pub client_to_server: EntityHashMap<Entity>,


@@ 94,21 102,11 @@ pub struct ServerMessageRegistry {
    forward: HashMap<TypeId, LaneIndex>,
    reverse: HashMap<LaneIndex, fn(Vec<u8>, &mut World)>,
}
impl ServerMessageRegistry {
    pub fn message_count(&self) -> usize {
        self.forward.len()
    }
}
#[derive(Resource, Default, Debug)]
pub struct ClientMessageRegistry {
    forward: HashMap<TypeId, LaneIndex>,
    reverse: HashMap<LaneIndex, fn(Vec<u8>, ClientId, &mut World)>,
}
impl ClientMessageRegistry {
    pub fn message_count(&self) -> usize {
        self.forward.len()
    }
}

static COUNTER: AtomicU32 = AtomicU32::new(0);
fn get_lane_index() -> LaneIndex {


@@ 116,39 114,43 @@ fn get_lane_index() -> LaneIndex {
}

pub trait NetAppExt {
    fn add_mapped_server_message<T: Message + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self;
    fn add_server_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self;
    fn add_mapped_client_message<T: Message + Clone + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self;
    fn add_client_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self;
    fn add_mapped_server_message<T: Message + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self;
    fn add_server_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self;
    fn add_mapped_client_message<T: Message + Clone + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self;
    fn add_client_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self;
}
impl NetAppExt for App {
    fn add_mapped_server_message<T: Message + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self {
    fn add_mapped_server_message<T: Message + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self {
        let mut registry = self.world_mut().resource_mut::<ServerMessageRegistry>();
        register_mapped_server_message::<T>(&mut registry);
        self.world_mut().resource_mut::<TransportLanes>().lanes.push(kind);
        self
            .add_message::<ToClients<T>>()
            .add_message::<T>()
            .add_systems(Update, send_to_client::<T>)
    }
    fn add_server_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self {
    fn add_server_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self {
        let mut registry = self.world_mut().resource_mut::<ServerMessageRegistry>();
        register_server_message::<T>(&mut registry);
        self.world_mut().resource_mut::<TransportLanes>().lanes.push(kind);
        self
            .add_message::<ToClients<T>>()
            .add_message::<T>()
            .add_systems(Update, send_to_client::<T>)
    }
    fn add_mapped_client_message<T: Message + Clone + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self {
    fn add_mapped_client_message<T: Message + Clone + MapEntities + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self {
        let mut registry = self.world_mut().resource_mut::<ClientMessageRegistry>();
        register_client_message::<T>(&mut registry);
        self.world_mut().resource_mut::<TransportLanes>().lanes.push(kind);
        self
            .add_message::<FromClients<T>>()
            .add_message::<T>()
            .add_systems(Update, send_mapped_to_server::<T>)
    }
    fn add_client_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self) -> &mut Self {
    fn add_client_message<T: Message + TypePath + Serialize + for<'a> Deserialize<'a>>(&mut self, kind: LaneKind) -> &mut Self {
        let mut registry = self.world_mut().resource_mut::<ClientMessageRegistry>();
        register_client_message::<T>(&mut registry);
        self.world_mut().resource_mut::<TransportLanes>().lanes.push(kind);
        self
            .add_message::<FromClients<T>>()
            .add_message::<T>()


@@ 260,7 262,7 @@ fn recv_from_server(
        Res<ServerMessageRegistry>,
    )> = SystemState::new(world);
    let (mut sessions, message_registry) = system_state.get_mut(world);
    let mut messages = Vec::new();
    let mut messages = SmolVec::new();
    for mut transport in sessions.iter_mut() {
        for message in transport.recv.msgs.drain() {
            let payload = message.payload;


@@ 283,7 285,7 @@ fn recv_from_client(
        Res<ClientMessageRegistry>,
    )> = SystemState::new(world);
    let (mut clients, message_registry) = system_state.get_mut(world);
    let mut messages = Vec::new();
    let mut messages = SmolVec::new();
    for (client_entity, mut transport) in clients.iter_mut() {
        for message in transport.recv.msgs.drain() {
            let payload = message.payload;

M crates/unified/src/shared/net/part.rs => crates/unified/src/shared/net/part.rs +5 -11
@@ 2,19 2,13 @@ use bevy::prelude::{Entity, Message, Transform, TypePath};
use serde::{Deserialize, Serialize};
use crate::shared::config::planet::Planet;
use crate::shared::ecs::{Part, Temperature};
use crate::shared::net::was_updated::WasUpdated;

#[derive(Serialize, Deserialize, Message, TypePath, Debug, Clone)]
pub struct PartUpdatePacket {
    pub updated_parts: Vec<PartDto>
}

#[derive(Serialize, Deserialize, TypePath, Debug, Clone)]
#[derive(Serialize, Deserialize, TypePath, Debug, Clone, Message)]
pub struct PartDto {
    pub server_entity: Entity,

    pub part: Part,
    pub part_data_changed: bool,

    pub transform: Transform,
    pub temperature: Temperature,
    pub part: WasUpdated<Part>,
    pub transform: WasUpdated<Transform>,
    pub temperature: WasUpdated<Temperature>,
}
\ No newline at end of file

M crates/unified/src/shared/net/planet.rs => crates/unified/src/shared/net/planet.rs +4 -10
@@ 1,18 1,12 @@
use bevy::prelude::{Entity, Message, Transform, TypePath};
use serde::{Deserialize, Serialize};
use crate::shared::config::planet::Planet;
use crate::shared::net::was_updated::WasUpdated;

#[derive(Serialize, Deserialize, Message, TypePath, Debug, Clone)]
pub struct PlanetUpdatePacket {
    pub updated_planets: Vec<PlanetDto>
}

#[derive(Serialize, Deserialize, TypePath, Debug, Clone)]
#[derive(Serialize, Deserialize, TypePath, Debug, Clone, Message)]
pub struct PlanetDto {
    pub server_entity: Entity,

    pub planet: Planet,
    pub planet_data_changed: bool,

    pub transform: Transform
    pub planet: WasUpdated<Planet>,
    pub transform: WasUpdated<Transform>
}
\ No newline at end of file

A crates/unified/src/shared/net/was_updated.rs => crates/unified/src/shared/net/was_updated.rs +24 -0
@@ 0,0 1,24 @@
use std::fmt::Debug;
use bevy::prelude::{Mut, Ref};
use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned;
use crate::prelude::DetectChanges;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WasUpdated<T: Clone + Serialize + Debug>(T, bool);
impl<T: Clone + Serialize + DeserializeOwned + Debug> From<Ref<'_, T>> for WasUpdated<T> {
    fn from(value: Ref<T>) -> Self {
        let changed = value.is_changed();
        Self(value.into_inner().clone(), changed)
    }
}
impl<T: Clone + Serialize + DeserializeOwned + Debug> WasUpdated<T> {
    pub fn update(&self, mut data: Mut<T>) {
        if self.1 {
            *data = self.0.clone();
        }
    }
    pub fn data(&self) -> &T {
        &self.0
    }
}
\ No newline at end of file