use bevy_app::{App, Plugin, FixedUpdate};
use bevy_asset::{AssetEvent, AssetId, Assets};
use bevy_audio::AudioSource;
use bevy_connect::prelude::Channel;
use bevy_ecs::entity::Entity;
use bevy_ecs::hierarchy::{ChildOf, Children};
use bevy_ecs::message::MessageReader;
use bevy_ecs::name::Name;
use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::query::{Added, Changed, With, Without};
use bevy_ecs::reflect::AppTypeRegistry;
use bevy_ecs::schedule::IntoScheduleConfigs;
use bevy_ecs::schedule::SystemCondition;
use bevy_ecs::system::{Commands, Query, Res, ResMut};
use bevy_ecs::world::World;
use bevy_ecs::{component::Component, schedule::common_conditions::resource_exists};
use bevy_entity_uuid::{EntityLookup, EntityUuid, EntityUuidDeleted};
use bevy_image::Image;
use bevy_mesh::Mesh;
use bevy_mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes};
use bevy_pbr::StandardMaterial;
use bevy_reflect::PartialReflect;
use bevy_reflect::Reflect;
use bevy_state::condition::in_state;
use std::any::{TypeId, type_name};
use tracing::debug;
use uuid::Uuid;
use crate::{
ClientState, ServerState, SyncEntity, SyncExclude, SyncMark,
binreflect::reflect_to_bin,
lib_priv::{SyncTrackerRes, sync_audio_enabled, sync_material_enabled, sync_mesh_enabled},
networking::{
assets::{SendAudioCommand, SendImageCommand, SendMeshCommand},
send_message_world,
},
proto::Message,
};
#[derive(PartialEq, Eq, Hash)]
pub(crate) struct ComponentChangeId {
pub(crate) uuid: Uuid,
pub(crate) name: String,
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub(crate) struct LastValueOf<T: Component + Reflect>(T);
pub(crate) fn setup_tracking_of_component<T>(app: &mut App)
where
T: Component + Reflect + Clone,
{
if TypeId::of::<T>() == TypeId::of::<SkinnedMesh>() {
app.add_systems(
FixedUpdate,
changed_skinned_mesh_detect.run_if(
resource_exists::<Channel<Message>>
.and_then(should_send)
.and_then(in_state(ServerState::Connected).or_else(in_state(ClientState::Connected))),
),
);
} else if TypeId::of::<T>() == TypeId::of::<Name>() {
app.add_systems(
FixedUpdate,
changed_name_detect.run_if(
resource_exists::<Channel<Message>>
.and_then(should_send)
.and_then(in_state(ServerState::Connected).or_else(in_state(ClientState::Connected))),
),
);
} else {
app.add_systems(
FixedUpdate,
changed_component_detect::<T>.run_if(
resource_exists::<Channel<Message>>
.and_then(should_send)
.and_then(in_state(ServerState::Connected).or_else(in_state(ClientState::Connected))),
),
);
}
}
pub(crate) struct SyncTrackPlugin;
impl Plugin for SyncTrackPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
FixedUpdate,
(
entity_marked_for_sync,
entity_removed_from_sync,
entity_parented,
react_on_changed_materials.run_if(sync_material_enabled),
react_on_changed_images.run_if(sync_material_enabled),
react_on_changed_meshes.run_if(sync_mesh_enabled),
react_on_changed_audios.run_if(sync_audio_enabled),
)
.chain()
.run_if(resource_exists::<Channel<Message>>.and_then(should_send))
.run_if(in_state(ServerState::Connected).or_else(in_state(ClientState::Connected))),
);
}
}
#[allow(clippy::needless_pass_by_value)]
fn should_send(c: Res<Channel<Message>>) -> bool {
!(c.is_host() && c.destinations().len() == 1)
}
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::type_complexity)]
pub(crate) fn changed_component_detect<T: Component + Reflect + Clone>(
mut cmd: Commands,
q: Query<(Entity, &EntityUuid, &T), (With<SyncEntity>, Without<SyncExclude<T>>, Changed<T>)>,
q_last: Query<&LastValueOf<T>>,
registry: Res<AppTypeRegistry>,
) {
let registry = registry.read();
for (e, sup, component) in q.iter() {
if let Ok(last) = q_last.get(e)
&& let Some(equal) = last.0.reflect_partial_eq(component.as_partial_reflect())
&& equal
{
continue;
}
let type_name = type_name::<T>();
debug!("Detected component change of {}", type_name);
let refl = component.as_partial_reflect();
let name = refl.get_represented_type_info().unwrap().type_path().into();
let change_id = ComponentChangeId {
uuid: sup.uuid,
name,
};
debug!("Serializing component change of {}", type_name);
let bin = match reflect_to_bin(refl, ®istry) {
Ok(bin) => bin,
Err(e) => {
debug!(
"Could not serialize component {:?}, {:?}",
refl.reflect_type_ident(),
e
);
continue;
}
};
let msg = Message::ComponentUpdated {
uuid: sup.uuid,
name: change_id.name,
data: bin,
};
debug!(
"Sending component changed of {} for entity {:?}:\n{:?}",
type_name, sup.uuid, refl
);
cmd.queue(move |world: &mut World| {
send_message_world(world, msg);
});
cmd.entity(e).insert(LastValueOf(component.clone()));
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn changed_name_detect(
mut cmd: Commands,
q: Query<
(Entity, &EntityUuid, &Name),
(With<SyncEntity>, Without<SyncExclude<Name>>, Changed<Name>),
>,
q_last: Query<&LastValueOf<Name>>,
) {
for (e, sup, component) in q.iter() {
if let Ok(last) = q_last.get(e)
&& last.0 == *component
{
continue;
}
debug!("Detected component change of Name");
let name = component.to_string();
let msg = Message::ComponentNameUpdated {
uuid: sup.uuid,
data: name,
};
debug!(
"Sending component changed of Name for entity {:?}:\n{:?}",
sup.uuid, component
);
cmd.queue(move |world: &mut World| {
send_message_world(world, msg);
});
cmd.entity(e).insert(LastValueOf(component.clone()));
}
}
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::type_complexity)]
pub(crate) fn changed_skinned_mesh_detect(
mut cmd: Commands,
registry: Res<AppTypeRegistry>,
assets: Res<Assets<SkinnedMeshInverseBindposes>>,
entity_map: Res<EntityLookup>,
q: Query<
(&EntityUuid, &SkinnedMesh),
(
With<SyncEntity>,
Without<SyncExclude<SkinnedMesh>>,
Changed<SkinnedMesh>,
),
>,
) {
let registry = registry.read();
for (sup, component) in q.iter() {
let refl = SyncTrackerRes::to_skinned_mapper(&assets, &entity_map, component)
.as_partial_reflect()
.to_dynamic();
let name = refl.get_represented_type_info().unwrap().type_path().into();
let change_id = ComponentChangeId {
uuid: sup.uuid,
name,
};
let bin = match reflect_to_bin(refl.as_ref(), ®istry) {
Ok(bin) => bin,
Err(e) => {
debug!(
"Could not send component {:?}, {:?}",
refl.reflect_type_ident(),
e
);
continue;
}
};
let msg = Message::ComponentUpdated {
uuid: sup.uuid,
name: change_id.name,
data: bin,
};
cmd.queue(move |world: &mut World| {
send_message_world(world, msg);
});
}
}
pub(crate) fn entity_marked_for_sync(query: Query<Entity, Added<SyncMark>>, mut cmd: Commands) {
for id in query {
cmd.queue(move |world: &mut World| {
let mut e = world.entity_mut(id);
let uuid = match e.get::<EntityUuid>() {
Some(eu) => eu.uuid,
None => Uuid::new_v4(),
};
e.insert(EntityUuid { uuid })
.insert(SyncEntity)
.remove::<SyncMark>();
send_message_world(world, Message::EntitySpawn { uuid });
debug!("New entity sent for {}", uuid);
});
}
}
pub(crate) fn entity_removed_from_sync(
mut cmd: Commands,
mut events: MessageReader<EntityUuidDeleted>,
) {
for event in events.read() {
let uuid = event.0;
cmd.queue(move |world: &mut World| {
send_message_world(world, Message::EntityDelete { uuid });
debug!("Entity deletion sent for {}", uuid);
});
}
}
pub(crate) fn entity_parented(
mut cmd: Commands,
query: Query<(&ChildOf, &EntityUuid, &SyncEntity), Changed<ChildOf>>,
query_parent: Query<(Entity, &EntityUuid, &SyncEntity), With<Children>>,
) {
for (p, sup, _) in query.iter() {
let Ok(parent) = query_parent.get(p.parent()) else {
continue;
};
let id = sup.uuid;
let pid = parent.1.uuid;
cmd.queue(move |world: &mut World| {
send_message_world(
world,
Message::EntityParented {
entity_uuid: id,
parent_uuid: pid,
},
);
});
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn react_on_changed_materials(
mut cmd: Commands,
mut track: ResMut<SyncTrackerRes>,
registry: Res<AppTypeRegistry>,
materials: Res<Assets<StandardMaterial>>,
mut events: MessageReader<AssetEvent<StandardMaterial>>,
) {
let registry = registry.read();
for event in &mut events.read() {
match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
let Some(material) = materials.get(*id) else {
continue;
};
let AssetId::Uuid { uuid: id } = id else {
continue;
};
if track.skip_network_handle_change(*id) {
continue;
}
let Ok(bin) = reflect_to_bin(material.as_partial_reflect(), ®istry) else {
continue;
};
let msg = Message::StandardMaterialUpdated {
uuid: *id,
material: bin,
};
cmd.queue(move |world: &mut World| {
send_message_world(world, msg);
});
}
AssetEvent::Removed { id: _ } => {}
_ => (),
}
}
}
pub(crate) fn react_on_changed_meshes(
mut cmd: Commands,
mut track: ResMut<SyncTrackerRes>,
mut events: MessageReader<AssetEvent<Mesh>>,
) {
for event in &mut events.read() {
match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
let AssetId::Uuid { uuid: id } = id else {
continue;
};
if track.skip_network_handle_change(*id) {
continue;
}
cmd.queue(SendMeshCommand(None, *id));
}
AssetEvent::Removed { id: _ } => {}
_ => (),
}
}
}
pub(crate) fn react_on_changed_images(
mut cmd: Commands,
mut track: ResMut<SyncTrackerRes>,
mut events: MessageReader<AssetEvent<Image>>,
) {
for event in &mut events.read() {
match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
let AssetId::Uuid { uuid: id } = id else {
continue;
};
if track.skip_network_handle_change(*id) {
continue;
}
cmd.queue(SendImageCommand(None, *id));
}
AssetEvent::Removed { id: _ } => {}
_ => (),
}
}
}
pub(crate) fn react_on_changed_audios(
mut cmd: Commands,
mut track: ResMut<SyncTrackerRes>,
mut events: MessageReader<AssetEvent<AudioSource>>,
) {
for event in &mut events.read() {
match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
let AssetId::Uuid { uuid: id } = id else {
continue;
};
if track.skip_network_handle_change(*id) {
continue;
}
cmd.queue(SendAudioCommand(None, *id));
}
AssetEvent::Removed { id: _ } => {}
_ => (),
}
}
}