bevy_sync 0.19.0

Plugin for synchronizing entities and components between server and its clients.
Documentation
use bevy_app::{App, Plugin};
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_connect::events::MessageReceivedEvent;
use bevy_connect::prelude::{Channel, SessionDisconnectCommand, SessionPromoteToHostCommand};
use bevy_connect::{ClientId, SessionPlugin};
use bevy_ecs::component::{Component, ComponentId};
use bevy_ecs::entity::Entity;
use bevy_ecs::message::Messages;
use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::resource::Resource;
use bevy_ecs::system::{Command, Res};
use bevy_ecs::world::{Mut, World};
use bevy_entity_uuid::{EntityLookup, EntityUuidPlugin};
use bevy_image::Image;
use bevy_light::{DirectionalLight, PointLight, SpotLight};
use bevy_math::Mat4;
use bevy_mesh::Mesh;
use bevy_mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes};
use bevy_pbr::{ParallaxMappingMethod, StandardMaterial};
use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::{DynamicTypePath, FromReflect, GetTypeRegistration, ReflectFromReflect};
use bevy_reflect::{Reflect, TypePath};
use bevy_state::app::AppExtStates;
use std::any::TypeId;
use std::collections::{HashMap, HashSet};
use tracing::{debug, warn};
use uuid::Uuid;

use crate::networking::assets::{AssetMessage, SyncAssetPlugin};
use crate::proto::Message;
use crate::track::SyncTrackPlugin;
use crate::{
    ClientState, InitialSyncFinished, ServerState, SyncComponent, SyncExclude, SyncMark,
    SyncPlugin, bundle_fix::BundleFixPlugin, client::ClientSyncPlugin,
    handle_uuid_fix::HandleUUIDFixPlugin, server::ServerSyncPlugin,
    track::setup_tracking_of_component,
};

use crate::{ConnectCommand, DisconnectCommand, PromoteToHostCommand, SyncWorld};

#[allow(clippy::struct_excessive_bools)]
#[derive(Resource, Default)]
pub(crate) struct SyncTrackerRes {
    pub(crate) registered_componets_for_sync: HashSet<ComponentId>,
    /// Tracks [`SyncExclude`] for component T. key: component id of T, value: component id of [`SyncExclude<T>`]
    pub(crate) sync_exclude_cid_of_component_cid: HashMap<ComponentId, ComponentId>,
    pub(crate) pushed_handles_from_network: HashSet<Uuid>,

    pub(crate) sync_materials: bool,
    pub(crate) sync_meshes: bool,
    pub(crate) sync_audios: bool,

    pub(crate) host_promotion_in_progress: bool,
}

#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_material_enabled(tracker: Res<SyncTrackerRes>) -> bool {
    tracker.sync_materials
}

#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_mesh_enabled(tracker: Res<SyncTrackerRes>) -> bool {
    tracker.sync_meshes
}

#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_audio_enabled(tracker: Res<SyncTrackerRes>) -> bool {
    tracker.sync_audios
}

impl SyncTrackerRes {
    pub(crate) fn skip_network_handle_change(&mut self, id: Uuid) -> bool {
        if self.pushed_handles_from_network.contains(&id) {
            debug!(
                "Debouncing network handle change, was already pushed. {:?}",
                id
            );
            self.pushed_handles_from_network.remove(&id);
            return true;
        }
        false
    }

    pub(crate) fn to_skinned_mapper(
        assets: &Assets<SkinnedMeshInverseBindposes>,
        entity_map: &EntityLookup,
        component: &SkinnedMesh,
    ) -> SkinnedMeshSyncMapper {
        let mut joints_uuid = Vec::<Uuid>::new();
        for e in &component.joints {
            if let Some(uuid) = entity_map.entity_to_uuid(e) {
                joints_uuid.push(uuid);
            }
        }
        let poses = assets.get(component.inverse_bindposes.id()).unwrap();
        let poses = (**poses).to_vec();
        SkinnedMeshSyncMapper {
            inverse_bindposes: poses,
            joints: joints_uuid,
        }
    }

    pub(crate) fn to_skinned_mesh(world: &mut World, mapper: SkinnedMeshSyncMapper) -> SkinnedMesh {
        world.resource_scope(
            |world, mut assets: Mut<Assets<SkinnedMeshInverseBindposes>>| {
                let entity_map = world.resource::<EntityLookup>();
                let mut joints = Vec::<Entity>::new();
                for uuid in &mapper.joints {
                    if let Some(e) = entity_map.uuid_to_entity(uuid) {
                        joints.push(e);
                    }
                }
                let poses = mapper.inverse_bindposes;
                let poses: SkinnedMeshInverseBindposes = poses.into();
                let handle = assets.add(poses);
                SkinnedMesh {
                    inverse_bindposes: handle,
                    joints,
                }
            },
        )
    }
}

impl SyncComponent for App {
    fn sync_component<T>(&mut self) -> &mut Self
    where
        T: Component
            + TypePath
            + DynamicTypePath
            + Reflect
            + FromReflect
            + GetTypeRegistration
            + Clone,
    {
        // application may try to setup sync without knowing if bevy_sync was enabled.
        if self.world().get_resource::<SyncTrackerRes>().is_none() {
            warn!("Trying to register sync_component in bevy_sync, but bevy_sync is not enabled.");
            return self;
        }

        self.register_type::<T>();
        self.register_type_data::<T, ReflectFromReflect>();
        let c_id = self.world_mut().register_component::<T>();
        let c_exclude_id = self.world_mut().register_component::<SyncExclude<T>>();
        let mut track = self.world_mut().resource_mut::<SyncTrackerRes>();
        track.registered_componets_for_sync.insert(c_id);
        track
            .sync_exclude_cid_of_component_cid
            .insert(c_id, c_exclude_id);
        setup_tracking_of_component::<T>(self);
        setup_cascade_registrations::<T>(self);

        self
    }

    fn sync_materials(&mut self, enable: bool) {
        let mut tracker = self.world_mut().resource_mut::<SyncTrackerRes>();
        tracker.sync_materials = enable;
    }

    fn sync_meshes(&mut self, enable: bool) {
        let mut tracker = self.world_mut().resource_mut::<SyncTrackerRes>();
        tracker.sync_meshes = enable;
    }

    fn sync_audios(&mut self, enable: bool) {
        let mut tracker = self.world_mut().resource_mut::<SyncTrackerRes>();
        tracker.sync_audios = enable;
    }
}

#[derive(Component, Debug, Clone, Reflect, Default)]
#[reflect(Component, Default)]
pub(crate) struct SkinnedMeshSyncMapper {
    pub inverse_bindposes: Vec<Mat4>,
    pub joints: Vec<Uuid>,
}

fn setup_cascade_registrations<T: Component + Reflect + FromReflect + GetTypeRegistration>(
    app: &mut App,
) {
    if TypeId::of::<T>() == TypeId::of::<SkinnedMesh>() {
        app.register_type::<SkinnedMeshSyncMapper>();
    }

    if TypeId::of::<T>() == TypeId::of::<Mesh>() {
        app.register_type::<Image>();
        app.register_type::<Handle<Image>>();
    }

    if TypeId::of::<T>() == TypeId::of::<Handle<StandardMaterial>>() {
        app.register_type_data::<StandardMaterial, ReflectFromReflect>();
        app.register_type::<Color>();
        app.register_type::<Image>();
        app.register_type::<Handle<Image>>();
        app.register_type::<Option<Handle<Image>>>();
        app.register_type::<ParallaxMappingMethod>();
    }

    if TypeId::of::<T>() == TypeId::of::<PointLight>() {
        app.register_type::<Color>();
    }
    if TypeId::of::<T>() == TypeId::of::<SpotLight>() {
        app.register_type::<Color>();
    }
    if TypeId::of::<T>() == TypeId::of::<DirectionalLight>() {
        app.register_type::<Color>();
    }
}

impl Plugin for SyncPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(EntityUuidPlugin);

        app.add_plugins(SessionPlugin::<Message>::default());
        app.add_plugins(SessionPlugin::<AssetMessage>::default());

        app.add_message::<InitialSyncFinished>();
        app.register_type::<SyncMark>();
        app.init_resource::<SyncTrackerRes>();
        app.init_state::<ServerState>();
        app.init_state::<ClientState>();

        app.add_plugins(ServerSyncPlugin);
        app.add_plugins(ClientSyncPlugin);
        app.add_plugins(SyncTrackPlugin);
        app.add_plugins(SyncAssetPlugin);
        app.add_plugins(BundleFixPlugin);
        app.add_plugins(HandleUUIDFixPlugin);

        app.add_plugins(crate::metrics::MetricsPlugin);
    }
}

impl Command for ConnectCommand {
    type Out = ();

    fn apply(self, world: &mut World) {
        world.insert_resource(self.config.clone());
        if self.is_host {
            crate::networking::setup_server(&mut world.commands(), &self.config.clone());
        } else {
            crate::networking::setup_client(&mut world.commands(), &self.config.clone());
        }
    }
}

impl Command for DisconnectCommand {
    type Out = ();

    fn apply(self, world: &mut World) {
        world
            .commands()
            .queue(SessionDisconnectCommand::<Message>::default());
    }
}

impl Command for PromoteToHostCommand {
    type Out = ();

    fn apply(self, world: &mut World) {
        let promotion = SessionPromoteToHostCommand::<Message>::new(self.0, self.1);
        world.commands().queue(promotion);
    }
}

impl SyncWorld for &World {
    fn sync_host_uuid(&self) -> Option<ClientId> {
        self.resource::<Channel<Message>>().host_uuid()
    }

    fn sync_self_uuid(&self) -> ClientId {
        self.resource::<Channel<Message>>().uuid()
    }

    fn sync_all_client_uuids(&self) -> HashSet<ClientId> {
        self.resource::<Channel<Message>>().destinations()
    }

    fn events_queue_count(&self) -> usize {
        let events = self.resource::<Messages<MessageReceivedEvent<Message>>>();
        events.len()
    }
}