use bevy::{gltf::extensions::GltfExtensionHandlers, prelude::*};
use bevy_asset::RenderAssetUsages;
use bevy_mesh::{Indices, skinning::SkinnedMeshInverseBindposes};
use bevy_pbr::PbrPlugin;
use bevy_reflect::{DynamicTypePath, GetTypeRegistration};
use bevy_state::app::StatesPlugin;
use bevy_sync::{SyncWorld, prelude::*};
use serde::{Deserialize, Serialize};
use std::{
env,
error::Error,
fmt::Display,
marker::PhantomData,
net::{IpAddr, Ipv4Addr},
sync::{
LazyLock,
atomic::{AtomicU16, Ordering},
},
thread::sleep,
time::Duration,
};
use uuid::Uuid;
use wgpu_types::PrimitiveTopology;
#[derive(Component)]
pub struct MyNonSynched;
#[derive(Component, Reflect, Default, PartialEq, Serialize, Deserialize, Debug, Clone)]
#[reflect(Component)]
pub struct MySynched {
pub value: i32,
}
#[derive(Component, Reflect, Default, PartialEq, Serialize, Deserialize, Debug, Clone)]
#[reflect(Component)]
pub struct MySynched2 {
pub value: i32,
}
#[derive(Component, Reflect, Default, PartialEq, Serialize, Deserialize, Debug, Clone)]
#[reflect(Component)]
pub struct MyName {
pub name: String,
}
#[derive(Debug)]
pub struct TestError(String);
impl Display for TestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for TestError {
fn description(&self) -> &str {
&self.0
}
}
pub struct TestRun {
pub params: SyncConnectionParameters,
connection_max_wait_updates: u32,
updates_per_run: usize,
}
pub static PORTS: AtomicU16 = AtomicU16::new(4000);
impl Default for TestRun {
fn default() -> Self {
let ps = PORTS.fetch_add(1, Ordering::SeqCst);
let pa = PORTS.fetch_add(1, Ordering::SeqCst);
Self {
params: SyncConnectionParameters::Direct {
port: ps,
ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
asset_port: pa,
},
connection_max_wait_updates: 20,
updates_per_run: 10,
}
}
}
pub struct TestEnv {
pub server: App,
pub clients: Vec<App>,
}
impl TestEnv {
pub fn update(&mut self, count: usize) {
let dur = Duration::from_millis(1000 / (count as u64));
for _ in 0..count {
self.server.update();
for capp in &mut self.clients {
capp.update();
}
sleep(dur);
}
}
pub fn setup_registration<
T: Component
+ TypePath
+ DynamicTypePath
+ Reflect
+ FromReflect
+ GetTypeRegistration
+ Clone,
>(
&mut self,
) {
self.server.sync_component::<T>();
for c in &mut self.clients {
c.sync_component::<T>();
}
}
pub fn assert_no_message_left(&mut self) {
for _ in 0..10 {
sleep(Duration::from_millis(100));
self.update(1);
}
assert_eq!(
0,
self.server.world().events_queue_count(),
"There are still events pending to be processed in the host"
);
for c in &self.clients {
assert_eq!(
0,
c.world().events_queue_count(),
"There are still events pending to be processed in a client"
);
}
}
}
impl TestRun {
pub fn no_pre_setup(_: &mut TestEnv) {}
pub fn no_setup(_: &mut TestEnv) {}
#[must_use]
pub fn for_assets() -> Self {
TestRun {
..Default::default()
}
}
pub fn run<F0, F1, F2, T0, T1>(
&self,
client_count: u32,
mut pre_connect: F0,
mut post_connect: F1,
mut assertion: F2,
) where
F0: FnMut(&mut TestEnv) -> T0,
F1: FnMut(&mut TestEnv) -> T1,
F2: FnMut(&mut TestEnv, T0, T1),
{
let mut test_run = TestEnv {
server: create_server(),
clients: vec![],
};
for _ in 0..client_count {
test_run.clients.push(create_client());
}
info!("###### [run] Running preconnect");
let x = pre_connect(&mut test_run);
info!("###### [run] Connecting");
connect_envs(self, &mut test_run.server, &mut test_run.clients).unwrap();
info!("###### [run] Running post connect");
let y = post_connect(&mut test_run);
info!("###### [run] Updating systems");
test_run.update(self.updates_per_run * test_run.clients.len());
info!("###### [run] Asserting");
assertion(&mut test_run, x, y);
test_run.update(2);
test_run.assert_no_message_left();
disconnect(&mut test_run.server);
for mut client in test_run.clients {
disconnect(&mut client);
}
}
}
fn disconnect(app: &mut App) {
let disconnect = DisconnectCommand;
app.world_mut().commands().queue(disconnect);
for _ in 0..10 {
app.update();
}
}
fn create_server() -> App {
let mut sapp = App::new();
add_plugins(&mut sapp);
sapp.world_mut().spawn(Transform::default());
sapp
}
fn create_client() -> App {
let mut capp = App::new();
add_plugins(&mut capp);
capp
}
static LOG: LazyLock<()> = LazyLock::new(|| {
if env::var("LOG").is_ok() {
tracing_subscriber::fmt()
.with_env_filter("info,bevy_sync=trace,bevy_connect=trace")
.init();
}
});
fn add_plugins(app: &mut App) {
let () = &*LOG;
app.add_plugins(MinimalPlugins);
app.add_plugins(StatesPlugin);
app.add_plugins(AssetPlugin::default());
app.init_asset::<Shader>();
app.init_asset::<Mesh>();
app.init_asset::<Image>();
app.init_asset::<AudioSource>();
app.init_asset::<SkinnedMeshInverseBindposes>();
app.init_resource::<GltfExtensionHandlers>();
app.add_plugins(PbrPlugin::default());
app.add_plugins(SyncPlugin);
}
fn connect_envs(env: &TestRun, sapp: &mut App, capps: &mut [App]) -> Result<(), Box<dyn Error>> {
sapp.world_mut().commands().queue(ConnectCommand {
is_host: true,
config: env.params.clone(),
});
for capp in capps {
capp.world_mut().commands().queue(ConnectCommand {
is_host: false,
config: env.params.clone(),
});
wait_until_connected(sapp, capp, env.connection_max_wait_updates)?;
}
info!("All clients connected");
Ok(())
}
fn wait_until_connected(
sapp: &mut App,
capp: &mut App,
updates: u32,
) -> Result<(), Box<dyn Error>> {
sapp.update();
capp.update();
let mut count = 0;
while count < updates {
sapp.update();
capp.update();
if let ClientState::Connected = capp
.world()
.get_resource::<State<ClientState>>()
.unwrap()
.get()
{
info!("Client connected.");
return Ok(());
}
sleep(Duration::from_millis(20));
count += 1;
}
Err(TestError("Client did not connect.".into()).into())
}
pub fn spawn_new_material(app: &mut App) -> AssetId<StandardMaterial> {
let mut materials = app.world_mut().resource_mut::<Assets<StandardMaterial>>();
let id = Uuid::new_v4();
let material = StandardMaterial {
base_color: Color::srgb(1.0, 0.0, 0.0),
..Default::default()
};
let handle = Handle::<StandardMaterial>::Uuid(id, PhantomData);
let _ = materials.insert(id, material);
app.world_mut().spawn(MeshMaterial3d(handle));
id.into()
}
pub fn spawn_new_mesh(app: &mut App) -> AssetId<Mesh> {
let mut meshes = app.world_mut().resource_mut::<Assets<Mesh>>();
let mut assets = vec![];
for (id, _) in meshes.iter() {
assets.push(id);
}
for id in assets {
let _ = meshes.remove(id);
}
let id = Uuid::new_v4();
let mesh = sample_mesh();
let handle = Handle::<Mesh>::Uuid(id, PhantomData);
let _ = meshes.insert(id, mesh);
app.world_mut().spawn(Mesh3d(handle));
id.into()
}
pub fn spawn_new_image(app: &mut App) -> AssetId<Image> {
let mut images = app.world_mut().resource_mut::<Assets<Image>>();
let id = Uuid::new_v4();
let image = sample_image();
let handle = Handle::<Image>::Uuid(id, PhantomData);
let _ = images.insert(id, image);
let material = StandardMaterial {
base_color: Color::srgb(1.0, 0.0, 0.0),
base_color_texture: Some(handle),
..Default::default()
};
let mut materials = app.world_mut().resource_mut::<Assets<StandardMaterial>>();
let handle = Handle::<StandardMaterial>::Uuid(id, PhantomData);
let _ = materials.insert(id, material);
app.world_mut().spawn(MeshMaterial3d(handle));
id.into()
}
pub fn spawn_new_material_nouuid(app: &mut App) -> Handle<StandardMaterial> {
let mut materials = app.world_mut().resource_mut::<Assets<StandardMaterial>>();
materials.add(StandardMaterial {
base_color: Color::srgb(1.0, 0.0, 0.0),
..Default::default()
})
}
pub fn spawn_new_mesh_nouuid(app: &mut App) -> Handle<Mesh> {
let mut meshes = app.world_mut().resource_mut::<Assets<Mesh>>();
meshes.add(sample_mesh())
}
#[must_use]
pub fn sample_mesh() -> Mesh {
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![
[0.0, 0.0, 0.0],
[1.0, 2.0, 0.0],
[2.0, 2.0, 0.0],
[1.0, 0.0, 0.0],
],
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_UV_0,
vec![[0.0, 1.0], [0.5, 0.0], [1.0, 0.0], [0.5, 1.0]],
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_NORMAL,
vec![
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
],
)
.with_inserted_indices(Indices::U32(vec![
0, 3, 1, 1, 3, 2,
]))
}
#[must_use]
pub fn sample_image() -> Image {
Image::default()
}
pub fn sample_image_handle_strong(images: &mut Assets<Image>) -> Handle<Image> {
images.add(Image::default())
}
pub fn spawn_new_audio(app: &mut App) -> AssetId<AudioSource> {
let mut assets = app.world_mut().resource_mut::<Assets<AudioSource>>();
let id = Uuid::new_v4();
let asset = sample_audio();
let _ = assets.insert(id, asset);
id.into()
}
#[must_use]
pub fn sample_audio() -> AudioSource {
AudioSource {
bytes: vec![11; 1_000_000].into(),
}
}