diff --git a/Cargo.toml b/Cargo.toml index cccade60f..27d4fe98a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ default = [ "player_list", "scoreboard", "world_border", + "world_time", "weather", "testing", ] @@ -34,6 +35,7 @@ network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] scoreboard = ["dep:valence_scoreboard"] world_border = ["dep:valence_world_border"] +world_time = ["dep:valence_world_time"] weather = ["dep:valence_weather"] testing = [] @@ -58,6 +60,7 @@ valence_registry.workspace = true valence_scoreboard = { workspace = true, optional = true } valence_weather = { workspace = true, optional = true } valence_world_border = { workspace = true, optional = true } +valence_world_time = { workspace = true, optional = true } valence_lang.workspace = true valence_text.workspace = true valence_ident.workspace = true @@ -195,4 +198,5 @@ valence_server_common = { path = "crates/valence_server_common", version = "0.2. valence_text = { path = "crates/valence_text", version = "0.2.0-alpha.1" } valence_weather = { path = "crates/valence_weather", version = "0.2.0-alpha.1" } valence_world_border = { path = "crates/valence_world_border", version = "0.2.0-alpha.1" } +valence_world_time = { path = "crates/valence_world_time", version = "0.2.0-alpha.1" } zip = "0.6.3" diff --git a/assets/depgraph.svg b/assets/depgraph.svg index b9523f69e..3c179e7f9 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -4,11 +4,11 @@ - + %3 - + 0 @@ -18,140 +18,140 @@ 1 - -valence_server + +valence_server 0->1 - - + + 2 - -valence_entity + +valence_entity 1->2 - - + + 11 - -valence_registry + +valence_registry 1->11 - - + + 10 - -valence_server_common + +valence_server_common 2->10 - - + + 11->10 - - + + 6 - -valence_protocol + +valence_protocol 10->6 - - + + 3 - -valence_math + +valence_math 4 - -valence_nbt + +valence_nbt 5 - -valence_ident + +valence_ident 7 - -valence_generated + +valence_generated 6->7 - - + + 9 - -valence_text + +valence_text 6->9 - - + + 7->3 - - + + 7->5 - - + + 9->4 - - + + 9->5 - - + + 8 - -valence_build_utils + +valence_build_utils @@ -162,8 +162,8 @@ 12->1 - - + + @@ -174,8 +174,8 @@ 13->1 - - + + @@ -186,32 +186,32 @@ 14->1 - - + + 15 - -valence_lang + +valence_lang 16 - -valence_network + +valence_network 16->1 - - + + 16->15 - - + + @@ -222,8 +222,8 @@ 17->1 - - + + @@ -234,14 +234,14 @@ 18->1 - - + + 19 - -valence_spatial + +valence_spatial @@ -252,8 +252,8 @@ 20->1 - - + + @@ -264,116 +264,134 @@ 21->1 - - + + 22 - -dump_schedule + +valence_world_time + + + +22->1 + + 23 - -valence + +dump_schedule - - -22->23 - - + + +24 + +valence - + -23->0 - - +23->24 + + - + -23->12 - - +24->0 + + - + -23->13 - - +24->12 + + - + -23->14 - - +24->13 + + - + -23->16 - - +24->14 + + - + -23->17 - - +24->16 + + - + -23->18 - - +24->17 + + - + -23->20 - - +24->18 + + - + -23->21 - - - - - -24 - -packet_inspector +24->20 + + - + -24->6 - - +24->21 + + + + + +24->22 + + 25 - -playground + +packet_inspector - - -25->23 - - + + +25->6 + + 26 - -stresser - - - -26->6 - - + +playground + + + +26->24 + + + + + +27 + +stresser + + + +27->6 + + diff --git a/crates/valence_world_time/Cargo.toml b/crates/valence_world_time/Cargo.toml new file mode 100644 index 000000000..5abff562d --- /dev/null +++ b/crates/valence_world_time/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "valence_world_time" +description = "World time support for Valence" +readme = "README.md" +version.workspace = true +edition.workspace = true +repository.workspace = true +documentation.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy_app.workspace = true +bevy_ecs.workspace = true +valence_server.workspace = true +derive_more.workspace = true \ No newline at end of file diff --git a/crates/valence_world_time/README.md b/crates/valence_world_time/README.md new file mode 100644 index 000000000..8c0d507d3 --- /dev/null +++ b/crates/valence_world_time/README.md @@ -0,0 +1,75 @@ +# Controlling World Time + +This module contains Components and Systems needed to update, tick, +broadcast information about the time of day and world age of a +[`ChunkLayer`]. + +## Enable world time + +To control world time of an [`ChunkLayer`], simply insert the +[`WorldTimeBundle`] bundle. We also need to broadcast world time updates to +clients. The [`IntervalBroadcast::default()`] provides configuration to +mimic vanilla behavior: + +```rust ignore +fn enable(mut commands: Commands, instance: Entity) { + commands.entity(instance).insert(WorldTimeBundle::default()); +} +``` + +## Set the time explicitly + +Mutating [`WorldTime`] will not automatically broadcast the +change to clients. Mutating [`SetTimeQuery`] to modify time +and broadcast the time changes immediately. + +```rust ignore +fn into_the_night(mut instances: Query<(&mut WorldTime, SetTimeQuery), With>) { + for (mut t1, mut t2) in instances.iter_mut() { + let time_to_set = DayPhase::Night.into(); + + // Using [`WorldTime`] - Change won't broadcast immediately + t1.time_of_day = time_to_set; + // Using [`SetTimeQuery`] - Change broadcast immediately + t2.time_of_day = time_to_set; + } +} +``` + +## Advacing the world time + +Time of day and world age can be ticked individually using +[`LinearTimeTicking`] and [`LinearWorldAging`] respectively. +If these components don't meet your requirements +(eg: you need time increment follow a sine wave ~~for some reason~~), +you can tick the time yourself by modifying the respective +fields on [`WorldTime`]. + +## Prevent client from automatically update WorldTime + +_(mimics `/gamerule doDaylightCycle false`)_ + +By default, client will continue to update world time if the server +doesn't send packet to sync time between client and server. +This can be toggled by using [`WorldTime::set_client_time_ticking()`] +of [`WorldTime`] to true. + +Here is an example of mimicking `/gamerule doDaylightCycle `: + +```rust ignore +#[derive(Component)] +pub struct DaylightCycle(pub bool); + +fn handle_game_rule_daylight_cycle( + mut instances: Query< + (&mut WorldTime, &mut LinearTimeTicking, &DaylightCycle), + Changed, + >, +) { + for (mut time, mut ticking, doCycle) in instances.iter_mut() { + // Stop client from update + time.set_client_time_ticking(!doCycle.0); + ticking.speed = if doCycle.0 { 1 } else { 0 }; + } +} +``` diff --git a/crates/valence_world_time/src/extra.rs b/crates/valence_world_time/src/extra.rs new file mode 100644 index 000000000..54da745c0 --- /dev/null +++ b/crates/valence_world_time/src/extra.rs @@ -0,0 +1,115 @@ +use crate::WorldTime; + +pub const DAY_LENGTH: u64 = 24000; + +/// Notable events of a 24-hour Minecraft day +pub enum DayPhase { + Day = 0, + Noon = 6000, + Sunset = 12000, + Night = 13000, + Midnight = 18000, + Sunrise = 23000, +} + +impl From for u64 { + fn from(value: DayPhase) -> Self { + value as Self + } +} + +/// Reference: +pub enum MoonPhase { + FullMoon = 0, + WaningGibbous = 1, + ThirdQuarter = 2, + WaningCrescent = 3, + NewMoon = 4, + WaxingCrescent = 5, + FirstQuarter = 6, + WaxingGibbous = 7, +} + +impl From for u64 { + fn from(value: MoonPhase) -> Self { + value as Self + } +} + +impl WorldTime { + /// This function ensure that adding time will not resulting in + /// time_of_day flipping sign. + pub fn add_time(&mut self, amount: impl Into) { + let client_ticking = self.client_time_ticking(); + self.time_of_day = self.time_of_day.abs().wrapping_add(amount.into()); + if self.time_of_day < 0 { + self.time_of_day = self.time_of_day + i64::MAX + 1; + } + + self.set_client_time_ticking(client_ticking); + } + + /// If the client advances world time locally without server updates. + pub fn client_time_ticking(&self) -> bool { + self.time_of_day >= 0 + } + + /// Sets if the client advances world time locally without server updates. + /// Note: If the resulting calculation set time_of_day to 0. This function + /// will set time -1 if time_of_day is 0 and is time ticking = false to + /// workaround protocol limitations + pub fn set_client_time_ticking(&mut self, val: bool) { + self.time_of_day = if val { + self.time_of_day.abs() + } else { + -self.time_of_day.abs() + }; + } + + /// Get the time part of `time_of_day` + pub fn current_day_time(&self) -> u64 { + self.time_of_day as u64 % DAY_LENGTH + } + + /// Set the time part of `time_of_day` + /// Use the [`DayPhase`] enum to easily handle common time + /// of day events without the need to look up information in the wiki. + pub fn set_current_day_time(&mut self, time: impl Into) { + let client_ticking = self.client_time_ticking(); + self.time_of_day = (self.day() * DAY_LENGTH + time.into() % DAY_LENGTH) as i64; + self.set_client_time_ticking(client_ticking); + } + + /// Get the current day part of `time_of_day` + pub fn day(&self) -> u64 { + self.time_of_day as u64 / DAY_LENGTH + } + + /// Set the current day `time_of_day` + pub fn set_day(&mut self, day: u64) { + let client_ticking = self.client_time_ticking(); + self.time_of_day = (day * DAY_LENGTH + self.current_day_time()) as i64; + self.set_client_time_ticking(client_ticking); + } + + /// Set the time_of_day to the next specified [`DayPhase`] + pub fn warp_to_next_day_phase(&mut self, phase: DayPhase) { + let phase_num: u64 = phase.into(); + if self.current_day_time() >= phase_num { + self.set_day(self.day() + 1); + } + + self.set_current_day_time(phase_num); + } + + /// Set the time_of_day to the next specified [`MoonPhase`] + pub fn wrap_to_next_moon_phase(&mut self, phase: MoonPhase) { + let phase_no: u64 = phase.into(); + if self.day() % 8 >= phase_no { + self.set_day(self.day() + 8 - (self.day() % 8)) + } + + self.set_day(self.day() + phase_no - self.day() % 8); + self.set_current_day_time(DayPhase::Night); + } +} diff --git a/crates/valence_world_time/src/lib.rs b/crates/valence_world_time/src/lib.rs new file mode 100644 index 000000000..f06f02ad1 --- /dev/null +++ b/crates/valence_world_time/src/lib.rs @@ -0,0 +1,251 @@ +#![doc = include_str!("../README.md")] +#![allow(clippy::type_complexity)] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] + +pub mod extra; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::bundle::Bundle; +use bevy_ecs::component::Component; +use bevy_ecs::query::Changed; +use bevy_ecs::schedule::IntoSystemConfigs; +use bevy_ecs::system::{Query, Res}; +use derive_more::{Deref, DerefMut}; +use valence_server::client::{Client, FlushPacketsSet, VisibleChunkLayer}; +use valence_server::protocol::packets::play::WorldTimeUpdateS2c; +use valence_server::protocol::WritePacket; +use valence_server::{ChunkLayer, Server}; + +pub struct WorldTimePlugin; + +impl Plugin for WorldTimePlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PostUpdate, + ( + handle_interval_broadcast, + handle_linear_time_ticking, + handle_linear_world_aging, + ) + .before(FlushPacketsSet) + .before(handle_layer_time_broadcast), + ) + .add_systems( + PostUpdate, + handle_layer_time_broadcast.before(init_time_for_new_clients), + ) + .add_systems(PostUpdate, init_time_for_new_clients); + } +} + +#[derive(Bundle, Default, Debug)] +pub struct WorldTimeBundle { + pub world_time: WorldTime, + pub broadcast: WorldTimeBroadcast, + pub interval: IntervalBroadcast, + pub linear_ticker: LinearTimeTicking, + pub linear_ticker_timestamp: LinearTimeTickerTimestamp, + pub linear_world_age: LinearWorldAging, + pub linear_world_age_timestamp: LinearWorldAgingTimestamp, +} + +/// The base component to store time in a layer. +/// Tip: If you are looking to modify time in a layer, use +/// [`SetTimeQuery`] to also broadcast time immediately +#[derive(Component, Default, PartialEq, Clone, Copy, Debug)] +pub struct WorldTime { + /// The age of the world in 1/20ths of a second. + pub world_age: i64, + /// The current time of day in 1/20ths of a second. + /// The value should be in the range \[0, 24000]. + /// 6000 is noon, 12000 is sunset, and 18000 is midnight. + pub time_of_day: i64, +} + +/// Store information about the last broadcasted time. You shouldn't +/// mutate this component directly. +#[derive(Component, Default, Clone, Copy, Debug)] +pub struct WorldTimeBroadcast { + pub last_broadcasted: WorldTime, + pub timestamp: i64, + pub will_broadcast_this_tick: bool, +} + +/// This component will signal [`WorldTimeBroadcast`] to send +/// [`WorldTimeUpdateS2c`] packet on an interval. Note that +/// it compares the last broadcasted timestamp with the +/// current server tick to determine if an update should be sent. +#[derive(Component, Deref, DerefMut, Clone, Copy, Debug)] +pub struct IntervalBroadcast(pub i64); + +impl Default for IntervalBroadcast { + fn default() -> Self { + Self(20) + } +} + +/// Use this struct to set time and broadcast it immediately at +/// this tick +#[derive(Debug)] +pub struct SetTimeQuery { + time: &'static mut WorldTime, + broadcast: &'static mut WorldTimeBroadcast, +} + +impl Deref for SetTimeQuery { + type Target = WorldTime; + + fn deref(&self) -> &Self::Target { + self.time + } +} + +impl DerefMut for SetTimeQuery { + fn deref_mut(&mut self) -> &mut Self::Target { + self.broadcast.will_broadcast_this_tick = true; + self.time + } +} + +/// This component is responsible for managing time in a +/// linear fashion. It is commonly used to handle day-night cycles and +/// similar time-dependent processes. This component employs both an interval +/// and a rate to control time progression. +#[derive(Component, Clone, Copy, Debug)] +pub struct LinearTimeTicking { + /// The time interval (in server tick) between each time tick. + pub interval: i64, + + /// The rate at which time advances. A rate of 1 corresponds to real-time, + /// while values less than 1 make time progress slower than the server tick + /// rate. + pub rate: i64, +} + +impl Default for LinearTimeTicking { + fn default() -> Self { + Self { + interval: 1, + rate: 1, + } + } +} + +#[derive(Component, Default, Deref, DerefMut, Clone, Copy, Debug)] +pub struct LinearTimeTickerTimestamp(pub i64); + +/// Similar to [`LinearTimeTicking`] but for world age +#[derive(Component, Clone, Copy, Debug)] +pub struct LinearWorldAging { + /// The time interval (in server tick) between each time tick. + pub interval: i64, + + /// The rate at which world age advances. A rate of 1 corresponds to + /// real-time, while values less than 1 make time progress slower than + /// the server tick rate. + pub rate: i64, +} + +impl Default for LinearWorldAging { + fn default() -> Self { + Self { + interval: 1, + rate: 1, + } + } +} + +#[derive(Component, Default, Deref, DerefMut, Clone, Copy, Debug)] +pub struct LinearWorldAgingTimestamp(pub i64); + +fn init_time_for_new_clients( + mut clients: Query<(&mut Client, &VisibleChunkLayer), Changed>, + layers: Query<&WorldTime>, +) { + for (mut client, layer_ref) in &mut clients { + if let Ok(time) = layers.get(layer_ref.0) { + client.write_packet(&WorldTimeUpdateS2c { + time_of_day: time.time_of_day, + world_age: time.world_age, + }) + } + } +} + +fn handle_layer_time_broadcast( + mut layers: Query<(&mut ChunkLayer, &WorldTime, &mut WorldTimeBroadcast)>, + server: Res, +) { + for (mut layer, time, mut broadcast) in &mut layers { + if broadcast.will_broadcast_this_tick { + layer.write_packet(&WorldTimeUpdateS2c { + time_of_day: time.time_of_day, + world_age: time.world_age, + }); + + broadcast.will_broadcast_this_tick = false; + broadcast.timestamp = server.current_tick(); + } + } +} + +fn handle_interval_broadcast( + mut time: Query<(&IntervalBroadcast, &mut WorldTimeBroadcast)>, + server: Res, +) { + for (interval, mut broadcast) in &mut time { + if server.current_tick() - broadcast.timestamp >= interval.0 { + broadcast.will_broadcast_this_tick = true; + } + } +} + +fn handle_linear_time_ticking( + mut ticker: Query<( + &LinearTimeTicking, + &mut LinearTimeTickerTimestamp, + &mut WorldTime, + )>, + server: Res, +) { + for (info, mut ts, mut time) in &mut ticker { + let ct = server.current_tick(); + if ct - ts.0 >= info.interval { + time.time_of_day += info.rate; + ts.0 = ct; + } + } +} + +fn handle_linear_world_aging( + mut ticker: Query<( + &LinearWorldAging, + &mut LinearWorldAgingTimestamp, + &mut WorldTime, + )>, + server: Res, +) { + for (info, mut ts, mut time) in &mut ticker { + let ct = server.current_tick(); + if ct - ts.0 >= info.interval { + time.world_age += info.rate; + ts.0 = ct; + } + } +} diff --git a/examples/wrapping_time.rs b/examples/wrapping_time.rs new file mode 100644 index 000000000..590b10ff2 --- /dev/null +++ b/examples/wrapping_time.rs @@ -0,0 +1,158 @@ +#![allow(clippy::type_complexity)] + +use std::time::Instant; + +use bevy_app::App; +use valence::client::despawn_disconnected_clients; +use valence::inventory::HeldItem; +use valence::message::SendMessage; +use valence::prelude::*; +use valence_world_time::{LinearTimeTicking, WorldTime, WorldTimeBundle}; + +const SPAWN_Y: i32 = 64; + +fn main() { + App::new() + .insert_resource(LastTickTimestamp { + time: Instant::now(), + }) + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + despawn_disconnected_clients, + init_clients, + show_time_info, + change_time, + ), + ) + .run(); +} + +#[derive(Resource)] +struct LastTickTimestamp { + time: Instant, +} + +fn setup( + mut commands: Commands, + server: Res, + biomes: Res, + dimensions: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + let mut wb = WorldTimeBundle::default(); + wb.interval.0 = 200; + + commands.spawn((layer, wb)); +} + +fn show_time_info( + layers: Query<(&WorldTime, &LinearTimeTicking)>, + mut clients: Query<&mut Client>, + server: Res, + mut lt: ResMut, +) { + let layer = layers.single(); + for mut c in &mut clients { + let mspt = lt.time.elapsed().as_millis(); + lt.time = Instant::now(); + + let msg = format!( + "Server {} | mspt: {} | Time: {} | interval: {} | rate: {}", + server.current_tick(), + mspt, + layer.0.time_of_day, + layer.1.interval, + layer.1.rate + ); + c.send_action_bar_message(msg); + } +} + +fn change_time( + mut event: EventReader, + client: Query<(&Client, &HeldItem)>, + mut time: Query<&mut LinearTimeTicking>, +) { + let mut ticker = time.single_mut(); + for e in &mut event { + if let Ok((_, hi)) = client.get(e.client) { + match hi.slot() { + 36 => ticker.rate += 1, + 37 => ticker.rate -= 1, + 38 => ticker.interval += 1, + 39 => ticker.interval -= 1, + _ => (), + }; + } + } +} + +fn init_clients( + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + &mut Inventory, + ), + Added, + >, + layers: Query, With)>, +) { + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + mut inv, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + *game_mode = GameMode::Creative; + + client.send_chat_message( + " + Touch grass (left click) to control time! + - Diamond: increase rate + - Dirt: decrease rate + - Clock: increase interval + - Compass: decrease interval + + Have fun! + ", + ); + + inv.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, None)); + inv.set_slot(37, ItemStack::new(ItemKind::Dirt, 1, None)); + inv.set_slot(38, ItemStack::new(ItemKind::Clock, 1, None)); + inv.set_slot(39, ItemStack::new(ItemKind::Compass, 1, None)); + } +} diff --git a/src/lib.rs b/src/lib.rs index dfffdb3db..afb7e1d46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,8 @@ pub use valence_server::*; pub use valence_weather as weather; #[cfg(feature = "world_border")] pub use valence_world_border as world_border; +#[cfg(feature = "world_time")] +pub use valence_world_time as world_time; /// Contains the most frequently used items in Valence projects. /// @@ -231,6 +233,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_world_border::WorldBorderPlugin); } + #[cfg(feature = "world_time")] + { + group = group.add(valence_world_time::WorldTimePlugin); + } + #[cfg(feature = "boss_bar")] { group = group.add(valence_boss_bar::BossBarPlugin); diff --git a/src/tests.rs b/src/tests.rs index 8fa089e69..1259610ff 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -7,3 +7,4 @@ mod player_list; mod scoreboard; mod weather; mod world_border; +mod world_time; diff --git a/src/tests/world_time.rs b/src/tests/world_time.rs new file mode 100644 index 000000000..33caac4a3 --- /dev/null +++ b/src/tests/world_time.rs @@ -0,0 +1,72 @@ +use valence_server::protocol::packets::play::WorldTimeUpdateS2c; +use valence_world_time::extra::{DayPhase, MoonPhase, DAY_LENGTH}; +use valence_world_time::{WorldTime, WorldTimeBundle}; + +use crate::testing::ScenarioSingleClient; + +#[test] +fn test_world_time_add() { + let mut time = WorldTime::default(); + time.add_time(10); + + assert_eq!(10, time.time_of_day); + assert!(time.client_time_ticking()); + + time.set_client_time_ticking(false); + assert_eq!(-10, time.time_of_day); + + time.add_time(-11); + assert_eq!(-i64::MAX, time.time_of_day); +} + +#[test] +fn test_world_time_modifications() { + let mut time = WorldTime::default(); + + time.set_day(3); + time.set_current_day_time(12000u64); + assert_eq!(3 * DAY_LENGTH + 12000, time.time_of_day as u64); + + time.warp_to_next_day_phase(DayPhase::Day); + assert_eq!(4 * DAY_LENGTH, time.time_of_day as u64); + + time.set_day(0); + time.wrap_to_next_moon_phase(MoonPhase::NewMoon); + assert_eq!( + 4 * DAY_LENGTH + DayPhase::Night as u64, + time.time_of_day as u64 + ) +} + +#[test] +fn test_time_ticking_broadcast() { + let ScenarioSingleClient { + mut app, + client: _, + mut helper, + layer, + } = prepare(); + + for _ in 0..40 { + app.update() + } + + helper + .collect_received() + .assert_count::(2); + + let x: &WorldTime = app.world.get(layer).unwrap(); + assert_eq!(x.time_of_day, 40); +} + +fn prepare() -> ScenarioSingleClient { + let mut s = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + s.app.update(); + s.app + .world + .entity_mut(s.layer) + .insert(WorldTimeBundle::default()); + s +}