diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0930c0df..9872ed64 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,6 +2,7 @@ name: Rust CI on: pull_request: branches: [ "master" ] + types: [ opened, synchronize, reopened, ready_for_review ] push: branches: [ "master" ] workflow_dispatch: @@ -112,4 +113,4 @@ jobs: tool: cargo-nextest - name: Run Tests - run: cargo nextest run --target ${{ matrix.target }} --all-targets --all-features -E "not kind(bench)" \ No newline at end of file + run: cargo nextest run --target ${{ matrix.target }} --all-targets --all-features -E "not kind(bench)" diff --git a/Cargo.toml b/Cargo.toml index d3d44dfc..e5d59dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,7 +152,7 @@ temper-game-systems = { path = "src/game_systems" } interactions = { path = "src/game_systems/src/interactions" } # Asynchronous -tokio = { version = "1.50.0", features = [ +tokio = { version = "1.51.1", features = [ "macros", "net", "rt", @@ -197,14 +197,14 @@ sha1 = "0.11.0" num-bigint = "0.4.6" # Encoding/Serialization -serde = { version = "1.0.228", features = ["derive"] } +serde = { version = "1.0.228", features = ["derive", "std", "rc"] } serde_json = "1.0.149" serde_derive = "1.0.228" base64 = "0.22.1" bitcode = "0.6.9" bitcode_derive = "0.6.9" -toml = "1.1.0+spec-1.1.0" +toml = "1.1.2+spec-1.1.0" craftflow-nbt = "2.1.0" figment = { version = "0.10.19", features = ["toml", "env"] } simd-json = "0.17.0" @@ -215,9 +215,9 @@ serde_yaml_ng = "0.10.0" byteorder = "1.5.0" # Data types -dashmap = "7.0.0-rc2" +dashmap = { version = "7.0.0-rc2", features = ["serde"] } uuid = { version = "1.23.0", features = ["v4", "v3", "serde"] } -indexmap = { version = "2.13.0", features = ["serde"] } +indexmap = { version = "2.14.0", features = ["serde"] } bimap = "0.6.3" # Macros @@ -232,14 +232,14 @@ type_hash = "0.3.0" # Magic dhat = "0.3.3" -ctor = "0.8.0" +ctor = "0.9.1" # Compression/Decompression yazi = "0.2.1" flate2 = "1.1.9" # Database -heed = "0.22.1-nested-rtxns-7" +heed = "0.22.1" # Misc deepsize = "0.2.0" @@ -251,16 +251,16 @@ ctrlc = "3.5.2" num_cpus = "1.17.0" typename = "0.1.2" bevy_ecs = { version = "0.18.1", features = ["multi_threaded", "trace", "debug"], default-features = false } -bevy_math = "0.18.1" +bevy_math = { version = "0.18.1", features = ["serialize"] } once_cell = "1.21.4" mime_guess = "2.0.5" ## TUI/CLI crossterm = "0.29.0" ratatui-core = "0.1.0" -tui-input = "0.15.0" +tui-input = "0.15.1" ratatui = "0.30.0" -tui-logger = { version = "0.18.1", features = ["tracing-support", "crossterm"] } +tui-logger = { version = "0.18.2", features = ["tracing-support", "crossterm"] } clap = { version = "4.6.0", features = ["derive", "env"] } indicatif = "0.18.4" colored = "3.1.1" diff --git a/src/app/runtime/src/game_loop.rs b/src/app/runtime/src/game_loop.rs index 875793d1..d2981c88 100644 --- a/src/app/runtime/src/game_loop.rs +++ b/src/app/runtime/src/game_loop.rs @@ -9,26 +9,18 @@ use crate::errors::BinaryError; use crate::tui; use bevy_ecs::prelude::World; -use bevy_ecs::schedule::{ExecutorKind, Schedule}; +use bevy_ecs::schedule::Schedule; use crossbeam_channel::Sender; use std::sync::Arc; use std::time::{Duration, Instant}; -use temper_commands::infrastructure::register_command_systems; -use temper_config::server_config::get_global_config; -use temper_game_systems::{ - LanPinger, chunk_unloader, keep_alive_system, register_background_systems, - register_mob_systems, register_packet_handlers, register_physics_systems, - register_player_systems, register_shutdown_systems, register_world_systems, update_player_ping, - world_sync, -}; +use temper_game_systems::{LanPinger, register_schedules}; use temper_messages::register_messages; use temper_net_runtime::connection::{NewConnection, handle_connection}; use temper_net_runtime::server::create_server_listener; use temper_performance::tick::TickData; use temper_protocol::{PacketSender, create_packet_senders}; use temper_resources::register_resources; -use temper_scheduler::MissedTickBehavior; -use temper_scheduler::{Scheduler, TimedSchedule, drain_registered_schedules}; +use temper_scheduler::Scheduler; use temper_state::{GlobalState, GlobalStateResource}; use temper_utils::formatting::format_duration; use tracing::{Instrument, debug, error, info, info_span, trace, warn}; @@ -93,11 +85,9 @@ pub fn start_game_loop(global_state: GlobalState, no_tui: bool) -> Result<(), Bi server_command_rx, ); - // Build the timed scheduler with all periodic schedules (tick, sync, keepalive) - let mut timed = build_timed_scheduler(); - - // Register systems that run on shutdown (save world, disconnect players, etc.) - register_shutdown_systems(&mut shutdown_schedule); + // Build the timed scheduler with all periodic schedules and shutdown systems. + let mut timed = Scheduler::new(); + register_schedules(&mut timed, &mut shutdown_schedule); // ========================================================================= // PHASE 4: Start Network Thread @@ -238,93 +228,6 @@ pub fn start_game_loop(global_state: GlobalState, no_tui: bool) -> Result<(), Bi Ok(()) } -/// Builds the timed scheduler with all periodic game schedules. -/// -/// Each schedule runs at a specific interval and handles different aspects of the game: -/// - **tick**: Main game tick (player updates, packets, commands) - runs at configured TPS -/// - **world_sync**: Persists world data to disk - every 15 seconds -/// - **keepalive**: Sends keepalive packets to prevent timeouts - every 1 second -fn build_timed_scheduler() -> Scheduler { - let mut timed = Scheduler::new(); - - // ------------------------------------------------------------------------- - // TICK SCHEDULE - Main game loop tick - // ------------------------------------------------------------------------- - // This is the core game tick that runs at the configured TPS (ticks per second). - // It processes packets, updates players, handles commands, and runs game systems. - // Uses Burst behavior to catch up if ticks are missed (up to 5 at a time). - let build_tick = |s: &mut Schedule| { - s.set_executor_kind(ExecutorKind::SingleThreaded); - register_packet_handlers(s); // Handle incoming packets from players - register_player_systems(s); // Update player state (position, inventory, etc.) - register_command_systems(s); // Process queued commands - - register_background_systems(s); // Systems that run in the background (day cycle, chunk sending, etc.) - register_physics_systems(s); // Physics systems (movement, collision, etc.) - register_mob_systems(s); // Mob AI and behavior - register_world_systems(s); // World updates (block changes, redstone, etc.) - }; - let tick_period = Duration::from_secs(1) / get_global_config().tps; - timed.register( - TimedSchedule::new("tick", tick_period, build_tick) - .with_behavior(MissedTickBehavior::Burst) // Run missed ticks to catch up - .with_max_catch_up(5), // But only catch up 5 ticks max at once - ); - - // ------------------------------------------------------------------------- - // WORLD SYNC SCHEDULE - Periodic world persistence - // ------------------------------------------------------------------------- - // Saves the world state to disk periodically to prevent data loss. - // Uses Skip behavior - if we miss a sync, just wait for the next one. - let build_world_sync = |s: &mut Schedule| { - s.add_systems(world_sync::sync_world); - }; - timed.register( - TimedSchedule::new("world_sync", Duration::from_secs(15), build_world_sync) - .with_behavior(MissedTickBehavior::Skip), - ); - - // ------------------------------------------------------------------------- - // CHUNK GC SCHEDULE - Periodic chunk garbage collection - // ------------------------------------------------------------------------- - // - // Cleans up unused chunks from memory to free resources. - // Uses Skip behavior - if we miss a GC, just wait for the next one. - let build_chunk_gc = |s: &mut Schedule| { - s.add_systems(chunk_unloader::handle); - }; - timed.register( - TimedSchedule::new("chunk_gc", Duration::from_secs(5), build_chunk_gc) - .with_behavior(MissedTickBehavior::Skip), - ); - - // ------------------------------------------------------------------------- - // KEEPALIVE SCHEDULE - Prevents client timeout disconnects - // ------------------------------------------------------------------------- - // Sends keepalive packets to all connected players to maintain the connection. - // Has a 250ms phase offset to spread load away from tick boundaries. - // Also handles updating player ping values. - let build_keepalive = |s: &mut Schedule| { - s.add_systems(keep_alive_system::keep_alive_system); - s.add_systems(update_player_ping::handle); - }; - timed.register( - TimedSchedule::new("keepalive", Duration::from_secs(1), build_keepalive) - .with_behavior(MissedTickBehavior::Skip) - .with_phase(Duration::from_millis(250)), // Offset from tick schedule - ); - - // ------------------------------------------------------------------------- - // PLUGIN SCHEDULES - Dynamically registered by plugins - // ------------------------------------------------------------------------- - // Drain any schedules that plugins registered during initialization. - for pending in drain_registered_schedules() { - timed.register(pending.into_timed()); - } - - timed -} - /// Spawns the LAN broadcast pinger task. /// /// This broadcasts the server's presence on the local network using UDP multicast diff --git a/src/components/Cargo.toml b/src/components/Cargo.toml index e5655b8d..de332e90 100644 --- a/src/components/Cargo.toml +++ b/src/components/Cargo.toml @@ -18,3 +18,5 @@ temper-data = { workspace = true } bevy_math = { workspace = true } uuid = { workspace = true } type_hash = { workspace = true } +serde = { workspace = true } +crossbeam-queue = { workspace = true } diff --git a/src/components/src/combat.rs b/src/components/src/combat.rs index 17536b30..00360826 100644 --- a/src/components/src/combat.rs +++ b/src/components/src/combat.rs @@ -1,5 +1,6 @@ use crate::metadata::EntityMetadata; use bevy_ecs::prelude::Component; +use serde::{Deserialize, Serialize}; use temper_data::generated::entities::EntityType as VanillaEntityType; /// Combat properties for an entity. @@ -20,7 +21,7 @@ use temper_data::generated::entities::EntityType as VanillaEntityType; /// combat.set_invulnerable(10); // 10 ticks of invulnerability /// assert!(!combat.can_be_damaged()); /// ``` -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Serialize, Deserialize)] pub struct CombatProperties { /// True if an entity is attackable /// diff --git a/src/components/src/entity_identity.rs b/src/components/src/entity_identity.rs index ab84c942..2a6536ff 100644 --- a/src/components/src/entity_identity.rs +++ b/src/components/src/entity_identity.rs @@ -1,4 +1,5 @@ use bevy_ecs::prelude::Component; +use serde::{Deserialize, Serialize}; use std::sync::atomic::{AtomicI32, Ordering}; /// Global entity ID counter for non-player entities. @@ -18,7 +19,7 @@ static ENTITY_ID_COUNTER: AtomicI32 = AtomicI32::new(1_000_000); /// let pig_identity = Identity::new(Some("Pig".to_string())); /// assert!(pig_identity.entity_id >= 1_000_000); /// ``` -#[derive(Debug, Component, Clone)] +#[derive(Debug, Component, Clone, Serialize, Deserialize)] pub struct Identity { /// Network entity ID used in packets. /// Must be unique across all entities in the server. diff --git a/src/components/src/last_chunk_pos.rs b/src/components/src/last_chunk_pos.rs new file mode 100644 index 00000000..d7c322aa --- /dev/null +++ b/src/components/src/last_chunk_pos.rs @@ -0,0 +1,11 @@ +use bevy_ecs::prelude::Component; +use temper_core::pos::ChunkPos; + +#[derive(Component, Clone, Copy, Debug, Eq, PartialEq)] +pub struct LastChunkPos(pub ChunkPos); + +impl LastChunkPos { + pub fn new(chunk: ChunkPos) -> Self { + Self(chunk) + } +} diff --git a/src/components/src/last_synced_position.rs b/src/components/src/last_synced_position.rs index ad156cc4..12b8078a 100644 --- a/src/components/src/last_synced_position.rs +++ b/src/components/src/last_synced_position.rs @@ -1,9 +1,10 @@ use crate::player::position::Position; use bevy_ecs::prelude::Component; use bevy_math::DVec3; +use serde::{Deserialize, Serialize}; /// Component that tracks the last position synchronized to clients -#[derive(Component, Debug, Clone, Copy)] +#[derive(Component, Debug, Clone, Copy, Serialize, Deserialize)] pub struct LastSyncedPosition(pub DVec3); impl LastSyncedPosition { diff --git a/src/components/src/lib.rs b/src/components/src/lib.rs index 84898435..6985ce94 100644 --- a/src/components/src/lib.rs +++ b/src/components/src/lib.rs @@ -7,6 +7,7 @@ pub mod player; // Core entity components based on temper-data pub mod combat; +pub mod last_chunk_pos; pub mod last_synced_position; pub mod metadata; pub mod physical; diff --git a/src/components/src/physical.rs b/src/components/src/physical.rs index c0ddc0d8..b3c6478d 100644 --- a/src/components/src/physical.rs +++ b/src/components/src/physical.rs @@ -1,5 +1,6 @@ use bevy_ecs::prelude::Component; use bevy_math::bounding::Aabb3d; +use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; use temper_data::generated::entities::EntityType as VanillaEntityType; @@ -7,7 +8,7 @@ use temper_data::generated::entities::EntityType as VanillaEntityType; /// /// Represents the volume occupied by an entity in the world. /// Used for collision detection and physics. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct BoundingBox { aabb: Aabb3d, } diff --git a/src/components/src/player/entity_tracker.rs b/src/components/src/player/entity_tracker.rs new file mode 100644 index 00000000..463a0f18 --- /dev/null +++ b/src/components/src/player/entity_tracker.rs @@ -0,0 +1,19 @@ +use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::prelude::{Component, Entity}; +use crossbeam_queue::SegQueue; +use uuid::Uuid; + +/// Tracks entities that a player should start tracking, as well as entities that are currently being +/// tracked and entities that should be untracked. To track an entity, add its UUID and **entity type ID** to the `to_track` queue. +/// To untrack an entity, add its Entity to the `to_untrack` queue. The `tracking` set contains the ECS Entity IDs of currently tracked entities. +/// +/// This component has several uses: +/// - It allows the server to keep track of which entities a player should be aware of and send the appropriate spawn and destroy packets. +/// - It can be used to optimize entity updates by only sending updates for entities that are currently being tracked by the player. +/// - It can be used to manage entity visibility and interactions, ensuring that players only interact with entities they are supposed to be aware of. +#[derive(Component, Default)] +pub struct EntityTracker { + pub to_track: SegQueue<(Uuid, u16)>, + pub tracking: EntityHashSet, + pub to_untrack: SegQueue, +} diff --git a/src/components/src/player/grounded.rs b/src/components/src/player/grounded.rs index fc263b8a..f0cf209c 100644 --- a/src/components/src/player/grounded.rs +++ b/src/components/src/player/grounded.rs @@ -1,6 +1,7 @@ use bevy_ecs::prelude::Component; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Component, Copy, Clone)] +#[derive(Debug, Default, Component, Copy, Clone, Serialize, Deserialize)] pub struct OnGround(pub bool); impl From for OnGround { diff --git a/src/components/src/player/mod.rs b/src/components/src/player/mod.rs index e525c41f..af5bb497 100644 --- a/src/components/src/player/mod.rs +++ b/src/components/src/player/mod.rs @@ -1,6 +1,7 @@ pub mod abilities; pub mod chunk_receiver; pub mod client_information; +pub mod entity_tracker; pub mod experience; pub mod gamemode; pub mod gameplay_state; diff --git a/src/components/src/player/player_bundle.rs b/src/components/src/player/player_bundle.rs index 9098ca60..1a8d7a97 100644 --- a/src/components/src/player/player_bundle.rs +++ b/src/components/src/player/player_bundle.rs @@ -1,6 +1,7 @@ use crate::bounds::CollisionBounds; use crate::entity_identity::Identity; use crate::player::chunk_receiver::ChunkReceiver; +use crate::player::entity_tracker::EntityTracker; use crate::player::grounded::OnGround; use crate::player::player_marker::PlayerMarker; use crate::player::player_properties::PlayerProperties; @@ -36,6 +37,7 @@ pub struct PlayerBundle { pub on_ground: OnGround, pub chunk_receiver: ChunkReceiver, pub collision_bounds: CollisionBounds, + pub entity_tracker: EntityTracker, // Inventory pub inventory: Inventory, diff --git a/src/components/src/player/position.rs b/src/components/src/player/position.rs index df3e08d6..84c4ec2f 100644 --- a/src/components/src/player/position.rs +++ b/src/components/src/player/position.rs @@ -1,5 +1,6 @@ use bevy_ecs::prelude::Component; use bevy_math::DVec3; +use serde::{Deserialize, Serialize}; use std::ops::DerefMut; use std::{ fmt::{Debug, Display, Formatter}, @@ -8,7 +9,7 @@ use std::{ use temper_codec::net_types::network_position::NetworkPosition; use temper_core::pos::ChunkPos; -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Serialize, Deserialize)] pub struct Position { pub coords: DVec3, } diff --git a/src/components/src/player/rotation.rs b/src/components/src/player/rotation.rs index 8e044013..eab1002b 100644 --- a/src/components/src/player/rotation.rs +++ b/src/components/src/player/rotation.rs @@ -1,9 +1,10 @@ use bevy_ecs::prelude::Component; use bitcode_derive::{Decode, Encode}; +use serde::{Deserialize, Serialize}; use std::fmt::Debug; use type_hash::TypeHash; -#[derive(Component, Clone, Copy, Default, Decode, Encode, TypeHash)] +#[derive(Component, Clone, Copy, Default, Decode, Encode, TypeHash, Serialize, Deserialize)] pub struct Rotation { pub yaw: f32, pub pitch: f32, diff --git a/src/components/src/player/velocity.rs b/src/components/src/player/velocity.rs index 676ce248..aa4a7847 100644 --- a/src/components/src/player/velocity.rs +++ b/src/components/src/player/velocity.rs @@ -1,12 +1,13 @@ use bevy_ecs::prelude::Component; use bevy_math::{DVec3, Vec3A}; +use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; /// Velocity component representing the rate of change of position. /// /// Measured in blocks per tick (at 60 TPS). /// Positive Y is upward. -#[derive(Debug, Component, Clone, Copy)] +#[derive(Debug, Component, Clone, Copy, Serialize, Deserialize)] pub struct Velocity { pub vec: Vec3A, } diff --git a/src/default_commands/Cargo.toml b/src/default_commands/Cargo.toml index 01c0b787..360903b4 100644 --- a/src/default_commands/Cargo.toml +++ b/src/default_commands/Cargo.toml @@ -19,6 +19,7 @@ bimap = { workspace = true } temper-nbt = { workspace = true } temper-state = { workspace = true } temper-resources = { workspace = true } +temper-entities = { workspace = true } ctor = { workspace = true } tracing = { workspace = true } diff --git a/src/default_commands/src/kill.rs b/src/default_commands/src/kill.rs index 37386d80..6c65cbbd 100644 --- a/src/default_commands/src/kill.rs +++ b/src/default_commands/src/kill.rs @@ -1,16 +1,12 @@ #![expect(clippy::type_complexity)] -use bevy_ecs::prelude::{Commands, Entity, Query}; -use temper_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use bevy_ecs::prelude::{Entity, MessageWriter, Query}; use temper_commands::arg::entities::EntityArgument; use temper_commands::Sender; use temper_components::entity_identity::Identity; use temper_components::player::player_marker::PlayerMarker; use temper_macros::command; -use temper_net_runtime::connection::StreamWriter; -use temper_protocol::outgoing::remove_entities::RemoveEntitiesPacket; -use temper_protocol::outgoing::system_message::SystemMessagePacket; -use temper_text::{Color, NamedColor, TextComponentBuilder}; +use temper_messages::destroy_entity::DestroyEntity; #[command("kill")] fn kill_command( @@ -18,61 +14,23 @@ fn kill_command( #[arg] entity_argument: EntityArgument, args: ( Query<(Entity, &Identity, Option<&PlayerMarker>)>, - Commands, - Query<&StreamWriter>, + MessageWriter, ), ) { - let (query, mut cmd, conn_query) = args; + let (query, mut writer) = args; let selected_entities = entity_argument.resolve(query.iter()); - let mut removed_entities = Vec::new(); - - let mut removed_count = 0; - let killed_message = SystemMessagePacket { - message: temper_nbt::NBT::new( - TextComponentBuilder::new("You have been killed. How sad :(") - .bold() - .color(Color::Named(NamedColor::Red)) - .build(), - ), - overlay: false, - }; - for entity in selected_entities { - if let Ok((ent, identity, player_marker)) = query.get(entity) { - if player_marker.is_none() { - removed_entities.push(identity.entity_id.into()); - cmd.entity(ent).despawn(); - removed_count += 1; - } else { - // Don't remove players, just send them a killed message - if let Ok(conn) = conn_query.get(ent) { - if let Err(err) = conn.send_packet_ref(&killed_message) { - sender.send_message( - format!("Failed to send killed message: {}", err).into(), - false, - ); - } - } - } - } - } - - let packet = RemoveEntitiesPacket { - entity_ids: LengthPrefixedVec::new(removed_entities), - }; - - for conn in conn_query.iter() { - if let Err(err) = conn.send_packet_ref(&packet) { - sender.send_message( - format!("Failed to send RemoveEntitiesPacket: {}", err).into(), - false, - ); - } - } + selected_entities.iter().for_each(|e| { + writer.write(DestroyEntity(*e)); + }); sender.send_message( - format!("Killed {} entities (excluding players).", removed_count).into(), + format!( + "Killed {} entities (excluding players).", + selected_entities.len() + ) + .into(), false, ); } diff --git a/src/default_commands/src/spawn.rs b/src/default_commands/src/spawn.rs index 700ce7c0..1980aabf 100644 --- a/src/default_commands/src/spawn.rs +++ b/src/default_commands/src/spawn.rs @@ -5,100 +5,101 @@ use temper_commands::{ arg::{primitive::PrimitiveArgument, utils::parser_error, CommandArgument, ParserResult}, CommandContext, Sender, Suggestion, }; +use temper_entities::entity_types::EntityTypeEnum; use temper_macros::command; -use temper_messages::{EntityType, SpawnEntityCommand}; +use temper_messages::SpawnEntityCommand; use temper_text::TextComponent; /// Wrapper type for EntityType that implements CommandArgument #[derive(Debug, Clone, Copy)] -struct EntityTypeArg(EntityType); +struct EntityTypeArg(EntityTypeEnum); lazy_static! { - static ref MAPPED_ENTITIES: BiMap<&'static str, EntityType> = { + static ref MAPPED_ENTITIES: BiMap<&'static str, EntityTypeEnum> = { let mut m = BiMap::new(); // Add supported entities here - m.insert("allay", EntityType::Allay); - m.insert("armadillo", EntityType::Armadillo); - m.insert("axolotl", EntityType::Axolotl); - m.insert("bat", EntityType::Bat); - m.insert("bee", EntityType::Bee); - m.insert("camel", EntityType::Camel); - m.insert("cat", EntityType::Cat); - m.insert("cave_spider", EntityType::CaveSpider); - m.insert("chicken", EntityType::Chicken); - m.insert("cod", EntityType::Cod); - m.insert("cow", EntityType::Cow); - m.insert("dolphin", EntityType::Dolphin); - m.insert("donkey", EntityType::Donkey); - m.insert("drowned", EntityType::Drowned); - m.insert("enderman", EntityType::Enderman); - m.insert("fox", EntityType::Fox); - m.insert("frog", EntityType::Frog); - m.insert("goat", EntityType::Goat); - m.insert("horse", EntityType::Horse); - m.insert("iron_golem", EntityType::IronGolem); - m.insert("llama", EntityType::Llama); - m.insert("mooshroom", EntityType::Mooshroom); - m.insert("ocelot", EntityType::Ocelot); - m.insert("panda", EntityType::Panda); - m.insert("parrot", EntityType::Parrot); - m.insert("pig", EntityType::Pig); - m.insert("piglin", EntityType::Piglin); - m.insert("polar_bear", EntityType::PolarBear); - m.insert("pufferfish", EntityType::Pufferfish); - m.insert("rabbit", EntityType::Rabbit); - m.insert("salmon", EntityType::Salmon); - m.insert("sheep", EntityType::Sheep); - m.insert("skeleton_horse", EntityType::SkeletonHorse); - m.insert("sniffer", EntityType::Sniffer); - m.insert("snow_golem", EntityType::SnowGolem); - m.insert("spider", EntityType::Spider); - m.insert("squid", EntityType::Squid); - m.insert("strider", EntityType::Strider); - m.insert("tadpole", EntityType::Tadpole); - m.insert("trader_llama", EntityType::TraderLlama); - m.insert("tropical_fish", EntityType::TropicalFish); - m.insert("turtle", EntityType::Turtle); - m.insert("villager", EntityType::Villager); - m.insert("wandering_trader", EntityType::WanderingTrader); - m.insert("wolf", EntityType::Wolf); - m.insert("zombie_horse", EntityType::ZombieHorse); - m.insert("zombified_piglin", EntityType::ZombifiedPiglin); - m.insert("glow_squid", EntityType::GlowSquid); - m.insert("mule", EntityType::Mule); + m.insert("allay", EntityTypeEnum::Allay); + m.insert("armadillo", EntityTypeEnum::Armadillo); + m.insert("axolotl", EntityTypeEnum::Axolotl); + m.insert("bat", EntityTypeEnum::Bat); + m.insert("bee", EntityTypeEnum::Bee); + m.insert("camel", EntityTypeEnum::Camel); + m.insert("cat", EntityTypeEnum::Cat); + m.insert("cave_spider", EntityTypeEnum::CaveSpider); + m.insert("chicken", EntityTypeEnum::Chicken); + m.insert("cod", EntityTypeEnum::Cod); + m.insert("cow", EntityTypeEnum::Cow); + m.insert("dolphin", EntityTypeEnum::Dolphin); + m.insert("donkey", EntityTypeEnum::Donkey); + m.insert("drowned", EntityTypeEnum::Drowned); + m.insert("enderman", EntityTypeEnum::Enderman); + m.insert("fox", EntityTypeEnum::Fox); + m.insert("frog", EntityTypeEnum::Frog); + m.insert("goat", EntityTypeEnum::Goat); + m.insert("horse", EntityTypeEnum::Horse); + m.insert("iron_golem", EntityTypeEnum::IronGolem); + m.insert("llama", EntityTypeEnum::Llama); + m.insert("mooshroom", EntityTypeEnum::Mooshroom); + m.insert("ocelot", EntityTypeEnum::Ocelot); + m.insert("panda", EntityTypeEnum::Panda); + m.insert("parrot", EntityTypeEnum::Parrot); + m.insert("pig", EntityTypeEnum::Pig); + m.insert("piglin", EntityTypeEnum::Piglin); + m.insert("polar_bear", EntityTypeEnum::PolarBear); + m.insert("pufferfish", EntityTypeEnum::Pufferfish); + m.insert("rabbit", EntityTypeEnum::Rabbit); + m.insert("salmon", EntityTypeEnum::Salmon); + m.insert("sheep", EntityTypeEnum::Sheep); + m.insert("skeleton_horse", EntityTypeEnum::SkeletonHorse); + m.insert("sniffer", EntityTypeEnum::Sniffer); + m.insert("snow_golem", EntityTypeEnum::SnowGolem); + m.insert("spider", EntityTypeEnum::Spider); + m.insert("squid", EntityTypeEnum::Squid); + m.insert("strider", EntityTypeEnum::Strider); + m.insert("tadpole", EntityTypeEnum::Tadpole); + m.insert("trader_llama", EntityTypeEnum::TraderLlama); + m.insert("tropical_fish", EntityTypeEnum::TropicalFish); + m.insert("turtle", EntityTypeEnum::Turtle); + m.insert("villager", EntityTypeEnum::Villager); + m.insert("wandering_trader", EntityTypeEnum::WanderingTrader); + m.insert("wolf", EntityTypeEnum::Wolf); + m.insert("zombie_horse", EntityTypeEnum::ZombieHorse); + m.insert("zombified_piglin", EntityTypeEnum::ZombifiedPiglin); + m.insert("glow_squid", EntityTypeEnum::GlowSquid); + m.insert("mule", EntityTypeEnum::Mule); // Hostile entities - m.insert("blaze", EntityType::Blaze); - m.insert("bogged", EntityType::Bogged); - m.insert("breeze", EntityType::Breeze); - m.insert("creaking", EntityType::Creaking); - m.insert("creeper", EntityType::Creeper); - m.insert("elder_guardian", EntityType::ElderGuardian); - m.insert("endermite", EntityType::Endermite); - m.insert("evoker", EntityType::Evoker); - m.insert("ghast", EntityType::Ghast); - m.insert("guardian", EntityType::Guardian); - m.insert("hoglin", EntityType::Hoglin); - m.insert("husk", EntityType::Husk); - m.insert("magma_cube", EntityType::MagmaCube); - m.insert("phantom", EntityType::Phantom); - m.insert("piglin_brute", EntityType::PiglinBrute); - m.insert("pillager", EntityType::Pillager); - m.insert("ravager", EntityType::Ravager); - m.insert("shulker", EntityType::Shulker); - m.insert("silverfish", EntityType::Silverfish); - m.insert("skeleton", EntityType::Skeleton); - m.insert("slime", EntityType::Slime); - m.insert("stray", EntityType::Stray); - m.insert("vex", EntityType::Vex); - m.insert("vindicator", EntityType::Vindicator); - m.insert("warden", EntityType::Warden); - m.insert("witch", EntityType::Witch); - m.insert("wither_skeleton", EntityType::WitherSkeleton); - m.insert("zoglin", EntityType::Zoglin); - m.insert("zombie", EntityType::Zombie); - m.insert("zombie_villager", EntityType::ZombieVillager); + m.insert("blaze", EntityTypeEnum::Blaze); + m.insert("bogged", EntityTypeEnum::Bogged); + m.insert("breeze", EntityTypeEnum::Breeze); + m.insert("creaking", EntityTypeEnum::Creaking); + m.insert("creeper", EntityTypeEnum::Creeper); + m.insert("elder_guardian", EntityTypeEnum::ElderGuardian); + m.insert("endermite", EntityTypeEnum::Endermite); + m.insert("evoker", EntityTypeEnum::Evoker); + m.insert("ghast", EntityTypeEnum::Ghast); + m.insert("guardian", EntityTypeEnum::Guardian); + m.insert("hoglin", EntityTypeEnum::Hoglin); + m.insert("husk", EntityTypeEnum::Husk); + m.insert("magma_cube", EntityTypeEnum::MagmaCube); + m.insert("phantom", EntityTypeEnum::Phantom); + m.insert("piglin_brute", EntityTypeEnum::PiglinBrute); + m.insert("pillager", EntityTypeEnum::Pillager); + m.insert("ravager", EntityTypeEnum::Ravager); + m.insert("shulker", EntityTypeEnum::Shulker); + m.insert("silverfish", EntityTypeEnum::Silverfish); + m.insert("skeleton", EntityTypeEnum::Skeleton); + m.insert("slime", EntityTypeEnum::Slime); + m.insert("stray", EntityTypeEnum::Stray); + m.insert("vex", EntityTypeEnum::Vex); + m.insert("vindicator", EntityTypeEnum::Vindicator); + m.insert("warden", EntityTypeEnum::Warden); + m.insert("witch", EntityTypeEnum::Witch); + m.insert("wither_skeleton", EntityTypeEnum::WitherSkeleton); + m.insert("zoglin", EntityTypeEnum::Zoglin); + m.insert("zombie", EntityTypeEnum::Zombie); + m.insert("zombie_villager", EntityTypeEnum::ZombieVillager); m }; diff --git a/src/entities/Cargo.toml b/src/entities/Cargo.toml index 51a13dc0..2a8cf59d 100644 --- a/src/entities/Cargo.toml +++ b/src/entities/Cargo.toml @@ -9,3 +9,10 @@ bevy_ecs = { workspace = true } temper-data = { workspace = true } temper-components = { workspace = true } temper-core = { workspace = true } +serde = { workspace = true } + +[build-dependencies] +syn = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +heck = { workspace = true } \ No newline at end of file diff --git a/src/entities/build.rs b/src/entities/build.rs new file mode 100644 index 00000000..3fc79d48 --- /dev/null +++ b/src/entities/build.rs @@ -0,0 +1,64 @@ +use heck::ToShoutySnakeCase; +use proc_macro2::Ident; +use quote::quote; +use std::fs; +use std::path::Path; +use syn::{Item, parse_file}; + +fn main() { + println!("cargo::rerun-if-changed=src/markers.rs"); + println!("cargo::rerun-if-changed=build.rs"); + let src_path = "src/markers.rs"; + + let content = fs::read_to_string(src_path).expect("Failed to read source file"); + + let ast: syn::File = parse_file(&content).expect("Failed to parse Rust source file"); + + let target_module_name = "entity_types"; + + let mut enum_variants = Vec::new(); + let mut id_match_arms = Vec::new(); + + for item in ast.items { + if let Item::Mod(module) = &item + && module.ident == target_module_name + && let Some((_, items)) = &module.content + { + for item in items { + if let Item::Struct(struct_item) = item { + let ident = &struct_item.ident; + enum_variants.push(quote! { #ident }); + let caps = Ident::new( + &ident.to_string().to_shouty_snake_case(), + proc_macro2::Span::call_site(), + ); + id_match_arms.push(quote! { + Self::#ident => temper_data::generated::entities::EntityType::#caps, + }); + } + } + } + } + + let enum_name = Ident::new("EntityTypeEnum", proc_macro2::Span::call_site()); + let enum_def = quote! { + #[derive(Eq, PartialEq, serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Hash,)] + pub enum #enum_name { + #( #enum_variants ),* + } + + impl #enum_name { + pub fn to_entity_type(&self) -> temper_data::generated::entities::EntityType { + match self { + #( #id_match_arms )* + } + } + } + }; + + let output_path = Path::new("src/entity_types.rs"); + fs::create_dir_all(output_path.parent().unwrap()) + .expect("Failed to create directory for generated enum"); + + fs::write(output_path, format!("// @generated\n// This file is automatically generated from the list of markers in src/markers.rs\n{}", enum_def)).expect("Failed to write generated enum to file"); +} diff --git a/src/entities/src/bundles/mod.rs b/src/entities/src/bundles/mod.rs index 02138b3f..a6c268b2 100644 --- a/src/entities/src/bundles/mod.rs +++ b/src/entities/src/bundles/mod.rs @@ -41,11 +41,33 @@ macro_rules! define_entity_bundle { CombatProperties, EntityMetadata, LastSyncedPosition, SpawnProperties, }; - #[derive(Bundle)] + fn de_meta<'de, D>(_: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(EntityMetadata::from_vanilla( + &VanillaEntityType::$vanilla_type, + )) + } + + fn de_spawn<'de, D>(_: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(SpawnProperties::from_vanilla( + &VanillaEntityType::$vanilla_type, + )) + } + + #[derive(Bundle, serde::Serialize, serde::Deserialize)] pub struct $bundle_name { pub identity: Identity, + #[serde(skip_serializing)] + #[serde(deserialize_with = "de_meta")] pub metadata: EntityMetadata, pub combat: CombatProperties, + #[serde(skip_serializing)] + #[serde(deserialize_with = "de_spawn")] pub spawn: SpawnProperties, pub position: Position, pub rotation: Rotation, diff --git a/src/entities/src/entity_types.rs b/src/entities/src/entity_types.rs new file mode 100644 index 00000000..a9c5f8e7 --- /dev/null +++ b/src/entities/src/entity_types.rs @@ -0,0 +1,3 @@ +// @generated +// This file is automatically generated from the list of markers in src/markers.rs +# [derive (Eq , PartialEq , serde :: Serialize , serde :: Deserialize , Debug , Clone , Copy , Hash ,)] pub enum EntityTypeEnum { Allay , Armadillo , Axolotl , Bat , Camel , Cat , Chicken , Cod , Cow , Donkey , Frog , GlowSquid , Horse , Mooshroom , Mule , Ocelot , Parrot , Pig , Rabbit , Salmon , Sheep , SkeletonHorse , Sniffer , SnowGolem , Squid , Strider , Tadpole , TropicalFish , Turtle , Villager , WanderingTrader , ZombieHorse , Bee , CaveSpider , Dolphin , Drowned , Enderman , Fox , Goat , IronGolem , Llama , Panda , Piglin , PolarBear , Pufferfish , Spider , TraderLlama , Wolf , ZombifiedPiglin , Blaze , Bogged , Breeze , Creaking , Creeper , ElderGuardian , Endermite , Evoker , Ghast , Guardian , Hoglin , Husk , MagmaCube , Phantom , PiglinBrute , Pillager , Ravager , Shulker , Silverfish , Skeleton , Slime , Stray , Vex , Vindicator , Warden , Witch , WitherSkeleton , Zoglin , Zombie , ZombieVillager } impl EntityTypeEnum { pub fn to_entity_type (& self) -> temper_data :: generated :: entities :: EntityType { match self { Self :: Allay => temper_data :: generated :: entities :: EntityType :: ALLAY , Self :: Armadillo => temper_data :: generated :: entities :: EntityType :: ARMADILLO , Self :: Axolotl => temper_data :: generated :: entities :: EntityType :: AXOLOTL , Self :: Bat => temper_data :: generated :: entities :: EntityType :: BAT , Self :: Camel => temper_data :: generated :: entities :: EntityType :: CAMEL , Self :: Cat => temper_data :: generated :: entities :: EntityType :: CAT , Self :: Chicken => temper_data :: generated :: entities :: EntityType :: CHICKEN , Self :: Cod => temper_data :: generated :: entities :: EntityType :: COD , Self :: Cow => temper_data :: generated :: entities :: EntityType :: COW , Self :: Donkey => temper_data :: generated :: entities :: EntityType :: DONKEY , Self :: Frog => temper_data :: generated :: entities :: EntityType :: FROG , Self :: GlowSquid => temper_data :: generated :: entities :: EntityType :: GLOW_SQUID , Self :: Horse => temper_data :: generated :: entities :: EntityType :: HORSE , Self :: Mooshroom => temper_data :: generated :: entities :: EntityType :: MOOSHROOM , Self :: Mule => temper_data :: generated :: entities :: EntityType :: MULE , Self :: Ocelot => temper_data :: generated :: entities :: EntityType :: OCELOT , Self :: Parrot => temper_data :: generated :: entities :: EntityType :: PARROT , Self :: Pig => temper_data :: generated :: entities :: EntityType :: PIG , Self :: Rabbit => temper_data :: generated :: entities :: EntityType :: RABBIT , Self :: Salmon => temper_data :: generated :: entities :: EntityType :: SALMON , Self :: Sheep => temper_data :: generated :: entities :: EntityType :: SHEEP , Self :: SkeletonHorse => temper_data :: generated :: entities :: EntityType :: SKELETON_HORSE , Self :: Sniffer => temper_data :: generated :: entities :: EntityType :: SNIFFER , Self :: SnowGolem => temper_data :: generated :: entities :: EntityType :: SNOW_GOLEM , Self :: Squid => temper_data :: generated :: entities :: EntityType :: SQUID , Self :: Strider => temper_data :: generated :: entities :: EntityType :: STRIDER , Self :: Tadpole => temper_data :: generated :: entities :: EntityType :: TADPOLE , Self :: TropicalFish => temper_data :: generated :: entities :: EntityType :: TROPICAL_FISH , Self :: Turtle => temper_data :: generated :: entities :: EntityType :: TURTLE , Self :: Villager => temper_data :: generated :: entities :: EntityType :: VILLAGER , Self :: WanderingTrader => temper_data :: generated :: entities :: EntityType :: WANDERING_TRADER , Self :: ZombieHorse => temper_data :: generated :: entities :: EntityType :: ZOMBIE_HORSE , Self :: Bee => temper_data :: generated :: entities :: EntityType :: BEE , Self :: CaveSpider => temper_data :: generated :: entities :: EntityType :: CAVE_SPIDER , Self :: Dolphin => temper_data :: generated :: entities :: EntityType :: DOLPHIN , Self :: Drowned => temper_data :: generated :: entities :: EntityType :: DROWNED , Self :: Enderman => temper_data :: generated :: entities :: EntityType :: ENDERMAN , Self :: Fox => temper_data :: generated :: entities :: EntityType :: FOX , Self :: Goat => temper_data :: generated :: entities :: EntityType :: GOAT , Self :: IronGolem => temper_data :: generated :: entities :: EntityType :: IRON_GOLEM , Self :: Llama => temper_data :: generated :: entities :: EntityType :: LLAMA , Self :: Panda => temper_data :: generated :: entities :: EntityType :: PANDA , Self :: Piglin => temper_data :: generated :: entities :: EntityType :: PIGLIN , Self :: PolarBear => temper_data :: generated :: entities :: EntityType :: POLAR_BEAR , Self :: Pufferfish => temper_data :: generated :: entities :: EntityType :: PUFFERFISH , Self :: Spider => temper_data :: generated :: entities :: EntityType :: SPIDER , Self :: TraderLlama => temper_data :: generated :: entities :: EntityType :: TRADER_LLAMA , Self :: Wolf => temper_data :: generated :: entities :: EntityType :: WOLF , Self :: ZombifiedPiglin => temper_data :: generated :: entities :: EntityType :: ZOMBIFIED_PIGLIN , Self :: Blaze => temper_data :: generated :: entities :: EntityType :: BLAZE , Self :: Bogged => temper_data :: generated :: entities :: EntityType :: BOGGED , Self :: Breeze => temper_data :: generated :: entities :: EntityType :: BREEZE , Self :: Creaking => temper_data :: generated :: entities :: EntityType :: CREAKING , Self :: Creeper => temper_data :: generated :: entities :: EntityType :: CREEPER , Self :: ElderGuardian => temper_data :: generated :: entities :: EntityType :: ELDER_GUARDIAN , Self :: Endermite => temper_data :: generated :: entities :: EntityType :: ENDERMITE , Self :: Evoker => temper_data :: generated :: entities :: EntityType :: EVOKER , Self :: Ghast => temper_data :: generated :: entities :: EntityType :: GHAST , Self :: Guardian => temper_data :: generated :: entities :: EntityType :: GUARDIAN , Self :: Hoglin => temper_data :: generated :: entities :: EntityType :: HOGLIN , Self :: Husk => temper_data :: generated :: entities :: EntityType :: HUSK , Self :: MagmaCube => temper_data :: generated :: entities :: EntityType :: MAGMA_CUBE , Self :: Phantom => temper_data :: generated :: entities :: EntityType :: PHANTOM , Self :: PiglinBrute => temper_data :: generated :: entities :: EntityType :: PIGLIN_BRUTE , Self :: Pillager => temper_data :: generated :: entities :: EntityType :: PILLAGER , Self :: Ravager => temper_data :: generated :: entities :: EntityType :: RAVAGER , Self :: Shulker => temper_data :: generated :: entities :: EntityType :: SHULKER , Self :: Silverfish => temper_data :: generated :: entities :: EntityType :: SILVERFISH , Self :: Skeleton => temper_data :: generated :: entities :: EntityType :: SKELETON , Self :: Slime => temper_data :: generated :: entities :: EntityType :: SLIME , Self :: Stray => temper_data :: generated :: entities :: EntityType :: STRAY , Self :: Vex => temper_data :: generated :: entities :: EntityType :: VEX , Self :: Vindicator => temper_data :: generated :: entities :: EntityType :: VINDICATOR , Self :: Warden => temper_data :: generated :: entities :: EntityType :: WARDEN , Self :: Witch => temper_data :: generated :: entities :: EntityType :: WITCH , Self :: WitherSkeleton => temper_data :: generated :: entities :: EntityType :: WITHER_SKELETON , Self :: Zoglin => temper_data :: generated :: entities :: EntityType :: ZOGLIN , Self :: Zombie => temper_data :: generated :: entities :: EntityType :: ZOMBIE , Self :: ZombieVillager => temper_data :: generated :: entities :: EntityType :: ZOMBIE_VILLAGER , } } } \ No newline at end of file diff --git a/src/entities/src/lib.rs b/src/entities/src/lib.rs index d04bf6b3..6678975b 100644 --- a/src/entities/src/lib.rs +++ b/src/entities/src/lib.rs @@ -1,5 +1,7 @@ pub mod bundles; pub mod components; +#[rustfmt::skip] +pub mod entity_types; pub mod markers; // Re-exports to facilitate use diff --git a/src/game_systems/Cargo.toml b/src/game_systems/Cargo.toml index bb3a872c..3361ebd6 100644 --- a/src/game_systems/Cargo.toml +++ b/src/game_systems/Cargo.toml @@ -14,6 +14,18 @@ world = { path = "./src/world" } interactions = { path = "./src/interactions" } bevy_ecs = { workspace = true } +temper-commands = { workspace = true } +temper-config = { workspace = true } +temper-scheduler = { workspace = true } + +[dev-dependencies] +bevy_math = { workspace = true } +temper-components = { workspace = true } +temper-core = { workspace = true } +temper-entities = { workspace = true } +temper-macros = { workspace = true } +temper-messages = { workspace = true } +temper-state = { workspace = true } [lints] workspace = true diff --git a/src/game_systems/src/background/Cargo.toml b/src/game_systems/src/background/Cargo.toml index 479822a0..bb6de569 100644 --- a/src/game_systems/src/background/Cargo.toml +++ b/src/game_systems/src/background/Cargo.toml @@ -21,4 +21,7 @@ temper-resources = { workspace = true } rand = { workspace = true } tokio = { workspace = true } temper-entities = { workspace = true } -temper-commands = { workspace = true } \ No newline at end of file +temper-commands = { workspace = true } +crossbeam-queue = { workspace = true } +temper-world = { workspace = true } +temper-nbt = { workspace = true } \ No newline at end of file diff --git a/src/game_systems/src/background/src/chunk_sending.rs b/src/game_systems/src/background/src/chunk_sending.rs index 30aeafe9..d7ba7726 100644 --- a/src/game_systems/src/background/src/chunk_sending.rs +++ b/src/game_systems/src/background/src/chunk_sending.rs @@ -1,10 +1,13 @@ -use bevy_ecs::prelude::{Entity, Query, Res}; +use bevy_ecs::prelude::{Entity, MessageWriter, Query, Res}; use bevy_math::{IVec2, IVec3}; +use crossbeam_queue::SegQueue; use std::cmp::max; +use std::sync::Arc; use std::sync::atomic::Ordering; use temper_codec::encode::NetEncodeOpts; use temper_components::player::chunk_receiver::ChunkReceiver; use temper_components::player::client_information::ClientInformationComponent; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::position::Position; use temper_config::server_config::get_global_config; use temper_core::dimension::Dimension; @@ -26,10 +29,12 @@ pub fn handle( &mut ChunkReceiver, &Position, &ClientInformationComponent, + &EntityTracker, )>, state: Res, + mut mob_load_writer: MessageWriter, ) { - for (eid, conn, mut chunk_receiver, pos, client_info) in query.iter_mut() { + for (eid, conn, mut chunk_receiver, pos, client_info, entity_tracker) in query.iter_mut() { if !state.0.players.is_connected(eid) { continue; // Skip if the player is not connected } @@ -93,6 +98,8 @@ pub fn handle( }) .expect("Failed to send SetCenterChunk"); + let entity_queue = Arc::new(SegQueue::new()); + for coordinates in needed_chunks .into_iter() .filter(|coord| { @@ -114,14 +121,28 @@ pub fn handle( .loaded .insert((coordinates.x(), coordinates.z())); let state = state.clone(); + if !state + .0 + .world + .get_cache() + .contains_key(&(coordinates, Dimension::Overworld)) + { + mob_load_writer.write(temper_messages::load_chunk_entities::LoadChunkEntities( + coordinates, + )); + } let is_compressed = conn.compress.load(Ordering::Relaxed); batch.execute({ + let entity_queue = entity_queue.clone(); move || { let chunk = state .0 .world .get_or_generate_chunk(coordinates, Dimension::Overworld) .expect("Failed to load or generate chunk"); + for kv in chunk.entities.iter() { + entity_queue.push((*kv.key(), kv.value().0.to_entity_type().id)); + } let packet = ChunkAndLightData::from_chunk(coordinates, &chunk) .expect("Failed to create ChunkAndLightData"); compress_packet( @@ -156,5 +177,10 @@ pub fn handle( conn.send_packet(packet) .expect("Failed to send UnloadChunk packet"); } + + // God, I hope the compiler can optimize this shit out + while let Some(entity_id) = entity_queue.pop() { + entity_tracker.to_track.push(entity_id); + } } } diff --git a/src/game_systems/src/background/src/chunk_unloader.rs b/src/game_systems/src/background/src/chunk_unloader.rs index b13f171b..b431350f 100644 --- a/src/game_systems/src/background/src/chunk_unloader.rs +++ b/src/game_systems/src/background/src/chunk_unloader.rs @@ -1,12 +1,19 @@ -use bevy_ecs::prelude::{Query, Res}; +use bevy_ecs::prelude::{Commands, Entity, Has, Query, Res}; use std::collections::HashSet; +use temper_components::last_chunk_pos::LastChunkPos; use temper_components::player::chunk_receiver::ChunkReceiver; +use temper_components::player::player_marker::PlayerMarker; use temper_core::dimension::Dimension; use temper_core::pos::ChunkPos; use temper_state::GlobalStateResource; use tracing::{error, trace}; -pub fn handle(state: Res, query: Query<&ChunkReceiver>) { +pub fn handle( + state: Res, + query: Query<&ChunkReceiver>, + mut cmd: Commands, + entity_query: Query<(Entity, &LastChunkPos, Has)>, +) { // If there are no connected players, unload all cached chunks if query.count() == 0 { let mut removed = 0; @@ -14,7 +21,7 @@ pub fn handle(state: Res, query: Query<&ChunkReceiver>) { let ((pos, dim), chunk) = chunk_candidate.pair(); removed += 1; // Write chunks back to the world storage - if chunk.sections.iter().any(|section| section.dirty) { + if chunk.is_dirty() { state .0 .world @@ -59,7 +66,18 @@ pub fn handle(state: Res, query: Query<&ChunkReceiver>) { .remove(&(*chunk_pos, Dimension::Overworld)); match removed_chunk { Some(((pos, dim), chunk)) => { - let dirty = chunk.sections.iter().any(|section| section.dirty); + for (entity, last_chunk, is_player) in entity_query.iter() { + if is_player || last_chunk.0 != *chunk_pos { + continue; + } + + trace!( + "Unloading live entity {:?} from chunk {:?} as it is no longer visible to any player.", + entity, chunk_pos + ); + cmd.entity(entity).despawn(); + } + let dirty = chunk.is_dirty(); if dirty { state .0 diff --git a/src/game_systems/src/background/src/connection_killer.rs b/src/game_systems/src/background/src/connection_killer.rs index 636fd5f5..4796908b 100644 --- a/src/game_systems/src/background/src/connection_killer.rs +++ b/src/game_systems/src/background/src/connection_killer.rs @@ -123,7 +123,10 @@ pub fn connection_killer( } // --- 3. Fire PlayerLeaveEvent --- - leave_events.write(PlayerLeft(player_identity.clone())); + leave_events.write(PlayerLeft { + identity: player_identity.clone(), + entity: disconnecting_entity, + }); } else { // --- FAILURE: This is a "half-player" or zombie --- warn!( @@ -137,7 +140,10 @@ pub fn connection_killer( "-> (Half-player had identity: {})", player_identity.name.as_ref().expect("No Player Name") ); - leave_events.write(PlayerLeft(player_identity.clone())); + leave_events.write(PlayerLeft { + identity: player_identity.clone(), + entity: disconnecting_entity, + }); } else { warn!("-> (Half-player didn't even have an identity component!)"); } diff --git a/src/game_systems/src/background/src/cross_chunk_border.rs b/src/game_systems/src/background/src/cross_chunk_border.rs new file mode 100644 index 00000000..13e7dc51 --- /dev/null +++ b/src/game_systems/src/background/src/cross_chunk_border.rs @@ -0,0 +1,71 @@ +use bevy_ecs::prelude::{Entity, Has, MessageReader, MessageWriter, Query, Res}; +use temper_components::entity_identity::Identity; +use temper_components::player::player_marker::PlayerMarker; +use temper_core::dimension::Dimension::Overworld; +use temper_messages::chunk_calc::ChunkCalc; +use temper_messages::cross_chunk_boundary_event::ChunkBoundaryCrossed; +use temper_state::GlobalStateResource; +use temper_world::WorldError; +use tracing::error; + +pub fn cross_chunk_border( + mut chunk_cross_events: MessageReader, + query: Query<(Entity, &Identity, Has)>, + mut chunk_calc_messages: MessageWriter, + state: Res, +) { + 'ev_loop: for event in chunk_cross_events.read() { + let (entity, identity, is_player) = query + .get(event.entity) + .expect("Entity in ChunkBoundaryCrossed event does not exist"); + // If it's a player, send the chunk calc message + if is_player { + chunk_calc_messages.write(ChunkCalc(entity)); + } else { + // For mobs, we update the chunk they are saved in + let old_chunk_cords = event.old_chunk; + let new_chunk_cords = event.new_chunk; + // Pull out the entity data from the old chunk. We have to do it this way cos holding locks on multiple chunks at once can easily deadlock + let Some(extracted_old_data) = ({ + let chunk = state.0.world.get_chunk(old_chunk_cords, Overworld); + match chunk { + Ok(chunk) => { + let data = chunk.entities.remove(&identity.uuid); + chunk.mark_dirty(); + data + } + Err(WorldError::ChunkNotFound) => { + error!( + "Invalid old chunk coordinates in ChunkBoundaryCrossed event for entity {}: {:?}", + entity, old_chunk_cords + ); + continue 'ev_loop; + } + Err(e) => { + error!( + "Error accessing old chunk in ChunkBoundaryCrossed event for entity {}: {:?}", + entity, e + ); + continue 'ev_loop; + } + } + }) else { + error!( + "Entity {} not found in old chunk during ChunkBoundaryCrossed event", + entity + ); + continue 'ev_loop; + }; + // If the server crashes here, bye bye entity + { + let chunk = state + .0 + .world + .get_or_generate_chunk(new_chunk_cords, Overworld) + .expect("Failed to get or generate new chunk in ChunkBoundaryCrossed event"); + chunk.entities.insert(identity.uuid, extracted_old_data.1); + chunk.mark_dirty(); + } + } + } +} diff --git a/src/game_systems/src/background/src/destroy_entity.rs b/src/game_systems/src/background/src/destroy_entity.rs new file mode 100644 index 00000000..795f50a3 --- /dev/null +++ b/src/game_systems/src/background/src/destroy_entity.rs @@ -0,0 +1,75 @@ +use bevy_ecs::prelude::{Commands, Entity, Has, MessageReader, Query, Res}; +use temper_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use temper_components::entity_identity::Identity; +use temper_components::player::player_marker::PlayerMarker; +use temper_components::player::position::Position; +use temper_core::dimension::Dimension::Overworld; +use temper_messages::destroy_entity::DestroyEntity; +use temper_net_runtime::connection::StreamWriter; +use temper_protocol::outgoing::remove_entities::RemoveEntitiesPacket; +use temper_protocol::outgoing::system_message::SystemMessagePacket; +use temper_state::GlobalStateResource; +use temper_text::{Color, NamedColor, TextComponentBuilder}; +use tracing::trace; + +#[expect(clippy::type_complexity)] +pub fn destroy_entity_system( + mut commands: Commands, + mut destroy_entity_events: MessageReader, + query: Query<( + Entity, + &Position, + &Identity, + Has, + Option<&StreamWriter>, + )>, + state: Res, +) { + let mut destroyed_entities = Vec::new(); + let killed_message = SystemMessagePacket { + message: temper_nbt::NBT::new( + TextComponentBuilder::new("You have been killed. How sad :(") + .bold() + .color(Color::Named(NamedColor::Red)) + .build(), + ), + overlay: false, + }; + + for event in destroy_entity_events.read() { + if let Ok((_, position, identity, has_player_marker, conn_opt)) = query.get(event.0) { + if !has_player_marker { + destroyed_entities.push(identity.entity_id.into()); + commands.entity(event.0).despawn(); + let Ok(chunk) = state.0.world.get_chunk(position.chunk(), Overworld) else { + continue; + }; + if chunk.entities.remove(&identity.uuid).is_some() { + trace!( + "Entity {:?} destroyed and removed from chunk", + identity.entity_id + ); + chunk.mark_dirty(); + } + destroyed_entities.push(identity.entity_id.into()); + } else if let Some(conn) = conn_opt + && let Err(err) = conn.send_packet_ref(&killed_message) + { + trace!("Failed to send killed message: {}", err); + } + } + } + + let packet = RemoveEntitiesPacket { + entity_ids: LengthPrefixedVec::new(destroyed_entities), + }; + + for (_, _, _, has_player_marker, conn_opt) in query.iter() { + if has_player_marker + && let Some(conn) = conn_opt + && let Err(err) = conn.send_packet_ref(&packet) + { + trace!("Failed to send RemoveEntitiesPacket: {}", err); + } + } +} diff --git a/src/game_systems/src/background/src/entity_sending.rs b/src/game_systems/src/background/src/entity_sending.rs new file mode 100644 index 00000000..3f37eb31 --- /dev/null +++ b/src/game_systems/src/background/src/entity_sending.rs @@ -0,0 +1,101 @@ +use bevy_ecs::prelude::{Entity, Has, Query}; +use temper_components::entity_identity::Identity; +use temper_components::player::client_information::ClientInformationComponent; +use temper_components::player::entity_tracker::EntityTracker; +use temper_components::player::player_marker::PlayerMarker; +use temper_components::player::position::Position; +use temper_components::player::rotation::Rotation; +use temper_config::server_config::get_global_config; +use temper_net_runtime::connection::StreamWriter; +use temper_protocol::outgoing::remove_entities::RemoveEntitiesPacket; +use temper_protocol::outgoing::spawn_entity::SpawnEntityPacket; +use tracing::debug; + +/// Protocol entity type ID for player entities in the current target version. +const PLAYER_TYPE_ID: i32 = 149; + +pub fn send_untracked_entities( + mut player_query: Query<(&StreamWriter, &mut EntityTracker)>, + identity_query: Query<&Identity>, +) { + for (conn, entity_tracker) in player_query.iter_mut() { + while let Some(entity) = entity_tracker.to_untrack.pop() { + let Ok(identity) = identity_query.get(entity) else { + continue; + }; + + let packet = RemoveEntitiesPacket::from_entities(std::iter::once(identity.clone())); + conn.send_packet(packet) + .expect("Failed to send remove entities packet"); + } + } +} + +pub fn send_new_entities( + mut player_query: Query<( + &StreamWriter, + &mut EntityTracker, + &Position, + &ClientInformationComponent, + )>, + entity_query: Query<(Entity, &Identity, &Position, &Rotation, Has)>, +) { + for (conn, mut entity_tracker, player_pos, client_info) in player_query.iter_mut() { + let mut unresolved = Vec::new(); + + while let Some((uuid, entity_type_id)) = entity_tracker.to_track.pop() { + if let Some((entity, identity, entity_pos, rot, is_player)) = entity_query + .iter() + .find_map(|(entity, identity, pos, rot, is_player)| { + if identity.uuid == uuid { + Some((entity, identity, pos, rot, is_player)) + } else { + None + } + }) + { + if entity_tracker.tracking.contains(&entity) { + continue; + } + + let render_distance = client_info + .view_distance + .min(get_global_config().chunk_render_distance as u8); + if player_pos.distance(**entity_pos) > (render_distance as f64 * 16.0) { + continue; // Skip entities outside of render distance + } + + let entity_type_id = if is_player { + PLAYER_TYPE_ID + } else { + entity_type_id as i32 + }; + + let packet = SpawnEntityPacket::new( + identity.entity_id, + identity.uuid.as_u128(), + entity_type_id, + entity_pos, + rot, + ); + conn.send_packet(packet) + .expect("Failed to send spawn entity packet"); + debug!( + "Sent spawn packet for entity {} with UUID {} to player at position {:?}", + identity.entity_id, + identity.uuid, + player_pos.xyz() + ); + entity_tracker.tracking.insert(entity); + } else { + // Retry unresolved entities on a later tick instead of reinserting + // into the actively-drained queue and looping forever. + unresolved.push((uuid, entity_type_id)); + } + } + + for item in unresolved { + entity_tracker.to_track.push(item); + } + } +} diff --git a/src/game_systems/src/background/src/entity_tracking.rs b/src/game_systems/src/background/src/entity_tracking.rs new file mode 100644 index 00000000..dad5581f --- /dev/null +++ b/src/game_systems/src/background/src/entity_tracking.rs @@ -0,0 +1,67 @@ +use bevy_ecs::prelude::{Entity, Query}; +use temper_components::entity_identity::Identity; +use temper_components::player::chunk_receiver::ChunkReceiver; +use temper_components::player::entity_tracker::EntityTracker; +use temper_components::player::position::Position; +use temper_net_runtime::connection::StreamWriter; +use tracing::trace; + +pub fn refresh_visible_entities( + mut player_query: Query<(Entity, &StreamWriter, &ChunkReceiver, &mut EntityTracker)>, + entity_query: Query<(Entity, &Identity, &Position, Option<&StreamWriter>)>, +) { + for (player_entity, conn, chunk_receiver, mut tracker) in player_query.iter_mut() { + if !conn.is_running() { + continue; + } + + let tracked_entities = tracker.tracking.iter().copied().collect::>(); + for tracked_entity in tracked_entities { + let should_keep = entity_query + .get(tracked_entity) + .map(|(entity, _identity, pos, maybe_writer)| { + if entity == player_entity { + return false; + } + + if maybe_writer.is_none_or(|writer| !writer.is_running()) { + return false; + } + + let chunk = pos.chunk(); + chunk_receiver.loaded.contains(&(chunk.x(), chunk.z())) + }) + .unwrap_or(false); + + if !should_keep { + tracker.tracking.remove(&tracked_entity); + tracker.to_untrack.push(tracked_entity); + } + } + + for (entity, identity, pos, maybe_writer) in entity_query.iter() { + if entity == player_entity { + continue; + } + + if maybe_writer.is_none_or(|writer| !writer.is_running()) { + continue; + } + + let chunk = pos.chunk(); + if !chunk_receiver.loaded.contains(&(chunk.x(), chunk.z())) { + continue; + } + + if tracker.tracking.contains(&entity) { + continue; + } + + trace!( + "Queueing entity {} ({:?}) for player {:?}", + identity.entity_id, identity.uuid, player_entity + ); + tracker.to_track.push((identity.uuid, 0)); + } + } +} diff --git a/src/game_systems/src/background/src/entity_unloader.rs b/src/game_systems/src/background/src/entity_unloader.rs new file mode 100644 index 00000000..57e19749 --- /dev/null +++ b/src/game_systems/src/background/src/entity_unloader.rs @@ -0,0 +1,29 @@ +use bevy_ecs::prelude::{MessageWriter, Query, Res}; +use std::collections::HashSet; +use temper_components::player::chunk_receiver::ChunkReceiver; +use temper_core::pos::ChunkPos; +use temper_state::GlobalStateResource; + +pub fn handle( + state: Res, + query: Query<&ChunkReceiver>, + mut save_entity_writer: MessageWriter, +) { + let mut all_chunks: HashSet = HashSet::new(); + let mut visible_chunks = HashSet::new(); + 'chunk_iter: for chunk_candidate in state.0.world.get_cache() { + let (k, _v) = chunk_candidate.pair(); + all_chunks.insert(k.0); + for chunk_receiver in query.iter() { + if chunk_receiver.loaded.contains(&(k.0.x(), k.0.z())) { + visible_chunks.insert(k.0); + continue 'chunk_iter; + } + } + } + for chunk_pos in all_chunks.difference(&visible_chunks) { + save_entity_writer.write(temper_messages::save_chunk_entities::SaveChunkEntities( + *chunk_pos, + )); + } +} diff --git a/src/game_systems/src/background/src/lib.rs b/src/game_systems/src/background/src/lib.rs index 2f283f88..72b004dc 100644 --- a/src/game_systems/src/background/src/lib.rs +++ b/src/game_systems/src/background/src/lib.rs @@ -1,19 +1,15 @@ pub mod chunk_sending; pub mod chunk_unloader; pub mod connection_killer; +pub mod cross_chunk_border; pub mod day_cycle; +pub mod destroy_entity; +pub mod entity_sending; +pub mod entity_tracking; +pub mod entity_unloader; pub mod keep_alive_system; pub mod lan_pinger; pub mod mq; pub mod send_entity_updates; pub mod server_command; pub mod world_sync; - -pub fn register_background_systems(schedule: &mut bevy_ecs::prelude::Schedule) { - schedule.add_systems(chunk_sending::handle); - schedule.add_systems(connection_killer::connection_killer); - schedule.add_systems(day_cycle::tick_daylight_cycle); - schedule.add_systems(mq::process); - schedule.add_systems(send_entity_updates::handle); - schedule.add_systems(server_command::handle); -} diff --git a/src/game_systems/src/background/src/send_entity_updates.rs b/src/game_systems/src/background/src/send_entity_updates.rs index c2209ae9..55f4d908 100644 --- a/src/game_systems/src/background/src/send_entity_updates.rs +++ b/src/game_systems/src/background/src/send_entity_updates.rs @@ -1,7 +1,7 @@ -#![expect(clippy::type_complexity)] -use bevy_ecs::prelude::{MessageReader, Query}; +use bevy_ecs::prelude::{Entity, MessageReader, Query}; use temper_codec::net_types::angle::NetAngle; use temper_components::entity_identity::Identity; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::grounded::OnGround; use temper_components::player::position::Position; use temper_components::player::rotation::Rotation; @@ -15,15 +15,15 @@ use tracing::warn; pub fn handle( mut query: Query<( + Entity, &Position, &Velocity, &Rotation, &mut LastSyncedPosition, - Option<&Identity>, - Option<&Identity>, + &Identity, &OnGround, )>, - mut conn_query: Query<&StreamWriter>, + mut player_query: Query<(Entity, &StreamWriter, &EntityTracker)>, mut reader: MessageReader, ) { let mut entities_to_update = vec![]; @@ -31,23 +31,12 @@ pub fn handle( entities_to_update.push(msg.0); } for entity in entities_to_update { - if let Ok((pos, vel, rot, mut last_synced, entity_id_opt, player_id_opt, grounded)) = + if let Ok((entity, pos, vel, rot, mut last_synced, identity, grounded)) = query.get_mut(entity) { - let id = if let Some(entity_id) = entity_id_opt { - entity_id.entity_id - } else if let Some(player_id) = player_id_opt { - player_id.entity_id - } else { - warn!( - "Tried to send entity update for entity without identity: {:?}", - entity - ); - continue; - }; if last_synced.0.distance(pos.coords) >= 8.0 { let packet = TeleportEntityPacket { - entity_id: id.into(), + entity_id: identity.entity_id.into(), x: pos.x, y: pos.y, z: pos.z, @@ -58,8 +47,10 @@ pub fn handle( pitch: rot.pitch, on_ground: grounded.0, }; - for conn in conn_query.iter_mut() { - // TODO: Only send if the client is tracking this entity + for (recipient_entity, conn, tracker) in player_query.iter_mut() { + if recipient_entity == entity || !tracker.tracking.contains(&entity) { + continue; + } if let Err(e) = conn.send_packet_ref(&packet) { warn!( "Failed to send teleport packet for entity {:?}: {:?}", @@ -77,7 +68,7 @@ pub fn handle( ) }; let packet = UpdateEntityPositionAndRotationPacket { - entity_id: id.into(), + entity_id: identity.entity_id.into(), delta_x, delta_y, delta_z, @@ -85,8 +76,10 @@ pub fn handle( pitch: NetAngle::from_degrees(rot.pitch.into()), on_ground: grounded.0, }; - for conn in conn_query.iter_mut() { - // TODO: Only send if the client is tracking this entity + for (recipient_entity, conn, tracker) in player_query.iter_mut() { + if recipient_entity == entity || !tracker.tracking.contains(&entity) { + continue; + } if let Err(e) = conn.send_packet_ref(&packet) { warn!( "Failed to send entity update packet for entity {:?}: {:?}", diff --git a/src/game_systems/src/background/src/world_sync.rs b/src/game_systems/src/background/src/world_sync.rs index 95e45f9e..e63d4b90 100644 --- a/src/game_systems/src/background/src/world_sync.rs +++ b/src/game_systems/src/background/src/world_sync.rs @@ -32,9 +32,9 @@ pub fn sync_world( state: Res, mut last_synced: ResMut, ) { - if state.0.shut_down.load(std::sync::atomic::Ordering::Relaxed) { - return; - } + // if state.0.shut_down.load(std::sync::atomic::Ordering::Relaxed) { + // return; + // } // Always schedule a sync; frequency is handled by the schedule period. state.0.world.sync().expect("Failed to sync world"); diff --git a/src/game_systems/src/interactions/src/block_interactions.rs b/src/game_systems/src/interactions/src/block_interactions.rs index ddaa8a62..62b836eb 100644 --- a/src/game_systems/src/interactions/src/block_interactions.rs +++ b/src/game_systems/src/interactions/src/block_interactions.rs @@ -156,84 +156,3 @@ pub fn is_interactive(block_state_id: BlockStateId) -> bool { .and_then(get_interaction_type) .is_some() } - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - use temper_core::block_state_id::BlockStateId; - use temper_macros::block; - - #[test] - fn test_door_detection() { - let door_data = BlockData { - name: "minecraft:oak_door".to_string(), - properties: Some(BTreeMap::from([ - ("facing".to_string(), "north".to_string()), - ("open".to_string(), "false".to_string()), - ("half".to_string(), "lower".to_string()), - ("hinge".to_string(), "left".to_string()), - ])), - }; - - assert!(matches!( - get_interaction_type(&door_data), - Some(InteractionType::Toggleable("open")) - )); - } - - #[test] - fn test_try_interact_opens_door() { - // A closed oak door (lower half, north-facing, left hinge, unpowered) - let closed_door: BlockStateId = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); - - let result = try_interact(closed_door); - let InteractionResult::Toggled(new_id) = result else { - panic!("Expected Toggled, got {:?}", result); - }; - - let new_data = new_id - .to_block_data() - .expect("new state ID should be valid"); - let props = new_data.properties.expect("door should have properties"); - assert_eq!(props["open"], "true", "door should be open after interact"); - } - - #[test] - fn test_try_interact_closes_door() { - // An already-open oak door - let open_door: BlockStateId = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: true, powered: false }); - - let result = try_interact(open_door); - let InteractionResult::Toggled(new_id) = result else { - panic!("Expected Toggled, got {:?}", result); - }; - - let new_data = new_id - .to_block_data() - .expect("new state ID should be valid"); - let props = new_data.properties.expect("door should have properties"); - assert_eq!( - props["open"], "false", - "door should be closed after interact" - ); - } - - #[test] - fn test_try_interact_not_interactive() { - let stone: BlockStateId = block!("stone"); - assert!(matches!( - try_interact(stone), - InteractionResult::NotInteractive - )); - } - - #[test] - fn test_is_interactive() { - let door: BlockStateId = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); - let stone: BlockStateId = block!("stone"); - - assert!(is_interactive(door), "door should be interactive"); - assert!(!is_interactive(stone), "stone should not be interactive"); - } -} diff --git a/src/game_systems/src/lib.rs b/src/game_systems/src/lib.rs index 09b7f5fe..c9cedd60 100644 --- a/src/game_systems/src/lib.rs +++ b/src/game_systems/src/lib.rs @@ -1,10 +1,216 @@ -pub use background::{ - chunk_unloader, keep_alive_system, lan_pinger::LanPinger, register_background_systems, - world_sync, -}; -pub use mobs::register_mob_systems; -pub use packets::register_packet_handlers; -pub use physics::register_physics_systems; -pub use player::{register_player_systems, update_player_ping}; -pub use shutdown::register_shutdown_systems; -pub use world::register_world_systems; +use bevy_ecs::prelude::ApplyDeferred; +use bevy_ecs::schedule::{ExecutorKind, IntoScheduleConfigs, Schedule, SystemSet}; +use std::time::Duration; +use temper_commands::infrastructure::register_command_systems; +use temper_config::server_config::get_global_config; +use temper_scheduler::{drain_registered_schedules, MissedTickBehavior, Scheduler, TimedSchedule}; + +pub use background::lan_pinger::LanPinger; + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +enum TickPhase { + ChunkSending, + VisibleTracking, +} + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +enum ChunkGcPhase { + MarkForSave, + UnloadChunks, +} + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +enum ShutdownPhase { + EmitSaveMessages, + FlushWorld, + ShutdownPackets, +} + +// TODO: Clean this up with bevy's app thing +fn register_tick_systems(schedule: &mut Schedule) { + schedule.configure_sets( + ( + TickPhase::ChunkSending, + mobs::MobLoadSystems, + TickPhase::VisibleTracking, + ) + .chain(), + ); + + schedule.add_systems(packets::chunk_batch_ack::handle); + schedule.add_systems(packets::confirm_player_teleport::handle); + schedule.add_systems(packets::keep_alive::handle); + schedule.add_systems(packets::place_block::handle); + schedule.add_systems(interactions::interaction_listener::handle_block_interact); + schedule.add_systems(interactions::door_interaction::handle_door_toggled); + schedule.add_systems(packets::player_action::handle); + schedule.add_systems(packets::player_command::handle); + schedule.add_systems(packets::player_input::handle); + schedule.add_systems(packets::set_player_position::handle); + schedule.add_systems(packets::set_player_position_and_rotation::handle); + schedule.add_systems(packets::set_player_rotation::handle); + schedule.add_systems(packets::swing_arm::handle); + schedule.add_systems(packets::update_survival_mode_slot::handle); + schedule.add_systems(packets::close_container::handle); + schedule.add_systems(packets::player_loaded::handle); + schedule.add_systems(packets::command::handle); + schedule.add_systems(packets::command_suggestions::handle); + schedule.add_systems(packets::chat_message::handle); + schedule.add_systems(packets::set_creative_mode_slot::handle); + schedule.add_systems(packets::set_held_item::handle); + schedule.add_systems(packets::player_abilities::handle); + schedule.add_systems(packets::change_game_mode::handle); + schedule.add_systems(packets::pick_item_from_block::handle); + + schedule.add_systems(player::digging_system::handle_start_digging); + schedule.add_systems(player::digging_system::handle_finish_digging); + schedule.add_systems(player::digging_system::handle_start_digging); + schedule.add_systems(player::digging_system::handle_cancel_digging); + schedule.add_systems(player::entity_spawn::handle_spawn_entity); + schedule.add_systems(player::entity_spawn::spawn_command_processor); + schedule.add_systems(player::gamemode_change::handle); + schedule.add_systems(player::movement_broadcast::handle_player_move); + + schedule.add_systems( + ( + player::new_connections::accept_new_connections, + ApplyDeferred, + player::chunk_calculator::handle, + player::emit_player_joined::emit_player_joined, + player::player_spawn::handle, + ) + .chain(), + ); + schedule.add_systems(player::player_despawn::handle); + schedule.add_systems(player::player_join_message::handle); + schedule.add_systems(player::player_leave_message::handle); + schedule.add_systems(player::player_swimming::detect_player_swimming); + schedule.add_systems(player::player_tp::teleport_player); + schedule.add_systems(player::send_inventory_updates::handle_inventory_updates); + + register_command_systems(schedule); + + schedule.add_systems(background::chunk_sending::handle.in_set(TickPhase::ChunkSending)); + mobs::register_load_systems(schedule); + schedule.add_systems( + ( + background::entity_tracking::refresh_visible_entities, + background::entity_sending::send_untracked_entities, + background::entity_sending::send_new_entities, + background::send_entity_updates::handle, + ) + .chain() + .in_set(TickPhase::VisibleTracking), + ); + schedule.add_systems(background::connection_killer::connection_killer); + schedule.add_systems(background::day_cycle::tick_daylight_cycle); + schedule.add_systems(background::mq::process); + schedule.add_systems(background::server_command::handle); + schedule.add_systems(background::destroy_entity::destroy_entity_system); + + schedule.add_systems( + ( + physics::unground::handle, + physics::gravity::handle, + physics::drag::handle, + physics::velocity::handle, + physics::collisions::handle, + physics::chunk_boundary::handle, + background::cross_chunk_border::cross_chunk_border, + ) + .chain(), + ); + mobs::register_tick_systems(schedule); + + schedule.add_systems(world::particles::handle); +} + +fn register_world_sync_schedule_systems(schedule: &mut Schedule) { + schedule.add_systems(background::world_sync::sync_world); +} + +fn register_chunk_gc_schedule_systems(schedule: &mut Schedule) { + schedule.set_executor_kind(ExecutorKind::SingleThreaded); + schedule.configure_sets( + ( + ChunkGcPhase::MarkForSave, + mobs::MobSaveSystems, + ChunkGcPhase::UnloadChunks, + ) + .chain(), + ); + schedule.add_systems(background::entity_unloader::handle.in_set(ChunkGcPhase::MarkForSave)); + mobs::register_save_systems(schedule); + schedule.add_systems(background::chunk_unloader::handle.in_set(ChunkGcPhase::UnloadChunks)); +} + +fn register_keepalive_schedule_systems(schedule: &mut Schedule) { + schedule.add_systems(background::keep_alive_system::keep_alive_system); + schedule.add_systems(player::update_player_ping::handle); +} + +pub fn register_schedules(timed: &mut Scheduler, shutdown_schedule: &mut Schedule) { + let build_tick = |schedule: &mut Schedule| { + schedule.set_executor_kind(ExecutorKind::SingleThreaded); + register_tick_systems(schedule); + }; + let tick_period = Duration::from_secs(1) / get_global_config().tps; + timed.register( + TimedSchedule::new("tick", tick_period, build_tick) + .with_behavior(MissedTickBehavior::Burst) + .with_max_catch_up(5), + ); + + timed.register( + TimedSchedule::new( + "world_sync", + Duration::from_secs(15), + register_world_sync_schedule_systems, + ) + .with_behavior(MissedTickBehavior::Skip), + ); + + timed.register( + TimedSchedule::new( + "chunk_gc", + Duration::from_secs(5), + register_chunk_gc_schedule_systems, + ) + .with_behavior(MissedTickBehavior::Skip), + ); + + timed.register( + TimedSchedule::new( + "keepalive", + Duration::from_secs(1), + register_keepalive_schedule_systems, + ) + .with_behavior(MissedTickBehavior::Skip) + .with_phase(Duration::from_millis(250)), + ); + shutdown_schedule.set_executor_kind(ExecutorKind::SingleThreaded); + + // Force the chunk-saving systems to run before the world flushing and shutdown packet sending systems; + // otherwise we might end up with a world not fully saved if the server is killed at the wrong time during shutdown + shutdown_schedule.configure_sets( + ( + ShutdownPhase::EmitSaveMessages, + mobs::MobSaveSystems, + ShutdownPhase::FlushWorld, + ShutdownPhase::ShutdownPackets, + ) + .chain(), + ); + shutdown_schedule.add_systems( + shutdown::send_save_message::send_save_message.in_set(ShutdownPhase::EmitSaveMessages), + ); + mobs::register_save_systems(shutdown_schedule); + shutdown_schedule + .add_systems(background::world_sync::sync_world.in_set(ShutdownPhase::FlushWorld)); + shutdown_schedule + .add_systems(shutdown::send_shutdown_packet::handle.in_set(ShutdownPhase::ShutdownPackets)); + + for pending in drain_registered_schedules() { + timed.register(pending.into_timed()); + } +} diff --git a/src/game_systems/src/mobs/Cargo.toml b/src/game_systems/src/mobs/Cargo.toml index 6121c98f..6ad290fb 100644 --- a/src/game_systems/src/mobs/Cargo.toml +++ b/src/game_systems/src/mobs/Cargo.toml @@ -6,4 +6,14 @@ edition = "2024" [dependencies] bevy_ecs = { workspace = true } temper-components = { workspace = true } -temper-entities = { workspace = true } \ No newline at end of file +temper-entities = { workspace = true } +temper-messages = { workspace = true } +temper-state = { workspace = true } +temper-core = { workspace = true } +bitcode = { workspace = true } +tracing = { workspace = true } +temper-protocol = { workspace = true } +temper-net-runtime = { workspace = true } +temper-macros = { workspace = true } +temper-config = { workspace = true } +paste = { workspace = true } diff --git a/src/game_systems/src/mobs/src/collision_only.rs b/src/game_systems/src/mobs/src/collision_only.rs new file mode 100644 index 00000000..6ea4ef21 --- /dev/null +++ b/src/game_systems/src/mobs/src/collision_only.rs @@ -0,0 +1,219 @@ +use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule}; + +crate::define_standard_mob_save_load!( + allay, + marker = temper_entities::markers::entity_types::Allay, + bundle = temper_entities::AllayBundle, + entity_type = Allay, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + bat, + marker = temper_entities::markers::entity_types::Bat, + bundle = temper_entities::BatBundle, + entity_type = Bat, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + bee, + marker = temper_entities::markers::entity_types::Bee, + bundle = temper_entities::BeeBundle, + entity_type = Bee, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + blaze, + marker = temper_entities::markers::entity_types::Blaze, + bundle = temper_entities::BlazeBundle, + entity_type = Blaze, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + breeze, + marker = temper_entities::markers::entity_types::Breeze, + bundle = temper_entities::BreezeBundle, + entity_type = Breeze, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + cod, + marker = temper_entities::markers::entity_types::Cod, + bundle = temper_entities::CodBundle, + entity_type = Cod, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + dolphin, + marker = temper_entities::markers::entity_types::Dolphin, + bundle = temper_entities::DolphinBundle, + entity_type = Dolphin, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + drowned, + marker = temper_entities::markers::entity_types::Drowned, + bundle = temper_entities::DrownedBundle, + entity_type = Drowned, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + elder_guardian, + marker = temper_entities::markers::entity_types::ElderGuardian, + bundle = temper_entities::ElderGuardianBundle, + entity_type = ElderGuardian, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + ghast, + marker = temper_entities::markers::entity_types::Ghast, + bundle = temper_entities::GhastBundle, + entity_type = Ghast, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + glow_squid, + marker = temper_entities::markers::entity_types::GlowSquid, + bundle = temper_entities::GlowSquidBundle, + entity_type = GlowSquid, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + guardian, + marker = temper_entities::markers::entity_types::Guardian, + bundle = temper_entities::GuardianBundle, + entity_type = Guardian, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + parrot, + marker = temper_entities::markers::entity_types::Parrot, + bundle = temper_entities::ParrotBundle, + entity_type = Parrot, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + phantom, + marker = temper_entities::markers::entity_types::Phantom, + bundle = temper_entities::PhantomBundle, + entity_type = Phantom, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + pufferfish, + marker = temper_entities::markers::entity_types::Pufferfish, + bundle = temper_entities::PufferfishBundle, + entity_type = Pufferfish, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + salmon, + marker = temper_entities::markers::entity_types::Salmon, + bundle = temper_entities::SalmonBundle, + entity_type = Salmon, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + squid, + marker = temper_entities::markers::entity_types::Squid, + bundle = temper_entities::SquidBundle, + entity_type = Squid, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + tadpole, + marker = temper_entities::markers::entity_types::Tadpole, + bundle = temper_entities::TadpoleBundle, + entity_type = Tadpole, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + tropical_fish, + marker = temper_entities::markers::entity_types::TropicalFish, + bundle = temper_entities::TropicalFishBundle, + entity_type = TropicalFish, + runtime_components = (temper_entities::markers::HasCollisions) +); + +crate::define_standard_mob_save_load!( + vex, + marker = temper_entities::markers::entity_types::Vex, + bundle = temper_entities::VexBundle, + entity_type = Vex, + runtime_components = (temper_entities::markers::HasCollisions) +); + +pub fn register_load_systems(schedule: &mut Schedule) { + crate::add_systems_to_set!( + schedule, + crate::MobLoadSystems, + [ + load_allay, + load_bat, + load_bee, + load_blaze, + load_breeze, + load_cod, + load_dolphin, + load_drowned, + load_elder_guardian, + load_ghast, + load_glow_squid, + load_guardian, + load_parrot, + load_phantom, + load_pufferfish, + load_salmon, + load_squid, + load_tadpole, + load_tropical_fish, + load_vex, + ] + ); +} + +pub fn register_save_systems(schedule: &mut Schedule) { + crate::add_systems_to_set!( + schedule, + crate::MobSaveSystems, + [ + save_allay, + save_bat, + save_bee, + save_blaze, + save_breeze, + save_cod, + save_dolphin, + save_drowned, + save_elder_guardian, + save_ghast, + save_glow_squid, + save_guardian, + save_parrot, + save_phantom, + save_pufferfish, + save_salmon, + save_squid, + save_tadpole, + save_tropical_fish, + save_vex, + ] + ); +} diff --git a/src/game_systems/src/mobs/src/gravity_no_drag.rs b/src/game_systems/src/mobs/src/gravity_no_drag.rs new file mode 100644 index 00000000..63d7a390 --- /dev/null +++ b/src/game_systems/src/mobs/src/gravity_no_drag.rs @@ -0,0 +1,61 @@ +use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule}; + +crate::define_standard_mob_save_load!( + axolotl, + marker = temper_entities::markers::entity_types::Axolotl, + bundle = temper_entities::AxolotlBundle, + entity_type = Axolotl, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions + ) +); + +crate::define_standard_mob_save_load!( + magma_cube, + marker = temper_entities::markers::entity_types::MagmaCube, + bundle = temper_entities::MagmaCubeBundle, + entity_type = MagmaCube, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions + ) +); + +crate::define_standard_mob_save_load!( + slime, + marker = temper_entities::markers::entity_types::Slime, + bundle = temper_entities::SlimeBundle, + entity_type = Slime, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions + ) +); + +crate::define_standard_mob_save_load!( + strider, + marker = temper_entities::markers::entity_types::Strider, + bundle = temper_entities::StriderBundle, + entity_type = Strider, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions + ) +); + +pub fn register_load_systems(schedule: &mut Schedule) { + crate::add_systems_to_set!( + schedule, + crate::MobLoadSystems, + [load_axolotl, load_magma_cube, load_slime, load_strider,] + ); +} + +pub fn register_save_systems(schedule: &mut Schedule) { + crate::add_systems_to_set!( + schedule, + crate::MobSaveSystems, + [save_axolotl, save_magma_cube, save_slime, save_strider,] + ); +} diff --git a/src/game_systems/src/mobs/src/ground.rs b/src/game_systems/src/mobs/src/ground.rs new file mode 100644 index 00000000..52f5082e --- /dev/null +++ b/src/game_systems/src/mobs/src/ground.rs @@ -0,0 +1,789 @@ +use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule}; + +crate::define_standard_mob_save_load!( + armadillo, + marker = temper_entities::markers::entity_types::Armadillo, + bundle = temper_entities::ArmadilloBundle, + entity_type = Armadillo, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + bogged, + marker = temper_entities::markers::entity_types::Bogged, + bundle = temper_entities::BoggedBundle, + entity_type = Bogged, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + camel, + marker = temper_entities::markers::entity_types::Camel, + bundle = temper_entities::CamelBundle, + entity_type = Camel, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + cat, + marker = temper_entities::markers::entity_types::Cat, + bundle = temper_entities::CatBundle, + entity_type = Cat, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + cave_spider, + marker = temper_entities::markers::entity_types::CaveSpider, + bundle = temper_entities::CaveSpiderBundle, + entity_type = CaveSpider, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + chicken, + marker = temper_entities::markers::entity_types::Chicken, + bundle = temper_entities::ChickenBundle, + entity_type = Chicken, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + cow, + marker = temper_entities::markers::entity_types::Cow, + bundle = temper_entities::CowBundle, + entity_type = Cow, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + creaking, + marker = temper_entities::markers::entity_types::Creaking, + bundle = temper_entities::CreakingBundle, + entity_type = Creaking, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + creeper, + marker = temper_entities::markers::entity_types::Creeper, + bundle = temper_entities::CreeperBundle, + entity_type = Creeper, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + donkey, + marker = temper_entities::markers::entity_types::Donkey, + bundle = temper_entities::DonkeyBundle, + entity_type = Donkey, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + enderman, + marker = temper_entities::markers::entity_types::Enderman, + bundle = temper_entities::EndermanBundle, + entity_type = Enderman, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + endermite, + marker = temper_entities::markers::entity_types::Endermite, + bundle = temper_entities::EndermiteBundle, + entity_type = Endermite, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + evoker, + marker = temper_entities::markers::entity_types::Evoker, + bundle = temper_entities::EvokerBundle, + entity_type = Evoker, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + fox, + marker = temper_entities::markers::entity_types::Fox, + bundle = temper_entities::FoxBundle, + entity_type = Fox, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + frog, + marker = temper_entities::markers::entity_types::Frog, + bundle = temper_entities::FrogBundle, + entity_type = Frog, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + goat, + marker = temper_entities::markers::entity_types::Goat, + bundle = temper_entities::GoatBundle, + entity_type = Goat, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + hoglin, + marker = temper_entities::markers::entity_types::Hoglin, + bundle = temper_entities::HoglinBundle, + entity_type = Hoglin, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + horse, + marker = temper_entities::markers::entity_types::Horse, + bundle = temper_entities::HorseBundle, + entity_type = Horse, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + husk, + marker = temper_entities::markers::entity_types::Husk, + bundle = temper_entities::HuskBundle, + entity_type = Husk, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + iron_golem, + marker = temper_entities::markers::entity_types::IronGolem, + bundle = temper_entities::IronGolemBundle, + entity_type = IronGolem, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + llama, + marker = temper_entities::markers::entity_types::Llama, + bundle = temper_entities::LlamaBundle, + entity_type = Llama, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + mooshroom, + marker = temper_entities::markers::entity_types::Mooshroom, + bundle = temper_entities::MooshroomBundle, + entity_type = Mooshroom, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + mule, + marker = temper_entities::markers::entity_types::Mule, + bundle = temper_entities::MuleBundle, + entity_type = Mule, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + ocelot, + marker = temper_entities::markers::entity_types::Ocelot, + bundle = temper_entities::OcelotBundle, + entity_type = Ocelot, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + panda, + marker = temper_entities::markers::entity_types::Panda, + bundle = temper_entities::PandaBundle, + entity_type = Panda, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + pig, + marker = temper_entities::markers::entity_types::Pig, + bundle = temper_entities::PigBundle, + entity_type = Pig, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + piglin, + marker = temper_entities::markers::entity_types::Piglin, + bundle = temper_entities::PiglinBundle, + entity_type = Piglin, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + piglin_brute, + marker = temper_entities::markers::entity_types::PiglinBrute, + bundle = temper_entities::PiglinBruteBundle, + entity_type = PiglinBrute, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + pillager, + marker = temper_entities::markers::entity_types::Pillager, + bundle = temper_entities::PillagerBundle, + entity_type = Pillager, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + polar_bear, + marker = temper_entities::markers::entity_types::PolarBear, + bundle = temper_entities::PolarBearBundle, + entity_type = PolarBear, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + rabbit, + marker = temper_entities::markers::entity_types::Rabbit, + bundle = temper_entities::RabbitBundle, + entity_type = Rabbit, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + ravager, + marker = temper_entities::markers::entity_types::Ravager, + bundle = temper_entities::RavagerBundle, + entity_type = Ravager, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + sheep, + marker = temper_entities::markers::entity_types::Sheep, + bundle = temper_entities::SheepBundle, + entity_type = Sheep, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + shulker, + marker = temper_entities::markers::entity_types::Shulker, + bundle = temper_entities::ShulkerBundle, + entity_type = Shulker, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + silverfish, + marker = temper_entities::markers::entity_types::Silverfish, + bundle = temper_entities::SilverfishBundle, + entity_type = Silverfish, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + skeleton, + marker = temper_entities::markers::entity_types::Skeleton, + bundle = temper_entities::SkeletonBundle, + entity_type = Skeleton, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + skeleton_horse, + marker = temper_entities::markers::entity_types::SkeletonHorse, + bundle = temper_entities::SkeletonHorseBundle, + entity_type = SkeletonHorse, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + sniffer, + marker = temper_entities::markers::entity_types::Sniffer, + bundle = temper_entities::SnifferBundle, + entity_type = Sniffer, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + snow_golem, + marker = temper_entities::markers::entity_types::SnowGolem, + bundle = temper_entities::SnowGolemBundle, + entity_type = SnowGolem, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + spider, + marker = temper_entities::markers::entity_types::Spider, + bundle = temper_entities::SpiderBundle, + entity_type = Spider, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + stray, + marker = temper_entities::markers::entity_types::Stray, + bundle = temper_entities::StrayBundle, + entity_type = Stray, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + trader_llama, + marker = temper_entities::markers::entity_types::TraderLlama, + bundle = temper_entities::TraderLlamaBundle, + entity_type = TraderLlama, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + turtle, + marker = temper_entities::markers::entity_types::Turtle, + bundle = temper_entities::TurtleBundle, + entity_type = Turtle, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + villager, + marker = temper_entities::markers::entity_types::Villager, + bundle = temper_entities::VillagerBundle, + entity_type = Villager, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + vindicator, + marker = temper_entities::markers::entity_types::Vindicator, + bundle = temper_entities::VindicatorBundle, + entity_type = Vindicator, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + wandering_trader, + marker = temper_entities::markers::entity_types::WanderingTrader, + bundle = temper_entities::WanderingTraderBundle, + entity_type = WanderingTrader, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + warden, + marker = temper_entities::markers::entity_types::Warden, + bundle = temper_entities::WardenBundle, + entity_type = Warden, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + witch, + marker = temper_entities::markers::entity_types::Witch, + bundle = temper_entities::WitchBundle, + entity_type = Witch, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + wither_skeleton, + marker = temper_entities::markers::entity_types::WitherSkeleton, + bundle = temper_entities::WitherSkeletonBundle, + entity_type = WitherSkeleton, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + wolf, + marker = temper_entities::markers::entity_types::Wolf, + bundle = temper_entities::WolfBundle, + entity_type = Wolf, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + zoglin, + marker = temper_entities::markers::entity_types::Zoglin, + bundle = temper_entities::ZoglinBundle, + entity_type = Zoglin, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + zombie, + marker = temper_entities::markers::entity_types::Zombie, + bundle = temper_entities::ZombieBundle, + entity_type = Zombie, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + zombie_horse, + marker = temper_entities::markers::entity_types::ZombieHorse, + bundle = temper_entities::ZombieHorseBundle, + entity_type = ZombieHorse, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + zombie_villager, + marker = temper_entities::markers::entity_types::ZombieVillager, + bundle = temper_entities::ZombieVillagerBundle, + entity_type = ZombieVillager, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +crate::define_standard_mob_save_load!( + zombified_piglin, + marker = temper_entities::markers::entity_types::ZombifiedPiglin, + bundle = temper_entities::ZombifiedPiglinBundle, + entity_type = ZombifiedPiglin, + runtime_components = ( + temper_entities::markers::HasGravity, + temper_entities::markers::HasCollisions, + temper_entities::markers::HasWaterDrag + ) +); + +pub fn register_load_systems(schedule: &mut Schedule) { + crate::add_systems_to_set!( + schedule, + crate::MobLoadSystems, + [ + load_armadillo, + load_bogged, + load_camel, + load_cat, + load_cave_spider, + load_chicken, + load_cow, + load_creaking, + load_creeper, + load_donkey, + load_enderman, + load_endermite, + load_evoker, + load_fox, + load_frog, + load_goat, + load_hoglin, + load_horse, + load_husk, + load_iron_golem, + load_llama, + load_mooshroom, + load_mule, + load_ocelot, + load_panda, + load_pig, + load_piglin, + load_piglin_brute, + load_pillager, + load_polar_bear, + load_rabbit, + load_ravager, + load_sheep, + load_shulker, + load_silverfish, + load_skeleton, + load_skeleton_horse, + load_sniffer, + load_snow_golem, + load_spider, + load_stray, + load_trader_llama, + load_turtle, + load_villager, + load_vindicator, + load_wandering_trader, + load_warden, + load_witch, + load_wither_skeleton, + load_wolf, + load_zoglin, + load_zombie, + load_zombie_horse, + load_zombie_villager, + load_zombified_piglin, + ] + ); +} + +pub fn register_save_systems(schedule: &mut Schedule) { + crate::add_systems_to_set!( + schedule, + crate::MobSaveSystems, + [ + save_armadillo, + save_bogged, + save_camel, + save_cat, + save_cave_spider, + save_chicken, + save_cow, + save_creaking, + save_creeper, + save_donkey, + save_enderman, + save_endermite, + save_evoker, + save_fox, + save_frog, + save_goat, + save_hoglin, + save_horse, + save_husk, + save_iron_golem, + save_llama, + save_mooshroom, + save_mule, + save_ocelot, + save_panda, + save_pig, + save_piglin, + save_piglin_brute, + save_pillager, + save_polar_bear, + save_rabbit, + save_ravager, + save_sheep, + save_shulker, + save_silverfish, + save_skeleton, + save_skeleton_horse, + save_sniffer, + save_snow_golem, + save_spider, + save_stray, + save_trader_llama, + save_turtle, + save_villager, + save_vindicator, + save_wandering_trader, + save_warden, + save_witch, + save_wither_skeleton, + save_wolf, + save_zoglin, + save_zombie, + save_zombie_horse, + save_zombie_villager, + save_zombified_piglin, + ] + ); +} diff --git a/src/game_systems/src/mobs/src/lib.rs b/src/game_systems/src/mobs/src/lib.rs index 9ae3afb7..5833020d 100644 --- a/src/game_systems/src/mobs/src/lib.rs +++ b/src/game_systems/src/mobs/src/lib.rs @@ -1,6 +1,249 @@ -use bevy_ecs::prelude::Schedule; +//! How this binfire works: +//! - `define_entity_save_load` generates a pair of save/load systems for a specific entity bundle. +//! The generated save system listens for `SaveChunkEntities` and writes matching entities into +//! the chunk entity map in world state. +//! - The generated load system listens for `LoadChunkEntities`, deserializes matching saved +//! entities from the chunk, and respawns them with the required runtime-only marker/components. +//! - `define_standard_mob_save_load` is a wrapper that supplies the standard persisted +//! fields used by most mobs. You don't have to use this if you have a weird mob with different +//! persisted fields, but it should cover most cases and saves a lot of boilerplate. +//! +//! The macro invocations themselves live in the category modules: +//! - `ground` for mobs with gravity, collisions, and water drag +//! - `collision_only` for mobs that only need collisions +//! - `gravity_no_drag` for mobs with gravity/collisions but no water drag +//! +//! Those module-level macro invocations generate the concrete `save_*` and `load_*` systems. +//! The `register_load_systems` and `register_save_systems` functions in this file do not generate +//! systems; they just ask each category module to add its already-generated systems to the +//! appropriate Bevy system set. -mod pig; -pub fn register_mob_systems(schedule: &mut Schedule) { - schedule.add_systems(pig::tick_pig); +use bevy_ecs::schedule::{Schedule, SystemSet}; + +pub mod collision_only; +pub mod gravity_no_drag; +pub mod ground; + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct MobLoadSystems; + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct MobSaveSystems; + +pub fn register_tick_systems(_schedule: &mut Schedule) {} + +pub fn register_load_systems(schedule: &mut Schedule) { + ground::register_load_systems(schedule); + collision_only::register_load_systems(schedule); + gravity_no_drag::register_load_systems(schedule); +} + +pub fn register_save_systems(schedule: &mut Schedule) { + ground::register_save_systems(schedule); + collision_only::register_save_systems(schedule); + gravity_no_drag::register_save_systems(schedule); +} + +#[macro_export] +macro_rules! add_systems_to_set { + ($schedule:expr, $set:path, [ $( $system:path ),+ $(,)? ]) => { + $( + $schedule.add_systems($system.in_set($set)); + )+ + }; +} + +#[macro_export] +macro_rules! define_standard_mob_save_load { + ( + $name:ident, + marker = $marker:path, + bundle = $bundle:path, + entity_type = $entity_type:ident, + runtime_components = ( $( $runtime_component:path ),* $(,)? ) + ) => { + $crate::define_entity_save_load!( + $name, + marker = $marker, + bundle = $bundle, + entity_type = $entity_type, + runtime_components = ( $( $runtime_component ),* ), + fields = { + identity: temper_components::entity_identity::Identity => clone, + metadata: temper_components::metadata::EntityMetadata => copy, + combat: temper_components::combat::CombatProperties => copy, + spawn: temper_components::spawn::SpawnProperties => clone, + position: temper_components::player::position::Position => copy, + rotation: temper_components::player::rotation::Rotation => copy, + velocity: temper_components::player::velocity::Velocity => copy, + on_ground: temper_components::player::grounded::OnGround => copy, + last_synced_position: temper_components::last_synced_position::LastSyncedPosition => copy, + } + ); + }; +} + +/// Generates chunk save/load systems for an entity bundle. +/// +/// The macro expects the persisted field list to include: +/// - `identity`, used as the saved entity key +/// - `position`, used to determine the entity's chunk +/// +/// Field modes: +/// - `clone` expands to `field.clone()` +/// - `copy` expands to `*field` +/// +/// Example: +/// ```ignore +/// define_entity_save_load!( +/// pig, +/// marker = Pig, +/// bundle = PigBundle, +/// entity_type = Pig, +/// runtime_components = (HasGravity, HasCollisions, HasWaterDrag), +/// fields = { +/// identity: Identity => clone, +/// metadata: EntityMetadata => copy, +/// combat: CombatProperties => copy, +/// spawn: SpawnProperties => clone, +/// position: Position => copy, +/// rotation: Rotation => copy, +/// velocity: Velocity => copy, +/// on_ground: OnGround => copy, +/// last_synced_position: LastSyncedPosition => copy, +/// } +/// ); +/// ``` +#[macro_export] +macro_rules! define_entity_save_load { + (@field_value $field:ident, clone) => { + $field.clone() + }; + (@field_value $field:ident, copy) => { + *$field + }; + ( + $name:ident, + marker = $marker:path, + bundle = $bundle:path, + entity_type = $entity_type:ident, + runtime_components = ( $( $runtime_component:path ),* $(,)? ), + fields = { + $( + $field:ident : $field_ty:path => $mode:ident + ),* $(,)? + } + ) => { + paste::paste! { + pub fn []( + state: bevy_ecs::prelude::Res, + query: bevy_ecs::prelude::Query< + ( + $( &$field_ty, )* + ), + bevy_ecs::prelude::With<$marker>, + >, + mut reader: bevy_ecs::prelude::MessageReader< + temper_messages::save_chunk_entities::SaveChunkEntities, + >, + ) { + for message in reader.read() { + for ($($field,)*) in query.iter() { + let bundle = $bundle { + $( + $field: $crate::define_entity_save_load!(@field_value $field, $mode), + )* + }; + + if bundle.position.chunk() != message.0 { + continue; + } + + tracing::debug!( + "Saving {} with UUID {} at chunk {}", + stringify!($name), + bundle.identity.uuid, + message.0 + ); + + let chunk = state + .0 + .world + .get_or_generate_chunk( + message.0, + temper_core::dimension::Dimension::Overworld, + ) + .expect("Failed to get or generate chunk"); + + chunk.entities.insert( + bundle.identity.uuid, + ( + temper_entities::entity_types::EntityTypeEnum::$entity_type, + bitcode::serialize(&bundle) + .expect("Failed to serialize entity bundle"), + ), + ); + chunk.mark_dirty(); + } + } + } + + #[expect(unused_variables)] + pub fn []( + state: bevy_ecs::prelude::Res, + mut cmd: bevy_ecs::prelude::Commands, + mut reader: bevy_ecs::prelude::MessageReader< + temper_messages::load_chunk_entities::LoadChunkEntities, + >, + players: bevy_ecs::prelude::Query< + ( + &temper_net_runtime::connection::StreamWriter, + &temper_components::player::position::Position, + &temper_components::player::client_information::ClientInformationComponent, + ), + bevy_ecs::prelude::With< + temper_components::player::player_marker::PlayerMarker, + >, + >, + ) { + for message in reader.read() { + let Ok(chunk) = state + .0 + .world + .get_chunk( + message.0, + temper_core::dimension::Dimension::Overworld, + ) + else { + tracing::error!("Failed to load chunk {} for entity loading", message.0); + continue; + }; + + for kv in chunk.entities.iter() { + let (entity_type, data) = kv.value(); + if *entity_type + == temper_entities::entity_types::EntityTypeEnum::$entity_type + { + tracing::debug!( + "Loading entity of type {:?} from chunk {}", + entity_type, + message.0 + ); + let bundle: $bundle = bitcode::deserialize(data) + .expect("Failed to deserialize entity bundle"); + let last_chunk = temper_components::last_chunk_pos::LastChunkPos::new( + bundle.position.chunk(), + ); + cmd.spawn(( + bundle, + $marker, + $( $runtime_component, )* + last_chunk, + )); + } + } + } + } + } + }; } diff --git a/src/game_systems/src/mobs/src/pig.rs b/src/game_systems/src/mobs/src/pig.rs deleted file mode 100644 index 9cccc4cc..00000000 --- a/src/game_systems/src/mobs/src/pig.rs +++ /dev/null @@ -1,7 +0,0 @@ -use bevy_ecs::prelude::{Query, With}; -use temper_components::entity_identity::Identity; -use temper_components::player::position::Position; -use temper_entities::markers::entity_types::Pig; - -#[expect(unused_variables)] -pub fn tick_pig(query: Query<&Position, With>, players: Query<&Position, With>) {} diff --git a/src/game_systems/src/packets/src/lib.rs b/src/game_systems/src/packets/src/lib.rs index d2010f5e..72c02eb5 100644 --- a/src/game_systems/src/packets/src/lib.rs +++ b/src/game_systems/src/packets/src/lib.rs @@ -1,55 +1,24 @@ -use bevy_ecs::schedule::Schedule; - -mod change_game_mode; -mod chat_message; -mod chunk_batch_ack; +pub mod change_game_mode; +pub mod chat_message; +pub mod chunk_batch_ack; pub mod command; -mod command_suggestions; -mod confirm_player_teleport; -mod keep_alive; -mod pick_item_from_block; -mod place_block; -mod player_abilities; -mod player_action; -mod player_command; +pub mod command_suggestions; +pub mod confirm_player_teleport; +pub mod keep_alive; +pub mod pick_item_from_block; +pub mod place_block; +pub mod player_abilities; +pub mod player_action; +pub mod player_command; pub mod player_input; -mod player_loaded; -mod set_player_position; -mod set_player_position_and_rotation; -mod set_player_rotation; -mod swing_arm; - -pub fn register_packet_handlers(schedule: &mut Schedule) { - // Added separately so if we mess up the signature of one of the systems we can know exactly - // which one - schedule.add_systems(chunk_batch_ack::handle); - schedule.add_systems(confirm_player_teleport::handle); - schedule.add_systems(keep_alive::handle); - schedule.add_systems(place_block::handle); - schedule.add_systems(interactions::interaction_listener::handle_block_interact); - schedule.add_systems(interactions::door_interaction::handle_door_toggled); - schedule.add_systems(player_action::handle); - schedule.add_systems(player_command::handle); - schedule.add_systems(player_input::handle); - schedule.add_systems(set_player_position::handle); - schedule.add_systems(set_player_position_and_rotation::handle); - schedule.add_systems(set_player_rotation::handle); - schedule.add_systems(swing_arm::handle); - schedule.add_systems(update_survival_mode_slot::handle); - schedule.add_systems(close_container::handle); - schedule.add_systems(player_loaded::handle); - schedule.add_systems(command::handle); - schedule.add_systems(command_suggestions::handle); - schedule.add_systems(chat_message::handle); - schedule.add_systems(set_creative_mode_slot::handle); - schedule.add_systems(set_held_item::handle); - schedule.add_systems(player_abilities::handle); - schedule.add_systems(change_game_mode::handle); - schedule.add_systems(pick_item_from_block::handle); -} +pub mod player_loaded; +pub mod set_player_position; +pub mod set_player_position_and_rotation; +pub mod set_player_rotation; +pub mod swing_arm; pub mod set_creative_mode_slot; -mod close_container; +pub mod close_container; pub mod set_held_item; -mod update_survival_mode_slot; +pub mod update_survival_mode_slot; diff --git a/src/game_systems/src/packets/src/player_command.rs b/src/game_systems/src/packets/src/player_command.rs index ad443298..bfa1a85b 100644 --- a/src/game_systems/src/packets/src/player_command.rs +++ b/src/game_systems/src/packets/src/player_command.rs @@ -1,7 +1,7 @@ use bevy_ecs::prelude::{Entity, Query, Res}; use temper_codec::net_types::var_int::VarInt; use temper_components::entity_identity::Identity; -use temper_net_runtime::broadcast::broadcast_packet_except; +use temper_components::player::entity_tracker::EntityTracker; use temper_net_runtime::connection::StreamWriter; use temper_protocol::PlayerCommandPacketReceiver; use temper_protocol::incoming::player_command::PlayerCommandAction; @@ -12,7 +12,7 @@ use tracing::trace; /// Note: Sneaking is handled via PlayerInput packet, NOT here pub fn handle( receiver: Res, - conn_query: Query<(Entity, &StreamWriter)>, + conn_query: Query<(Entity, &StreamWriter, &EntityTracker)>, identity_query: Query<&Identity>, ) { for (event, eid) in receiver.0.try_iter() { @@ -34,12 +34,28 @@ pub fn handle( PlayerCommandAction::StartSprinting => { let packet = EntityMetadataPacket::new(entity_id, [EntityMetadata::entity_sprinting()]); - broadcast_packet_except(eid, &packet, conn_query.iter()); + for (recipient, writer, tracker) in conn_query.iter() { + if recipient == eid || !writer.is_running() || !tracker.tracking.contains(&eid) + { + continue; + } + if let Err(err) = writer.send_packet_ref(&packet) { + tracing::error!("Failed to send sprint-start metadata packet: {:?}", err); + } + } } PlayerCommandAction::StopSprinting => { let packet = EntityMetadataPacket::new(entity_id, [EntityMetadata::entity_clear_state()]); - broadcast_packet_except(eid, &packet, conn_query.iter()); + for (recipient, writer, tracker) in conn_query.iter() { + if recipient == eid || !writer.is_running() || !tracker.tracking.contains(&eid) + { + continue; + } + if let Err(err) = writer.send_packet_ref(&packet) { + tracing::error!("Failed to send sprint-stop metadata packet: {:?}", err); + } + } } _ => {} } diff --git a/src/game_systems/src/packets/src/player_input.rs b/src/game_systems/src/packets/src/player_input.rs index e85a6c19..4e2c116b 100644 --- a/src/game_systems/src/packets/src/player_input.rs +++ b/src/game_systems/src/packets/src/player_input.rs @@ -6,8 +6,8 @@ use bevy_ecs::prelude::{Entity, Query, Res}; use temper_codec::net_types::var_int::VarInt; use temper_components::entity_identity::Identity; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::sneak::SneakState; -use temper_net_runtime::broadcast::broadcast_packet_except; use temper_net_runtime::connection::StreamWriter; use temper_protocol::PlayerInputReceiver; use temper_protocol::outgoing::entity_metadata::{EntityMetadata, EntityMetadataPacket}; @@ -20,7 +20,7 @@ const FLAG_SNEAK: u8 = 0x20; /// PlayerInput contains movement flags including sneak (0x20). pub fn handle( receiver: Res, - conn_query: Query<(Entity, &StreamWriter)>, + conn_query: Query<(Entity, &StreamWriter, &EntityTracker)>, identity_query: Query<&Identity>, mut sneak_query: Query<&mut SneakState>, ) { @@ -73,6 +73,13 @@ pub fn handle( ) }; - broadcast_packet_except(eid, &packet, conn_query.iter()); + for (recipient, writer, tracker) in conn_query.iter() { + if recipient == eid || !writer.is_running() || !tracker.tracking.contains(&eid) { + continue; + } + if let Err(err) = writer.send_packet_ref(&packet) { + warn!("Failed to send player input metadata packet: {:?}", err); + } + } } } diff --git a/src/game_systems/src/packets/src/set_player_position.rs b/src/game_systems/src/packets/src/set_player_position.rs index a8ecf56e..f1328592 100644 --- a/src/game_systems/src/packets/src/set_player_position.rs +++ b/src/game_systems/src/packets/src/set_player_position.rs @@ -1,21 +1,21 @@ -use bevy_ecs::prelude::{MessageWriter, Query, Res}; +use bevy_ecs::prelude::{Entity, MessageWriter, Query, Res}; use temper_components::player::grounded::OnGround; use temper_components::player::position::Position; use temper_components::player::teleport_tracker::TeleportTracker; -use temper_messages::chunk_calc::ChunkCalc; +use temper_messages::cross_chunk_boundary_event::ChunkBoundaryCrossed; use temper_messages::packet_messages::Movement; use temper_protocol::SetPlayerPositionPacketReceiver; use tracing::trace; pub fn handle( receiver: Res, - mut query: Query<(&mut Position, &mut OnGround, &TeleportTracker)>, + mut query: Query<(Entity, &mut Position, &mut OnGround, &TeleportTracker)>, mut movement_messages: MessageWriter, - mut chunk_calc_messages: MessageWriter, + mut cross_chunk_border_msg: MessageWriter, ) { for (event, eid) in receiver.0.try_iter() { - if let Ok((mut pos, mut ground, tracker)) = query.get_mut(eid) { + if let Ok((entity, mut old_pos, mut ground, tracker)) = query.get_mut(eid) { if tracker.waiting_for_confirm { // Ignore position updates while waiting for teleport confirmation continue; @@ -23,20 +23,24 @@ pub fn handle( let new_pos = Position::new(event.x, event.feet_y, event.z); // Check if chunk changed - let old_chunk = (pos.x as i32 >> 4, pos.z as i32 >> 4); - let new_chunk = (new_pos.x as i32 >> 4, new_pos.z as i32 >> 4); + let old_chunk = old_pos.chunk(); + let new_chunk = new_pos.chunk(); if old_chunk != new_chunk { - chunk_calc_messages.write(ChunkCalc(eid)); + cross_chunk_border_msg.write(ChunkBoundaryCrossed { + entity, + old_chunk, + new_chunk, + }); } // Build movement message with delta BEFORE updating component let movement = Movement::new(eid) - .position_delta_from(&pos, &new_pos) + .position_delta_from(&old_pos, &new_pos) .on_ground(event.on_ground); // Update components - if pos.coords != new_pos.coords { - *pos = new_pos; + if old_pos.coords != new_pos.coords { + *old_pos = new_pos; } *ground = OnGround(event.on_ground); diff --git a/src/game_systems/src/packets/src/swing_arm.rs b/src/game_systems/src/packets/src/swing_arm.rs index b701b597..30f60f68 100644 --- a/src/game_systems/src/packets/src/swing_arm.rs +++ b/src/game_systems/src/packets/src/swing_arm.rs @@ -1,6 +1,7 @@ use bevy_ecs::prelude::{Entity, Query, Res}; use temper_codec::net_types::var_int::VarInt; use temper_components::entity_identity::Identity; +use temper_components::player::entity_tracker::EntityTracker; use temper_net_runtime::connection::StreamWriter; use temper_protocol::SwingArmPacketReceiver; use temper_protocol::outgoing::entity_animation::EntityAnimationPacket; @@ -10,7 +11,7 @@ use tracing::error; pub fn handle( receiver: Res, query: Query<&Identity>, - conn_query: Query<(Entity, &StreamWriter)>, + conn_query: Query<(Entity, &StreamWriter, &EntityTracker)>, state: Res, ) { for (event, eid) in receiver.0.try_iter() { @@ -20,13 +21,16 @@ pub fn handle( continue; }; let packet = EntityAnimationPacket::new(VarInt::new(game_id.entity_id), animation); - for (entity, conn) in conn_query.iter() { + for (entity, conn, tracker) in conn_query.iter() { if entity == eid { continue; // Skip sending to the player who triggered the event } if !state.0.players.is_connected(entity) { continue; // Skip if the player is not connected } + if !tracker.tracking.contains(&eid) { + continue; + } if let Err(e) = conn.send_packet_ref(&packet) { error!("Failed to send packet: {}", e); } diff --git a/src/game_systems/src/physics/src/chunk_boundary.rs b/src/game_systems/src/physics/src/chunk_boundary.rs new file mode 100644 index 00000000..99a2bc56 --- /dev/null +++ b/src/game_systems/src/physics/src/chunk_boundary.rs @@ -0,0 +1,29 @@ +use bevy_ecs::change_detection::DetectChanges; +use bevy_ecs::prelude::{Entity, MessageWriter, Query, Ref, Without}; +use temper_components::last_chunk_pos::LastChunkPos; +use temper_components::player::player_marker::PlayerMarker; +use temper_components::player::position::Position; +use temper_messages::cross_chunk_boundary_event::ChunkBoundaryCrossed; + +pub fn handle( + mut query: Query<(Entity, Ref, &mut LastChunkPos), Without>, + mut writer: MessageWriter, +) { + for (entity, pos, mut last_chunk) in query.iter_mut() { + if !pos.is_changed() { + continue; + } + + let new_chunk = pos.chunk(); + if last_chunk.0 == new_chunk { + continue; + } + + writer.write(ChunkBoundaryCrossed { + entity, + old_chunk: last_chunk.0, + new_chunk, + }); + last_chunk.0 = new_chunk; + } +} diff --git a/src/game_systems/src/physics/src/gravity.rs b/src/game_systems/src/physics/src/gravity.rs index a8485e61..09706cd1 100644 --- a/src/game_systems/src/physics/src/gravity.rs +++ b/src/game_systems/src/physics/src/gravity.rs @@ -23,7 +23,7 @@ type EntityQuery<'w, 's> = Query< >; // Just apply gravity to a mob's velocity. Application of velocity is handled elsewhere. -pub(crate) fn handle(mut entities: EntityQuery, state: Res) { +pub fn handle(mut entities: EntityQuery, state: Res) { for (mut vel, grounded, pos, is_water) in entities.iter_mut() { if grounded.0 { continue; @@ -51,188 +51,3 @@ pub(crate) fn handle(mut entities: EntityQuery, state: Res) } } } - -#[cfg(test)] -mod tests { - use super::*; - use bevy_ecs::prelude::*; - use bevy_math::DVec3; - use bevy_math::Vec3A; - use temper_components::player::grounded::OnGround; - use temper_components::player::velocity::Velocity; - use temper_entities::markers::HasGravity; - use temper_macros::block; - use temper_state::create_test_state; - - /// Creates a chunk with water blocks at the specified positions - /// This helper function is used to set up test scenarios where entities are in water - fn create_chunk_with_water(state: &GlobalStateResource, chunk_pos: ChunkPos) { - // Load or generate the chunk - let mut chunk = state - .0 - .world - .get_or_generate_mut(chunk_pos, Dimension::Overworld) - .expect("Failed to load or generate chunk"); - - chunk.fill(block!("water", { level: 0 })); - } - - #[test] - fn test_gravity_application() { - let mut world = World::new(); - let (state, _temp_dir) = create_test_state(); - world.insert_resource(state); - - let entity = world - .spawn(( - Velocity { vec: Vec3A::ZERO }, - OnGround(false), - Position { - coords: DVec3::new(0.0, 100.0, 0.0), - }, - HasGravity, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the gravity system - schedule.run(&mut world); - - let vel = world.get::(entity).unwrap(); - assert!( - vel.vec.y < 0.0, - "Velocity Y should be negative after gravity application" - ); - } - - #[test] - fn test_no_gravity_when_grounded() { - let mut world = World::new(); - let (state, _temp_dir) = create_test_state(); - world.insert_resource(state); - - let entity = world - .spawn(( - Velocity { vec: Vec3A::ZERO }, - OnGround(true), - Position { - coords: DVec3::new(0.0, 100.0, 0.0), - }, - HasGravity, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the gravity system - schedule.run(&mut world); - - let vel = world.get::(entity).unwrap(); - assert_eq!( - vel.vec.y, 0.0, - "Velocity Y should remain zero when grounded" - ); - } - - #[test] - fn test_water_entity_gravity_not_in_water() { - let mut world = World::new(); - let (state, _temp_dir) = create_test_state(); - world.insert_resource(state); - - let entity = world - .spawn(( - Velocity { vec: Vec3A::ZERO }, - OnGround(false), - Position { - coords: DVec3::new(0.0, 100.0, 0.0), - }, - HasGravity, - HasWaterDrag, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the gravity system - schedule.run(&mut world); - - let vel = world.get::(entity).unwrap(); - assert!( - vel.vec.y < 0.0, - "Water entity should have gravity applied when not in water" - ); - } - - #[test] - fn test_water_entity_no_gravity_when_grounded() { - let mut world = World::new(); - let (state, _temp_dir) = create_test_state(); - world.insert_resource(state); - - let entity = world - .spawn(( - Velocity { vec: Vec3A::ZERO }, - OnGround(true), - Position { - coords: DVec3::new(0.0, 100.0, 0.0), - }, - HasGravity, - HasWaterDrag, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the gravity system - schedule.run(&mut world); - - let vel = world.get::(entity).unwrap(); - assert_eq!( - vel.vec.y, 0.0, - "Water entity should not have gravity when grounded" - ); - } - - #[test] - fn test_water_entity_in_water_no_gravity() { - let mut world = World::new(); - let (state, _temp_dir) = create_test_state(); - - // Create a chunk with water blocks - let chunk_pos = ChunkPos::new(0, 0); - create_chunk_with_water(&state, chunk_pos); - - world.insert_resource(state); - - // Spawn entity at Y=65 (in water) - let entity = world - .spawn(( - Velocity { vec: Vec3A::ZERO }, - OnGround(false), - Position { - coords: DVec3::new(0.0, 65.0, 0.0), - }, - HasGravity, - HasWaterDrag, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the gravity system - schedule.run(&mut world); - - let vel = world.get::(entity).unwrap(); - assert_eq!( - vel.vec.y, 0.0, - "Water entity should not have gravity applied when in water (drag system handles it)" - ); - } -} diff --git a/src/game_systems/src/physics/src/lib.rs b/src/game_systems/src/physics/src/lib.rs index 97d538b8..c19a9f18 100644 --- a/src/game_systems/src/physics/src/lib.rs +++ b/src/game_systems/src/physics/src/lib.rs @@ -1,19 +1,6 @@ -use bevy_ecs::schedule::IntoScheduleConfigs; -mod collisions; -mod drag; -mod gravity; -mod unground; -mod velocity; - -pub fn register_physics_systems(schedule: &mut bevy_ecs::schedule::Schedule) { - schedule.add_systems( - ( - unground::handle, - gravity::handle, - drag::handle, - velocity::handle, - collisions::handle, - ) - .chain(), - ); -} +pub mod chunk_boundary; +pub mod collisions; +pub mod drag; +pub mod gravity; +pub mod unground; +pub mod velocity; diff --git a/src/game_systems/src/physics/src/velocity.rs b/src/game_systems/src/physics/src/velocity.rs index 112a3f36..912eff3e 100644 --- a/src/game_systems/src/physics/src/velocity.rs +++ b/src/game_systems/src/physics/src/velocity.rs @@ -11,161 +11,3 @@ pub fn handle(mut query: Query<(&Velocity, &mut Position)>) { pos.coords += vel.as_dvec3(); } } - -#[cfg(test)] -mod tests { - use super::*; - use bevy_ecs::message::MessageRegistry; - use bevy_ecs::prelude::*; - use bevy_math::Vec3A; - use temper_components::player::position::Position; - use temper_components::player::velocity::Velocity; - use temper_messages::entity_update::SendEntityUpdate; - - #[test] - fn test_velocity_updates_position() { - let mut world = World::new(); - let entity = world - .spawn(( - Velocity { - vec: Vec3A::new(1.0, 2.0, 3.0), - }, - Position { - coords: Vec3A::ZERO.as_dvec3(), - }, - )) - .id(); - MessageRegistry::register_message::(&mut world); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the velocity system - schedule.run(&mut world); - - let pos = world.get::(entity).unwrap(); - assert_eq!( - pos.coords, - Vec3A::new(1.0, 2.0, 3.0).as_dvec3(), - "Position should be updated based on velocity" - ); - } - - #[test] - fn test_no_update_when_unchanged() { - let mut world = World::new(); - let entity = world - .spawn(( - Velocity { vec: Vec3A::ZERO }, - Position { - coords: Vec3A::ZERO.as_dvec3(), - }, - )) - .id(); - - MessageRegistry::register_message::(&mut world); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the velocity system - schedule.run(&mut world); - - assert!( - world.get::(entity).is_some(), - "Entity should exist" - ); - - assert_eq!( - world.get::(entity).unwrap().coords, - Vec3A::ZERO.as_dvec3(), - "Position should remain unchanged" - ); - - let reader = world.get_resource::>().unwrap(); - let mut cursor = reader.get_cursor(); - let mut messages = vec![]; - for msg in cursor.read(reader) { - messages.push(msg); - } - assert_eq!( - messages.len(), - 0, - "No SendEntityUpdate message should be sent when unchanged" - ); - } - - #[test] - fn test_multiple_velocity_steps() { - let mut world = World::new(); - let entity = world - .spawn(( - Velocity { - vec: Vec3A::new(0.5, 0.0, 0.0), - }, - Position { - coords: Vec3A::ZERO.as_dvec3(), - }, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the velocity system multiple times - for _ in 0..4 { - schedule.run(&mut world); - } - - let pos = world.get::(entity).unwrap(); - assert_eq!( - pos.coords, - Vec3A::new(2.0, 0.0, 0.0).as_dvec3(), - "Position should be updated correctly after multiple steps" - ); - } - - #[test] - fn test_multiple_entities() { - let mut world = World::new(); - let entity1 = world - .spawn(( - Velocity { - vec: Vec3A::new(1.0, 0.0, 0.0), - }, - Position { - coords: Vec3A::ZERO.as_dvec3(), - }, - )) - .id(); - let entity2 = world - .spawn(( - Velocity { - vec: Vec3A::new(0.0, 1.0, 0.0), - }, - Position { - coords: Vec3A::ZERO.as_dvec3(), - }, - )) - .id(); - - let mut schedule = Schedule::default(); - schedule.add_systems(handle); - - // Run the velocity system - schedule.run(&mut world); - - let pos1 = world.get::(entity1).unwrap(); - let pos2 = world.get::(entity2).unwrap(); - assert_eq!( - pos1.coords, - Vec3A::new(1.0, 0.0, 0.0).as_dvec3(), - "Entity 1 position should be updated correctly" - ); - assert_eq!( - pos2.coords, - Vec3A::new(0.0, 1.0, 0.0).as_dvec3(), - "Entity 2 position should be updated correctly" - ); - } -} diff --git a/src/game_systems/src/player/Cargo.toml b/src/game_systems/src/player/Cargo.toml index 9e72a443..98217d89 100644 --- a/src/game_systems/src/player/Cargo.toml +++ b/src/game_systems/src/player/Cargo.toml @@ -25,3 +25,4 @@ temper-macros = { workspace = true } interactions = { workspace = true } tokio = { workspace = true } rand = { workspace = true } +bitcode = { workspace = true } diff --git a/src/game_systems/src/player/src/entity_spawn.rs b/src/game_systems/src/player/src/entity_spawn.rs index 7bbe712b..33603a05 100644 --- a/src/game_systems/src/player/src/entity_spawn.rs +++ b/src/game_systems/src/player/src/entity_spawn.rs @@ -1,120 +1,107 @@ use bevy_ecs::prelude::*; -use temper_components::entity_identity::Identity; +use temper_components::last_chunk_pos::LastChunkPos; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::position::Position; use temper_components::player::rotation::Rotation; use temper_entities::bundles::*; -use temper_entities::components::EntityMetadata; +use temper_entities::entity_types::EntityTypeEnum; use temper_entities::markers::entity_types::*; use temper_entities::markers::{HasCollisions, HasGravity, HasWaterDrag}; -use temper_messages::{EntityType, SpawnEntityCommand, SpawnEntityEvent}; -use temper_net_runtime::connection::StreamWriter; -use temper_protocol::outgoing::spawn_entity::SpawnEntityPacket; -use tracing::{error, warn}; +use temper_messages::{SpawnEntityCommand, SpawnEntityEvent}; +use temper_state::GlobalStateResource; +use tracing::warn; /// Macro for spawning ground entities (gravity + collisions + water drag) macro_rules! spawn_ground_entity { - ($commands:expr, $position:expr, $Bundle:ident, $Marker:ident) => {{ - let entity = $commands - .spawn(( - $Bundle::new($position), - $Marker, - HasGravity, - HasCollisions, - HasWaterDrag, - )) - .id(); - $commands.queue(move |world: &mut World| { - broadcast_entity_spawn(world, entity); + ($commands:expr, $position:expr, $Bundle:ident, $Marker:ident, $State:ident, $EType:path, $Query:ident) => {{ + let bundle = $Bundle::new($position); + let uuid = bundle.identity.uuid; + let last_chunk = LastChunkPos::new(bundle.position.chunk()); + let chunk = $State + .world + .get_or_generate_chunk( + $position.chunk(), + temper_core::dimension::Dimension::Overworld, + ) + .expect("Failed to get or generate chunk"); + chunk.entities.insert( + uuid, + ( + $EType, + bitcode::serialize(&bundle).expect("Failed to serialize entity bundle"), + ), + ); + chunk.mark_dirty(); + $commands.spawn(( + bundle, + $Marker, + HasGravity, + HasCollisions, + HasWaterDrag, + last_chunk, + )); + $Query.iter().for_each(|tracker| { + tracker.to_track.push((uuid, $EType.to_entity_type().id)); }); }}; } /// Macro for spawning flying/swimming entities (collisions only) macro_rules! spawn_flying_entity { - ($commands:expr, $position:expr, $Bundle:ident, $Marker:ident) => {{ - let entity = $commands - .spawn(($Bundle::new($position), $Marker, HasCollisions)) - .id(); - $commands.queue(move |world: &mut World| { - broadcast_entity_spawn(world, entity); + ($commands:expr, $position:expr, $Bundle:ident, $Marker:ident, $State:ident, $EType:path, $Query:ident) => {{ + let bundle = $Bundle::new($position); + let uuid = bundle.identity.uuid; + let last_chunk = LastChunkPos::new(bundle.position.chunk()); + let chunk = $State + .world + .get_or_generate_chunk( + $position.chunk(), + temper_core::dimension::Dimension::Overworld, + ) + .expect("Failed to get or generate chunk"); + chunk.entities.insert( + uuid, + ( + $EType, + bitcode::serialize(&bundle).expect("Failed to serialize entity bundle"), + ), + ); + chunk.mark_dirty(); + $commands.spawn((bundle, $Marker, HasCollisions, last_chunk)); + $Query.iter().for_each(|tracker| { + tracker.to_track.push((uuid, $EType.to_entity_type().id)); }); }}; } /// Macro for spawning entities with gravity but no water drag (lava/amphibian creatures) macro_rules! spawn_gravity_entity { - ($commands:expr, $position:expr, $Bundle:ident, $Marker:ident) => {{ - let entity = $commands - .spawn(($Bundle::new($position), $Marker, HasGravity, HasCollisions)) - .id(); - $commands.queue(move |world: &mut World| { - broadcast_entity_spawn(world, entity); + ($commands:expr, $position:expr, $Bundle:ident, $Marker:ident, $State:ident, $EType:path, $Query:ident) => {{ + let bundle = $Bundle::new($position); + let uuid = bundle.identity.uuid; + let last_chunk = LastChunkPos::new(bundle.position.chunk()); + let chunk = $State + .world + .get_or_generate_chunk( + $position.chunk(), + temper_core::dimension::Dimension::Overworld, + ) + .expect("Failed to get or generate chunk"); + chunk.entities.insert( + uuid, + ( + $EType, + bitcode::serialize(&bundle).expect("Failed to serialize entity bundle"), + ), + ); + chunk.mark_dirty(); + $commands.spawn((bundle, $Marker, HasGravity, HasCollisions, last_chunk)); + $Query.iter().for_each(|tracker| { + tracker.to_track.push((uuid, $EType.to_entity_type().id)); }); }}; } -/// Helper function to broadcast entity spawn packets to all connected players. -/// -/// This function queries the entity's components and sends the spawn packet -/// to all players. It's generic and works for any entity type. -/// -/// # Arguments -/// -/// * `world` - The Bevy world -/// * `entity` - The entity to broadcast -fn broadcast_entity_spawn(world: &mut World, entity: Entity) { - // Get entity components - let metadata = match world.get::(entity) { - Some(m) => m, - None => { - error!("Failed to get entity metadata for {:?}", entity); - return; - } - }; - let protocol_id = metadata.protocol_id(); - - let identity = match world.get::(entity) { - Some(i) => i, - None => { - error!("Failed to get entity identity for {:?}", entity); - return; - } - }; - - let position = match world.get::(entity) { - Some(p) => p, - None => { - error!("Failed to get entity position for {:?}", entity); - return; - } - }; - - let rotation = match world.get::(entity) { - Some(r) => r, - None => { - error!("Failed to get entity rotation for {:?}", entity); - return; - } - }; - - // Create spawn packet - let spawn_packet = SpawnEntityPacket::new( - identity.entity_id, - identity.uuid.as_u128(), - protocol_id as i32, - position, - rotation, - ); - - // Broadcast to all connected players - let mut writer_query = world.query::<&StreamWriter>(); - for writer in writer_query.iter(world) { - if let Err(e) = writer.send_packet_ref(&spawn_packet) { - error!("Failed to send spawn packet: {:?}", e); - } - } -} - /// System that processes spawn commands from messages pub fn spawn_command_processor( mut spawn_commands: MessageReader, @@ -143,146 +130,834 @@ pub fn spawn_command_processor( /// System that listens for `SpawnEntityEvent` and spawns the entity, /// then broadcasts the spawn packet. -pub fn handle_spawn_entity(mut events: MessageReader, mut commands: Commands) { +pub fn handle_spawn_entity( + mut events: MessageReader, + mut commands: Commands, + state: Res, + query: Query<&EntityTracker>, +) { for event in events.read() { let pos = event.position; + let state = state.0.clone(); match event.entity_type { // Ground entities (gravity + collisions + water drag) - EntityType::Pig => spawn_ground_entity!(commands, pos, PigBundle, Pig), - EntityType::Cow => spawn_ground_entity!(commands, pos, CowBundle, Cow), - EntityType::Armadillo => { - spawn_ground_entity!(commands, pos, ArmadilloBundle, Armadillo) - } - EntityType::Camel => spawn_ground_entity!(commands, pos, CamelBundle, Camel), - EntityType::Cat => spawn_ground_entity!(commands, pos, CatBundle, Cat), - EntityType::CaveSpider => { - spawn_ground_entity!(commands, pos, CaveSpiderBundle, CaveSpider) - } - EntityType::Chicken => spawn_ground_entity!(commands, pos, ChickenBundle, Chicken), - EntityType::Donkey => spawn_ground_entity!(commands, pos, DonkeyBundle, Donkey), - EntityType::Enderman => spawn_ground_entity!(commands, pos, EndermanBundle, Enderman), - EntityType::Fox => spawn_ground_entity!(commands, pos, FoxBundle, Fox), - EntityType::Frog => spawn_ground_entity!(commands, pos, FrogBundle, Frog), - EntityType::Goat => spawn_ground_entity!(commands, pos, GoatBundle, Goat), - EntityType::Horse => spawn_ground_entity!(commands, pos, HorseBundle, Horse), - EntityType::IronGolem => { - spawn_ground_entity!(commands, pos, IronGolemBundle, IronGolem) - } - EntityType::Llama => spawn_ground_entity!(commands, pos, LlamaBundle, Llama), - EntityType::Mooshroom => { - spawn_ground_entity!(commands, pos, MooshroomBundle, Mooshroom) - } - EntityType::Mule => spawn_ground_entity!(commands, pos, MuleBundle, Mule), - EntityType::Ocelot => spawn_ground_entity!(commands, pos, OcelotBundle, Ocelot), - EntityType::Panda => spawn_ground_entity!(commands, pos, PandaBundle, Panda), - EntityType::Piglin => spawn_ground_entity!(commands, pos, PiglinBundle, Piglin), - EntityType::PolarBear => { - spawn_ground_entity!(commands, pos, PolarBearBundle, PolarBear) - } - EntityType::Rabbit => spawn_ground_entity!(commands, pos, RabbitBundle, Rabbit), - EntityType::Sheep => spawn_ground_entity!(commands, pos, SheepBundle, Sheep), - EntityType::SkeletonHorse => { - spawn_ground_entity!(commands, pos, SkeletonHorseBundle, SkeletonHorse) - } - EntityType::Sniffer => spawn_ground_entity!(commands, pos, SnifferBundle, Sniffer), - EntityType::Spider => spawn_ground_entity!(commands, pos, SpiderBundle, Spider), - EntityType::SnowGolem => { - spawn_ground_entity!(commands, pos, SnowGolemBundle, SnowGolem) - } - EntityType::TraderLlama => { - spawn_ground_entity!(commands, pos, TraderLlamaBundle, TraderLlama) - } - EntityType::Turtle => spawn_ground_entity!(commands, pos, TurtleBundle, Turtle), - EntityType::Villager => spawn_ground_entity!(commands, pos, VillagerBundle, Villager), - EntityType::WanderingTrader => { - spawn_ground_entity!(commands, pos, WanderingTraderBundle, WanderingTrader) - } - EntityType::Wolf => spawn_ground_entity!(commands, pos, WolfBundle, Wolf), - EntityType::ZombieHorse => { - spawn_ground_entity!(commands, pos, ZombieHorseBundle, ZombieHorse) - } - EntityType::ZombifiedPiglin => { - spawn_ground_entity!(commands, pos, ZombifiedPiglinBundle, ZombifiedPiglin) + EntityTypeEnum::Pig => { + spawn_ground_entity!( + commands, + pos, + PigBundle, + Pig, + state, + EntityTypeEnum::Pig, + query + ) + } + EntityTypeEnum::Cow => { + spawn_ground_entity!( + commands, + pos, + CowBundle, + Cow, + state, + EntityTypeEnum::Cow, + query + ) + } + EntityTypeEnum::Armadillo => { + spawn_ground_entity!( + commands, + pos, + ArmadilloBundle, + Armadillo, + state, + EntityTypeEnum::Armadillo, + query + ) + } + EntityTypeEnum::Camel => { + spawn_ground_entity!( + commands, + pos, + CamelBundle, + Camel, + state, + EntityTypeEnum::Camel, + query + ) + } + EntityTypeEnum::Cat => { + spawn_ground_entity!( + commands, + pos, + CatBundle, + Cat, + state, + EntityTypeEnum::Cat, + query + ) + } + EntityTypeEnum::CaveSpider => { + spawn_ground_entity!( + commands, + pos, + CaveSpiderBundle, + CaveSpider, + state, + EntityTypeEnum::CaveSpider, + query + ) + } + EntityTypeEnum::Chicken => spawn_ground_entity!( + commands, + pos, + ChickenBundle, + Chicken, + state, + EntityTypeEnum::Chicken, + query + ), + EntityTypeEnum::Donkey => spawn_ground_entity!( + commands, + pos, + DonkeyBundle, + Donkey, + state, + EntityTypeEnum::Donkey, + query + ), + EntityTypeEnum::Enderman => spawn_ground_entity!( + commands, + pos, + EndermanBundle, + Enderman, + state, + EntityTypeEnum::Enderman, + query + ), + EntityTypeEnum::Fox => { + spawn_ground_entity!( + commands, + pos, + FoxBundle, + Fox, + state, + EntityTypeEnum::Fox, + query + ) + } + EntityTypeEnum::Frog => { + spawn_ground_entity!( + commands, + pos, + FrogBundle, + Frog, + state, + EntityTypeEnum::Frog, + query + ) + } + EntityTypeEnum::Goat => { + spawn_ground_entity!( + commands, + pos, + GoatBundle, + Goat, + state, + EntityTypeEnum::Goat, + query + ) + } + EntityTypeEnum::Horse => { + spawn_ground_entity!( + commands, + pos, + HorseBundle, + Horse, + state, + EntityTypeEnum::Horse, + query + ) + } + EntityTypeEnum::IronGolem => { + spawn_ground_entity!( + commands, + pos, + IronGolemBundle, + IronGolem, + state, + EntityTypeEnum::IronGolem, + query + ) + } + EntityTypeEnum::Llama => { + spawn_ground_entity!( + commands, + pos, + LlamaBundle, + Llama, + state, + EntityTypeEnum::Llama, + query + ) + } + EntityTypeEnum::Mooshroom => { + spawn_ground_entity!( + commands, + pos, + MooshroomBundle, + Mooshroom, + state, + EntityTypeEnum::Mooshroom, + query + ) + } + EntityTypeEnum::Mule => { + spawn_ground_entity!( + commands, + pos, + MuleBundle, + Mule, + state, + EntityTypeEnum::Mule, + query + ) + } + EntityTypeEnum::Ocelot => spawn_ground_entity!( + commands, + pos, + OcelotBundle, + Ocelot, + state, + EntityTypeEnum::Ocelot, + query + ), + EntityTypeEnum::Panda => { + spawn_ground_entity!( + commands, + pos, + PandaBundle, + Panda, + state, + EntityTypeEnum::Panda, + query + ) + } + EntityTypeEnum::Piglin => spawn_ground_entity!( + commands, + pos, + PiglinBundle, + Piglin, + state, + EntityTypeEnum::Piglin, + query + ), + EntityTypeEnum::PolarBear => { + spawn_ground_entity!( + commands, + pos, + PolarBearBundle, + PolarBear, + state, + EntityTypeEnum::PolarBear, + query + ) + } + EntityTypeEnum::Rabbit => spawn_ground_entity!( + commands, + pos, + RabbitBundle, + Rabbit, + state, + EntityTypeEnum::Rabbit, + query + ), + EntityTypeEnum::Sheep => { + spawn_ground_entity!( + commands, + pos, + SheepBundle, + Sheep, + state, + EntityTypeEnum::Sheep, + query + ) + } + EntityTypeEnum::SkeletonHorse => { + spawn_ground_entity!( + commands, + pos, + SkeletonHorseBundle, + SkeletonHorse, + state, + EntityTypeEnum::SkeletonHorse, + query + ) + } + EntityTypeEnum::Sniffer => spawn_ground_entity!( + commands, + pos, + SnifferBundle, + Sniffer, + state, + EntityTypeEnum::Sniffer, + query + ), + EntityTypeEnum::Spider => spawn_ground_entity!( + commands, + pos, + SpiderBundle, + Spider, + state, + EntityTypeEnum::Spider, + query + ), + EntityTypeEnum::SnowGolem => { + spawn_ground_entity!( + commands, + pos, + SnowGolemBundle, + SnowGolem, + state, + EntityTypeEnum::SnowGolem, + query + ) + } + EntityTypeEnum::TraderLlama => { + spawn_ground_entity!( + commands, + pos, + TraderLlamaBundle, + TraderLlama, + state, + EntityTypeEnum::TraderLlama, + query + ) + } + EntityTypeEnum::Turtle => spawn_ground_entity!( + commands, + pos, + TurtleBundle, + Turtle, + state, + EntityTypeEnum::Turtle, + query + ), + EntityTypeEnum::Villager => spawn_ground_entity!( + commands, + pos, + VillagerBundle, + Villager, + state, + EntityTypeEnum::Villager, + query + ), + EntityTypeEnum::WanderingTrader => { + spawn_ground_entity!( + commands, + pos, + WanderingTraderBundle, + WanderingTrader, + state, + EntityTypeEnum::WanderingTrader, + query + ) + } + EntityTypeEnum::Wolf => { + spawn_ground_entity!( + commands, + pos, + WolfBundle, + Wolf, + state, + EntityTypeEnum::Wolf, + query + ) + } + EntityTypeEnum::ZombieHorse => { + spawn_ground_entity!( + commands, + pos, + ZombieHorseBundle, + ZombieHorse, + state, + EntityTypeEnum::ZombieHorse, + query + ) + } + EntityTypeEnum::ZombifiedPiglin => { + spawn_ground_entity!( + commands, + pos, + ZombifiedPiglinBundle, + ZombifiedPiglin, + state, + EntityTypeEnum::ZombifiedPiglin, + query + ) } // Flying entities (collisions only) - EntityType::Allay => spawn_flying_entity!(commands, pos, AllayBundle, Allay), - EntityType::Bat => spawn_flying_entity!(commands, pos, BatBundle, Bat), - EntityType::Bee => spawn_flying_entity!(commands, pos, BeeBundle, Bee), - EntityType::Parrot => spawn_flying_entity!(commands, pos, ParrotBundle, Parrot), + EntityTypeEnum::Allay => { + spawn_flying_entity!( + commands, + pos, + AllayBundle, + Allay, + state, + EntityTypeEnum::Allay, + query + ) + } + EntityTypeEnum::Bat => { + spawn_flying_entity!( + commands, + pos, + BatBundle, + Bat, + state, + EntityTypeEnum::Bat, + query + ) + } + EntityTypeEnum::Bee => { + spawn_flying_entity!( + commands, + pos, + BeeBundle, + Bee, + state, + EntityTypeEnum::Bee, + query + ) + } + EntityTypeEnum::Parrot => spawn_flying_entity!( + commands, + pos, + ParrotBundle, + Parrot, + state, + EntityTypeEnum::Parrot, + query + ), // Water creatures (collisions only, no gravity/water drag) - EntityType::Cod => spawn_flying_entity!(commands, pos, CodBundle, Cod), - EntityType::Dolphin => spawn_flying_entity!(commands, pos, DolphinBundle, Dolphin), - EntityType::Drowned => spawn_flying_entity!(commands, pos, DrownedBundle, Drowned), - EntityType::GlowSquid => { - spawn_flying_entity!(commands, pos, GlowSquidBundle, GlowSquid) + EntityTypeEnum::Cod => { + spawn_flying_entity!( + commands, + pos, + CodBundle, + Cod, + state, + EntityTypeEnum::Cod, + query + ) + } + EntityTypeEnum::Dolphin => spawn_flying_entity!( + commands, + pos, + DolphinBundle, + Dolphin, + state, + EntityTypeEnum::Dolphin, + query + ), + EntityTypeEnum::Drowned => spawn_flying_entity!( + commands, + pos, + DrownedBundle, + Drowned, + state, + EntityTypeEnum::Drowned, + query + ), + EntityTypeEnum::GlowSquid => { + spawn_flying_entity!( + commands, + pos, + GlowSquidBundle, + GlowSquid, + state, + EntityTypeEnum::GlowSquid, + query + ) + } + EntityTypeEnum::Pufferfish => { + spawn_flying_entity!( + commands, + pos, + PufferfishBundle, + Pufferfish, + state, + EntityTypeEnum::Pufferfish, + query + ) } - EntityType::Pufferfish => { - spawn_flying_entity!(commands, pos, PufferfishBundle, Pufferfish) + EntityTypeEnum::Salmon => spawn_flying_entity!( + commands, + pos, + SalmonBundle, + Salmon, + state, + EntityTypeEnum::Salmon, + query + ), + EntityTypeEnum::Squid => { + spawn_flying_entity!( + commands, + pos, + SquidBundle, + Squid, + state, + EntityTypeEnum::Squid, + query + ) } - EntityType::Salmon => spawn_flying_entity!(commands, pos, SalmonBundle, Salmon), - EntityType::Squid => spawn_flying_entity!(commands, pos, SquidBundle, Squid), - EntityType::Tadpole => spawn_flying_entity!(commands, pos, TadpoleBundle, Tadpole), - EntityType::TropicalFish => { - spawn_flying_entity!(commands, pos, TropicalFishBundle, TropicalFish) + EntityTypeEnum::Tadpole => spawn_flying_entity!( + commands, + pos, + TadpoleBundle, + Tadpole, + state, + EntityTypeEnum::Tadpole, + query + ), + EntityTypeEnum::TropicalFish => { + spawn_flying_entity!( + commands, + pos, + TropicalFishBundle, + TropicalFish, + state, + EntityTypeEnum::TropicalFish, + query + ) } // Special: gravity but no water drag (amphibians, lava creatures) - EntityType::Axolotl => spawn_gravity_entity!(commands, pos, AxolotlBundle, Axolotl), - EntityType::Strider => spawn_gravity_entity!(commands, pos, StriderBundle, Strider), - EntityType::MagmaCube => { - spawn_gravity_entity!(commands, pos, MagmaCubeBundle, MagmaCube) + EntityTypeEnum::Axolotl => spawn_gravity_entity!( + commands, + pos, + AxolotlBundle, + Axolotl, + state, + EntityTypeEnum::Axolotl, + query + ), + EntityTypeEnum::Strider => spawn_gravity_entity!( + commands, + pos, + StriderBundle, + Strider, + state, + EntityTypeEnum::Strider, + query + ), + EntityTypeEnum::MagmaCube => { + spawn_gravity_entity!( + commands, + pos, + MagmaCubeBundle, + MagmaCube, + state, + EntityTypeEnum::MagmaCube, + query + ) + } + EntityTypeEnum::Slime => { + spawn_gravity_entity!( + commands, + pos, + SlimeBundle, + Slime, + state, + EntityTypeEnum::Slime, + query + ) } - EntityType::Slime => spawn_gravity_entity!(commands, pos, SlimeBundle, Slime), // Hostile ground entities - EntityType::Bogged => spawn_ground_entity!(commands, pos, BoggedBundle, Bogged), - EntityType::Creaking => spawn_ground_entity!(commands, pos, CreakingBundle, Creaking), - EntityType::Creeper => spawn_ground_entity!(commands, pos, CreeperBundle, Creeper), - EntityType::Endermite => { - spawn_ground_entity!(commands, pos, EndermiteBundle, Endermite) - } - EntityType::Evoker => spawn_ground_entity!(commands, pos, EvokerBundle, Evoker), - EntityType::Hoglin => spawn_ground_entity!(commands, pos, HoglinBundle, Hoglin), - EntityType::Husk => spawn_ground_entity!(commands, pos, HuskBundle, Husk), - EntityType::PiglinBrute => { - spawn_ground_entity!(commands, pos, PiglinBruteBundle, PiglinBrute) - } - EntityType::Pillager => spawn_ground_entity!(commands, pos, PillagerBundle, Pillager), - EntityType::Ravager => spawn_ground_entity!(commands, pos, RavagerBundle, Ravager), - EntityType::Silverfish => { - spawn_ground_entity!(commands, pos, SilverfishBundle, Silverfish) - } - EntityType::Skeleton => spawn_ground_entity!(commands, pos, SkeletonBundle, Skeleton), - EntityType::Stray => spawn_ground_entity!(commands, pos, StrayBundle, Stray), - EntityType::Vindicator => { - spawn_ground_entity!(commands, pos, VindicatorBundle, Vindicator) - } - EntityType::Warden => spawn_ground_entity!(commands, pos, WardenBundle, Warden), - EntityType::Witch => spawn_ground_entity!(commands, pos, WitchBundle, Witch), - EntityType::WitherSkeleton => { - spawn_ground_entity!(commands, pos, WitherSkeletonBundle, WitherSkeleton) - } - EntityType::Zoglin => spawn_ground_entity!(commands, pos, ZoglinBundle, Zoglin), - EntityType::Zombie => spawn_ground_entity!(commands, pos, ZombieBundle, Zombie), - EntityType::ZombieVillager => { - spawn_ground_entity!(commands, pos, ZombieVillagerBundle, ZombieVillager) - } - EntityType::Shulker => spawn_ground_entity!(commands, pos, ShulkerBundle, Shulker), + EntityTypeEnum::Bogged => spawn_ground_entity!( + commands, + pos, + BoggedBundle, + Bogged, + state, + EntityTypeEnum::Bogged, + query + ), + EntityTypeEnum::Creaking => spawn_ground_entity!( + commands, + pos, + CreakingBundle, + Creaking, + state, + EntityTypeEnum::Creaking, + query + ), + EntityTypeEnum::Creeper => spawn_ground_entity!( + commands, + pos, + CreeperBundle, + Creeper, + state, + EntityTypeEnum::Creeper, + query + ), + EntityTypeEnum::Endermite => { + spawn_ground_entity!( + commands, + pos, + EndermiteBundle, + Endermite, + state, + EntityTypeEnum::Endermite, + query + ) + } + EntityTypeEnum::Evoker => spawn_ground_entity!( + commands, + pos, + EvokerBundle, + Evoker, + state, + EntityTypeEnum::Evoker, + query + ), + EntityTypeEnum::Hoglin => spawn_ground_entity!( + commands, + pos, + HoglinBundle, + Hoglin, + state, + EntityTypeEnum::Hoglin, + query + ), + EntityTypeEnum::Husk => { + spawn_ground_entity!( + commands, + pos, + HuskBundle, + Husk, + state, + EntityTypeEnum::Husk, + query + ) + } + EntityTypeEnum::PiglinBrute => { + spawn_ground_entity!( + commands, + pos, + PiglinBruteBundle, + PiglinBrute, + state, + EntityTypeEnum::PiglinBrute, + query + ) + } + EntityTypeEnum::Pillager => spawn_ground_entity!( + commands, + pos, + PillagerBundle, + Pillager, + state, + EntityTypeEnum::Pillager, + query + ), + EntityTypeEnum::Ravager => spawn_ground_entity!( + commands, + pos, + RavagerBundle, + Ravager, + state, + EntityTypeEnum::Ravager, + query + ), + EntityTypeEnum::Silverfish => { + spawn_ground_entity!( + commands, + pos, + SilverfishBundle, + Silverfish, + state, + EntityTypeEnum::Silverfish, + query + ) + } + EntityTypeEnum::Skeleton => spawn_ground_entity!( + commands, + pos, + SkeletonBundle, + Skeleton, + state, + EntityTypeEnum::Skeleton, + query + ), + EntityTypeEnum::Stray => { + spawn_ground_entity!( + commands, + pos, + StrayBundle, + Stray, + state, + EntityTypeEnum::Stray, + query + ) + } + EntityTypeEnum::Vindicator => { + spawn_ground_entity!( + commands, + pos, + VindicatorBundle, + Vindicator, + state, + EntityTypeEnum::Vindicator, + query + ) + } + EntityTypeEnum::Warden => spawn_ground_entity!( + commands, + pos, + WardenBundle, + Warden, + state, + EntityTypeEnum::Warden, + query + ), + EntityTypeEnum::Witch => { + spawn_ground_entity!( + commands, + pos, + WitchBundle, + Witch, + state, + EntityTypeEnum::Witch, + query + ) + } + EntityTypeEnum::WitherSkeleton => { + spawn_ground_entity!( + commands, + pos, + WitherSkeletonBundle, + WitherSkeleton, + state, + EntityTypeEnum::WitherSkeleton, + query + ) + } + EntityTypeEnum::Zoglin => spawn_ground_entity!( + commands, + pos, + ZoglinBundle, + Zoglin, + state, + EntityTypeEnum::Zoglin, + query + ), + EntityTypeEnum::Zombie => spawn_ground_entity!( + commands, + pos, + ZombieBundle, + Zombie, + state, + EntityTypeEnum::Zombie, + query + ), + EntityTypeEnum::ZombieVillager => { + spawn_ground_entity!( + commands, + pos, + ZombieVillagerBundle, + ZombieVillager, + state, + EntityTypeEnum::ZombieVillager, + query + ) + } + EntityTypeEnum::Shulker => spawn_ground_entity!( + commands, + pos, + ShulkerBundle, + Shulker, + state, + EntityTypeEnum::Shulker, + query + ), // Hostile flying entities - EntityType::Blaze => spawn_flying_entity!(commands, pos, BlazeBundle, Blaze), - EntityType::Breeze => spawn_flying_entity!(commands, pos, BreezeBundle, Breeze), - EntityType::Ghast => spawn_flying_entity!(commands, pos, GhastBundle, Ghast), - EntityType::Phantom => spawn_flying_entity!(commands, pos, PhantomBundle, Phantom), - EntityType::Vex => spawn_flying_entity!(commands, pos, VexBundle, Vex), + EntityTypeEnum::Blaze => { + spawn_flying_entity!( + commands, + pos, + BlazeBundle, + Blaze, + state, + EntityTypeEnum::Blaze, + query + ) + } + EntityTypeEnum::Breeze => spawn_flying_entity!( + commands, + pos, + BreezeBundle, + Breeze, + state, + EntityTypeEnum::Breeze, + query + ), + EntityTypeEnum::Ghast => { + spawn_flying_entity!( + commands, + pos, + GhastBundle, + Ghast, + state, + EntityTypeEnum::Ghast, + query + ) + } + EntityTypeEnum::Phantom => spawn_flying_entity!( + commands, + pos, + PhantomBundle, + Phantom, + state, + EntityTypeEnum::Phantom, + query + ), + EntityTypeEnum::Vex => { + spawn_flying_entity!( + commands, + pos, + VexBundle, + Vex, + state, + EntityTypeEnum::Vex, + query + ) + } // Hostile water entities - EntityType::ElderGuardian => { - spawn_flying_entity!(commands, pos, ElderGuardianBundle, ElderGuardian) + EntityTypeEnum::ElderGuardian => { + spawn_flying_entity!( + commands, + pos, + ElderGuardianBundle, + ElderGuardian, + state, + EntityTypeEnum::ElderGuardian, + query + ) } - EntityType::Guardian => spawn_flying_entity!(commands, pos, GuardianBundle, Guardian), + EntityTypeEnum::Guardian => spawn_flying_entity!( + commands, + pos, + GuardianBundle, + Guardian, + state, + EntityTypeEnum::Guardian, + query + ), } } } diff --git a/src/game_systems/src/player/src/lib.rs b/src/game_systems/src/player/src/lib.rs index 1fc23b84..a236133c 100644 --- a/src/game_systems/src/player/src/lib.rs +++ b/src/game_systems/src/player/src/lib.rs @@ -1,51 +1,15 @@ -use bevy_ecs::prelude::ApplyDeferred; -use bevy_ecs::schedule::IntoScheduleConfigs; - -mod chunk_calculator; -mod digging_system; -mod emit_player_joined; -mod entity_spawn; -mod gamemode_change; -mod movement_broadcast; -mod new_connections; -mod player_despawn; -mod player_join_message; -mod player_leave_message; -mod player_spawn; -mod player_swimming; -mod player_tp; -mod send_inventory_updates; +pub mod chunk_calculator; +pub mod digging_system; +pub mod emit_player_joined; +pub mod entity_spawn; +pub mod gamemode_change; +pub mod movement_broadcast; +pub mod new_connections; +pub mod player_despawn; +pub mod player_join_message; +pub mod player_leave_message; +pub mod player_spawn; +pub mod player_swimming; +pub mod player_tp; +pub mod send_inventory_updates; pub mod update_player_ping; - -pub fn register_player_systems(schedule: &mut bevy_ecs::schedule::Schedule) { - schedule.add_systems(digging_system::handle_start_digging); - schedule.add_systems(digging_system::handle_finish_digging); - schedule.add_systems(digging_system::handle_start_digging); - schedule.add_systems(digging_system::handle_cancel_digging); - schedule.add_systems(entity_spawn::handle_spawn_entity); - schedule.add_systems(entity_spawn::spawn_command_processor); - schedule.add_systems(gamemode_change::handle); - schedule.add_systems(movement_broadcast::handle_player_move); - - // Player connection handling - chained to ensure proper event timing: - // 1. accept_new_connections: Spawns entity + adds PendingPlayerJoin marker (deferred) - // 2. ApplyDeferred: Flushes commands, entity now exists and is queryable - // 3. chunk_calculator::handle: Starts sending chunks to the player immediately after they join - // 4. emit_player_joined: Fires PlayerJoined event (listeners can now query the entity) - schedule.add_systems( - ( - new_connections::accept_new_connections, - ApplyDeferred, - chunk_calculator::handle, - emit_player_joined::emit_player_joined, - ) - .chain(), - ); - schedule.add_systems(player_despawn::handle); - schedule.add_systems(player_join_message::handle); - schedule.add_systems(player_leave_message::handle); - schedule.add_systems(player_spawn::handle); - schedule.add_systems(player_swimming::detect_player_swimming); - schedule.add_systems(player_tp::teleport_player); - schedule.add_systems(send_inventory_updates::handle_inventory_updates); -} diff --git a/src/game_systems/src/player/src/movement_broadcast.rs b/src/game_systems/src/player/src/movement_broadcast.rs index f6094ef8..a64524f2 100644 --- a/src/game_systems/src/player/src/movement_broadcast.rs +++ b/src/game_systems/src/player/src/movement_broadcast.rs @@ -15,6 +15,7 @@ use bevy_ecs::prelude::{Entity, MessageReader, Query}; use temper_codec::net_types::angle::NetAngle; use temper_components::entity_identity::Identity; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::position::Position; use temper_components::player::rotation::Rotation; use temper_macros::NetEncode; @@ -53,7 +54,7 @@ const MAX_DELTA: i16 = (7.5 * 4096f32) as i16; pub fn handle_player_move( mut movement_msgs: MessageReader, query: Query<(&Position, &Rotation, &Identity)>, - broadcast_query: Query<(Entity, &StreamWriter)>, + broadcast_query: Query<(Entity, &StreamWriter, &EntityTracker)>, ) { for movement in movement_msgs.read() { let sender_entity = movement.entity; @@ -126,7 +127,7 @@ pub fn handle_player_move( }; // Broadcast to all other connected players - for (recipient_entity, writer) in broadcast_query.iter() { + for (recipient_entity, writer, tracker) in broadcast_query.iter() { // Skip sending to the sender if recipient_entity == sender_entity { continue; @@ -137,6 +138,10 @@ pub fn handle_player_move( continue; } + if !tracker.tracking.contains(&sender_entity) { + continue; + } + // Send the movement packet (position and/or rotation) if let Some(ref packet) = movement_packet && let Err(err) = writer.send_packet_ref(packet) diff --git a/src/game_systems/src/player/src/new_connections.rs b/src/game_systems/src/player/src/new_connections.rs index 1559fa16..84c396d2 100644 --- a/src/game_systems/src/player/src/new_connections.rs +++ b/src/game_systems/src/player/src/new_connections.rs @@ -2,6 +2,7 @@ use bevy_ecs::prelude::{Commands, MessageWriter, Res}; use std::time::Instant; use temper_components::bounds::CollisionBounds; use temper_components::player::chunk_receiver::ChunkReceiver; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::grounded::OnGround; use temper_components::player::keepalive::KeepAliveTracker; use temper_components::player::player_marker::PlayerMarker; @@ -79,6 +80,7 @@ pub fn accept_new_connections( z_offset_end: 0.3, }, player_marker: PlayerMarker, + entity_tracker: EntityTracker::default(), }; // --- 3. Spawn the PlayerBundle, then .insert() the network components --- diff --git a/src/game_systems/src/player/src/player_despawn.rs b/src/game_systems/src/player/src/player_despawn.rs index c43979cd..138aa083 100644 --- a/src/game_systems/src/player/src/player_despawn.rs +++ b/src/game_systems/src/player/src/player_despawn.rs @@ -6,6 +6,7 @@ use bevy_ecs::prelude::{Entity, MessageReader, Query, Res}; use temper_components::entity_identity::Identity; +use temper_components::player::entity_tracker::EntityTracker; use temper_messages::player_leave::PlayerLeft; use temper_net_runtime::connection::StreamWriter; use temper_protocol::outgoing::player_info_remove::PlayerInfoRemovePacket; @@ -16,11 +17,11 @@ use tracing::{error, trace}; /// Listens for `PlayerLeft` events and broadcasts despawn packets to remaining players. pub fn handle( mut events: MessageReader, - player_query: Query<(Entity, &Identity, &StreamWriter)>, + mut player_query: Query<(Entity, &Identity, &StreamWriter, &mut EntityTracker)>, state: Res, ) { for event in events.read() { - let left_player = &event.0; + let left_player = &event.identity; // Create packets once let remove_info_packet = PlayerInfoRemovePacket::single(left_player.uuid.as_u128()); @@ -30,7 +31,7 @@ pub fn handle( let mut notified_count = 0; // Broadcast to all remaining players - for (entity, identity, conn) in player_query.iter() { + for (entity, identity, conn, mut tracker) in player_query.iter_mut() { // Skip the player who left (their entity may already be despawning) if identity.uuid == left_player.uuid { continue; @@ -41,8 +42,9 @@ pub fn handle( continue; } - // Remove entity from world - if let Err(e) = conn.send_packet_ref(&remove_entity_packet) { + if tracker.tracking.remove(&event.entity) + && let Err(e) = conn.send_packet_ref(&remove_entity_packet) + { error!("Failed to send remove entities packet: {:?}", e); continue; } diff --git a/src/game_systems/src/player/src/player_leave_message.rs b/src/game_systems/src/player/src/player_leave_message.rs index 54fad5a3..522349e6 100644 --- a/src/game_systems/src/player/src/player_leave_message.rs +++ b/src/game_systems/src/player/src/player_leave_message.rs @@ -12,7 +12,7 @@ use tracing::trace; pub fn handle(mut events: MessageReader, player_query: Query<(Entity, &Identity)>) { // 1. Loop through each "player left" event for event in events.read() { - let player_who_left = &event.0; + let player_who_left = &event.identity; // 2. Build the "Player left the game" message let mut message = TextComponent::from(format!( diff --git a/src/game_systems/src/player/src/player_spawn.rs b/src/game_systems/src/player/src/player_spawn.rs index 7fbf5b90..fcafcaa3 100644 --- a/src/game_systems/src/player/src/player_spawn.rs +++ b/src/game_systems/src/player/src/player_spawn.rs @@ -1,36 +1,24 @@ -//! Handles spawning players for each other when they join the server. +//! Handles global player list updates when players join the server. //! //! When a player joins: -//! 1. Send existing players' info + spawn packets to the new player -//! 2. Broadcast the new player's info + spawn packets to existing players +//! 1. Send existing players' tab-list info to the new player +//! 2. Broadcast the new player's tab-list info to existing players +//! +//! Actual in-world entity spawning is handled by `EntityTracker`. use bevy_ecs::prelude::{Entity, MessageReader, Query, Res}; use temper_components::entity_identity::Identity; use temper_components::player::player_properties::PlayerProperties; -use temper_components::player::position::Position; -use temper_components::player::rotation::Rotation; -use temper_macros::get_registry_entry; use temper_messages::player_join::PlayerJoined; use temper_net_runtime::connection::StreamWriter; use temper_protocol::outgoing::player_info_update::PlayerInfoUpdatePacket; -use temper_protocol::outgoing::spawn_entity::SpawnEntityPacket; use temper_state::GlobalStateResource; use tracing::{error, trace}; -const PLAYER_TYPE_ID: i32 = - get_registry_entry!("minecraft:entity_type.entries.minecraft:player") as i32; - -/// Listens for `PlayerJoined` events and handles spawning players for each other. +/// Listens for `PlayerJoined` events and syncs tab-list state for all players. pub fn handle( mut events: MessageReader, - player_query: Query<( - Entity, - &Identity, - &Position, - &Rotation, - &StreamWriter, - &PlayerProperties, - )>, + player_query: Query<(Entity, &Identity, &StreamWriter, &PlayerProperties)>, state: Res, ) { for event in events.read() { @@ -38,11 +26,9 @@ pub fn handle( let new_player_identity = &event.identity; // Get the new player's connection and components - let Ok((_, _, new_pos, new_rot, new_conn, player_properties)) = - player_query.get(new_player_entity) - else { + let Ok((_, _, new_conn, player_properties)) = player_query.get(new_player_entity) else { error!( - "Failed to get new player components for spawn broadcast: {:?}", + "Failed to get new player components for tab sync: {:?}", new_player_entity ); continue; @@ -51,18 +37,11 @@ pub fn handle( // Create packets for the new player once (to broadcast to existing players) let new_player_info_packet = PlayerInfoUpdatePacket::new_player_join_packet(new_player_identity, player_properties); - let new_player_spawn_packet = SpawnEntityPacket::new( - new_player_identity.entity_id, - new_player_identity.uuid.as_u128(), - PLAYER_TYPE_ID, - new_pos, - new_rot, - ); - let mut spawned_for_new_player = 0; - let mut spawned_for_existing = 0; + let mut listed_for_new_player = 0; + let mut listed_for_existing = 0; - for (entity, identity, pos, rot, conn, player_properties) in player_query.iter() { + for (entity, identity, conn, player_properties) in player_query.iter() { // Skip self if entity == new_player_entity { continue; @@ -81,46 +60,21 @@ pub fn handle( error!("Failed to send existing player info to new player: {:?}", e); continue; } + listed_for_new_player += 1; - // 2. Send existing player's spawn packet to the new player - let existing_player_spawn = SpawnEntityPacket::new( - identity.entity_id, - identity.uuid.as_u128(), - PLAYER_TYPE_ID, - pos, - rot, - ); - if let Err(e) = new_conn.send_packet_ref(&existing_player_spawn) { - error!( - "Failed to send existing player spawn to new player: {:?}", - e - ); - continue; - } - spawned_for_new_player += 1; - - // 3. Send new player's info to existing player + // 2. Send new player's info to existing player if let Err(e) = conn.send_packet_ref(&new_player_info_packet) { error!("Failed to send new player info to existing player: {:?}", e); continue; } - - // 4. Send new player's spawn packet to existing player - if let Err(e) = conn.send_packet_ref(&new_player_spawn_packet) { - error!( - "Failed to send new player spawn to existing player: {:?}", - e - ); - continue; - } - spawned_for_existing += 1; + listed_for_existing += 1; } trace!( - "Player {} joined: sent {} existing players, spawned for {} existing players", + "Player {} joined: synced tab info for {} existing players and broadcast to {} players", new_player_identity.name.as_ref().expect("No Player Name"), - spawned_for_new_player, - spawned_for_existing + listed_for_new_player, + listed_for_existing ); } } diff --git a/src/game_systems/src/player/src/player_swimming.rs b/src/game_systems/src/player/src/player_swimming.rs index 454f9dc9..6051c40f 100644 --- a/src/game_systems/src/player/src/player_swimming.rs +++ b/src/game_systems/src/player/src/player_swimming.rs @@ -3,6 +3,7 @@ use bevy_math::DVec3; use std::collections::HashSet; use temper_codec::net_types::var_int::VarInt; use temper_components::entity_identity::Identity; +use temper_components::player::entity_tracker::EntityTracker; use temper_components::player::position::Position; use temper_components::player::swimming::SwimmingState; use temper_core::block_state_id::BlockStateId; @@ -43,8 +44,8 @@ fn is_player_in_water(state: &temper_state::GlobalState, pos: &Position) -> bool /// System that detects when players enter/exit water and updates their swimming state /// Also broadcasts the swimming pose to all connected clients pub fn detect_player_swimming( - mut swimmers: Query<(&Identity, Ref, &mut SwimmingState)>, - all_connections: Query<(Entity, &StreamWriter)>, + mut swimmers: Query<(Entity, &Identity, Ref, &mut SwimmingState)>, + all_connections: Query<(Entity, &StreamWriter, &EntityTracker)>, state: Res, mut world_change: MessageReader, ) { @@ -54,7 +55,7 @@ pub fn detect_player_swimming( changed_chunks.insert(pos); } } - for (identity, pos, mut swimming_state) in swimmers.iter_mut() { + for (entity, identity, pos, mut swimming_state) in swimmers.iter_mut() { if !changed_chunks.contains(&pos.chunk()) && !pos.is_changed() { continue; } @@ -72,7 +73,7 @@ pub fn detect_player_swimming( ], ); - broadcast_metadata(&packet, &all_connections, &state); + broadcast_metadata(entity, &packet, &all_connections, &state); } else if !in_water && swimming_state.is_swimming { swimming_state.is_swimming = false; @@ -85,21 +86,25 @@ pub fn detect_player_swimming( ], ); - broadcast_metadata(&packet, &all_connections, &state); + broadcast_metadata(entity, &packet, &all_connections, &state); } } } /// Helper function to broadcast entity metadata to all connected players fn broadcast_metadata( + sender: Entity, packet: &EntityMetadataPacket, - connections: &Query<(Entity, &StreamWriter)>, + connections: &Query<(Entity, &StreamWriter, &EntityTracker)>, state: &GlobalStateResource, ) { - for (entity, conn) in connections { + for (entity, conn, tracker) in connections { if !state.0.players.is_connected(entity) { continue; } + if entity == sender || !tracker.tracking.contains(&sender) { + continue; + } if let Err(err) = conn.send_packet_ref(packet) { error!("Failed to send entity metadata packet: {:?}", err); } diff --git a/src/game_systems/src/shutdown/Cargo.toml b/src/game_systems/src/shutdown/Cargo.toml index ef2c6d74..ee8ea75f 100644 --- a/src/game_systems/src/shutdown/Cargo.toml +++ b/src/game_systems/src/shutdown/Cargo.toml @@ -10,4 +10,7 @@ temper-net-runtime = { workspace = true } temper-protocol = { workspace = true } temper-state = { workspace = true } temper-text = { workspace = true } -tracing = { workspace = true } \ No newline at end of file +tracing = { workspace = true } +temper-messages = { workspace = true } +background = { path = "../background" } +mobs = { path = "../mobs" } \ No newline at end of file diff --git a/src/game_systems/src/shutdown/src/lib.rs b/src/game_systems/src/shutdown/src/lib.rs index 4520f9d1..4170e707 100644 --- a/src/game_systems/src/shutdown/src/lib.rs +++ b/src/game_systems/src/shutdown/src/lib.rs @@ -1,5 +1,2 @@ -mod send_shutdown_packet; - -pub fn register_shutdown_systems(schedule: &mut bevy_ecs::prelude::Schedule) { - schedule.add_systems(send_shutdown_packet::handle); -} +pub mod send_save_message; +pub mod send_shutdown_packet; diff --git a/src/game_systems/src/shutdown/src/send_save_message.rs b/src/game_systems/src/shutdown/src/send_save_message.rs new file mode 100644 index 00000000..2321032e --- /dev/null +++ b/src/game_systems/src/shutdown/src/send_save_message.rs @@ -0,0 +1,12 @@ +use bevy_ecs::prelude::{MessageWriter, Res}; +use temper_messages::save_chunk_entities::SaveChunkEntities; +use temper_state::GlobalStateResource; + +pub fn send_save_message( + mut writer: MessageWriter, + state: Res, +) { + for entry in state.0.world.get_cache() { + writer.write(SaveChunkEntities(entry.key().0)); + } +} diff --git a/src/game_systems/src/world/src/lib.rs b/src/game_systems/src/world/src/lib.rs index 94dcc53b..2bcccfde 100644 --- a/src/game_systems/src/world/src/lib.rs +++ b/src/game_systems/src/world/src/lib.rs @@ -1,5 +1 @@ -mod particles; - -pub fn register_world_systems(schedule: &mut bevy_ecs::schedule::Schedule) { - schedule.add_systems(particles::handle); -} +pub mod particles; diff --git a/src/game_systems/tests/interactions.rs b/src/game_systems/tests/interactions.rs new file mode 100644 index 00000000..35b38d68 --- /dev/null +++ b/src/game_systems/tests/interactions.rs @@ -0,0 +1,2 @@ +#[path = "interactions/block_interactions.rs"] +mod block_interactions; diff --git a/src/game_systems/tests/interactions/block_interactions.rs b/src/game_systems/tests/interactions/block_interactions.rs new file mode 100644 index 00000000..8e887fbb --- /dev/null +++ b/src/game_systems/tests/interactions/block_interactions.rs @@ -0,0 +1,75 @@ +use interactions::block_interactions::{ + get_interaction_type, is_interactive, try_interact, InteractionResult, InteractionType, +}; +use std::collections::BTreeMap; +use temper_core::block_data::BlockData; +use temper_core::block_state_id::BlockStateId; +use temper_macros::block; + +#[test] +fn door_detection() { + let door_data = BlockData { + name: "minecraft:oak_door".to_string(), + properties: Some(BTreeMap::from([ + ("facing".to_string(), "north".to_string()), + ("open".to_string(), "false".to_string()), + ("half".to_string(), "lower".to_string()), + ("hinge".to_string(), "left".to_string()), + ])), + }; + + assert!(matches!( + get_interaction_type(&door_data), + Some(InteractionType::Toggleable("open")) + )); +} + +#[test] +fn try_interact_opens_door() { + let closed_door = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); + + let result = try_interact(closed_door); + let InteractionResult::Toggled(new_id) = result else { + panic!("Expected Toggled, got {:?}", result); + }; + + let new_data = new_id + .to_block_data() + .expect("new state ID should be valid"); + let props = new_data.properties.expect("door should have properties"); + assert_eq!(props["open"], "true"); +} + +#[test] +fn try_interact_closes_door() { + let open_door = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: true, powered: false }); + + let result = try_interact(open_door); + let InteractionResult::Toggled(new_id) = result else { + panic!("Expected Toggled, got {:?}", result); + }; + + let new_data = new_id + .to_block_data() + .expect("new state ID should be valid"); + let props = new_data.properties.expect("door should have properties"); + assert_eq!(props["open"], "false"); +} + +#[test] +fn try_interact_not_interactive() { + let stone = block!("stone"); + assert!(matches!( + try_interact(stone), + InteractionResult::NotInteractive + )); +} + +#[test] +fn is_interactive_reports_doors_only() { + let door = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); + let stone = block!("stone"); + + assert!(is_interactive(door)); + assert!(!is_interactive(stone)); +} diff --git a/src/game_systems/tests/mobs.rs b/src/game_systems/tests/mobs.rs new file mode 100644 index 00000000..3435904e --- /dev/null +++ b/src/game_systems/tests/mobs.rs @@ -0,0 +1,8 @@ +#[path = "mobs/chunk_visibility_lifecycle.rs"] +mod chunk_visibility_lifecycle; +#[path = "mobs/cross_chunk_persistence.rs"] +mod cross_chunk_persistence; +#[path = "mobs/entity_persistence.rs"] +mod entity_persistence; +#[path = "mobs/player_distance_reload.rs"] +mod player_distance_reload; diff --git a/src/game_systems/tests/mobs/chunk_visibility_lifecycle.rs b/src/game_systems/tests/mobs/chunk_visibility_lifecycle.rs new file mode 100644 index 00000000..0fda222c --- /dev/null +++ b/src/game_systems/tests/mobs/chunk_visibility_lifecycle.rs @@ -0,0 +1,278 @@ +use background::{chunk_unloader, entity_unloader}; +use bevy_ecs::prelude::*; +use mobs::ground::{load_fox, load_pig, save_fox, save_pig}; +use player::chunk_calculator; +use temper_components::entity_identity::Identity; +use temper_components::last_chunk_pos::LastChunkPos; +use temper_components::player::chunk_receiver::ChunkReceiver; +use temper_components::player::client_information::ClientInformationComponent; +use temper_components::player::player_marker::PlayerMarker; +use temper_components::player::position::Position; +use temper_core::dimension::Dimension; +use temper_core::pos::ChunkPos; +use temper_entities::markers::entity_types::{Fox, Pig}; +use temper_entities::markers::{HasCollisions, HasGravity, HasWaterDrag}; +use temper_entities::{FoxBundle, PigBundle}; +use temper_messages::chunk_calc::ChunkCalc; +use temper_messages::load_chunk_entities::LoadChunkEntities; +use temper_state::create_test_state; + +fn emit_chunk_calc_for(entity: Entity) -> impl FnMut(MessageWriter) { + move |mut writer: MessageWriter| { + writer.write(ChunkCalc(entity)); + } +} + +fn emit_load_messages_for_known_chunks( + state: Res, + mut query: Query<&mut ChunkReceiver>, + mut writer: MessageWriter, +) { + for mut receiver in query.iter_mut() { + while let Some((x, z)) = receiver.loading.pop_front() { + let chunk = ChunkPos::new(x, z); + receiver.loaded.insert((x, z)); + + if state + .0 + .world + .chunk_exists(chunk, Dimension::Overworld) + .expect("chunk existence check should succeed") + { + writer.write(LoadChunkEntities(chunk)); + } + } + } +} + +fn spawn_test_player(world: &mut World, position: Position, loaded_chunk: ChunkPos) -> Entity { + let mut receiver = ChunkReceiver::default(); + receiver.loaded.insert((loaded_chunk.x(), loaded_chunk.z())); + + world + .spawn(( + position, + receiver, + ClientInformationComponent { + view_distance: 2, + ..Default::default() + }, + PlayerMarker, + )) + .id() +} + +#[test] +fn multiple_entities_in_one_chunk_reload_together_when_player_returns() { + let mut world = World::new(); + temper_messages::register_messages(&mut world); + + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state.clone()); + + let fox_position = Position::new(8.0, 64.0, 8.0); + let pig_position = Position::new(9.0, 64.0, 9.0); + let chunk = fox_position.chunk(); + + let fox_bundle = FoxBundle::new(fox_position); + let pig_bundle = PigBundle::new(pig_position); + let expected_fox_uuid = fox_bundle.identity.uuid; + let expected_pig_uuid = pig_bundle.identity.uuid; + + let player_entity = spawn_test_player(&mut world, fox_position, chunk); + + world.spawn(( + fox_bundle, + Fox, + HasGravity, + HasCollisions, + HasWaterDrag, + LastChunkPos::new(chunk), + )); + world.spawn(( + pig_bundle, + Pig, + HasGravity, + HasCollisions, + HasWaterDrag, + LastChunkPos::new(chunk), + )); + + { + let chunk_ref = state + .0 + .world + .get_or_generate_chunk(chunk, Dimension::Overworld) + .expect("test chunk should be cached"); + chunk_ref.entities.clear(); + chunk_ref.mark_dirty(); + } + + { + let mut player_pos = world + .get_mut::(player_entity) + .expect("player should exist"); + *player_pos = Position::new(2048.0, 64.0, 2048.0); + } + + let mut chunk_calc_schedule = Schedule::default(); + chunk_calc_schedule + .add_systems((emit_chunk_calc_for(player_entity), chunk_calculator::handle).chain()); + chunk_calc_schedule.run(&mut world); + + let mut unload_schedule = Schedule::default(); + unload_schedule.add_systems( + ( + entity_unloader::handle, + save_fox, + save_pig, + chunk_unloader::handle, + ) + .chain(), + ); + unload_schedule.run(&mut world); + + let mut entity_ref_query = world.query::>(); + assert_eq!( + entity_ref_query + .iter(&world) + .filter(|entity| entity.contains::()) + .count(), + 0, + "fox should unload when the player leaves the chunk" + ); + assert_eq!( + entity_ref_query + .iter(&world) + .filter(|entity| entity.contains::()) + .count(), + 0, + "pig should unload when the player leaves the chunk" + ); + + { + let saved_chunk = state + .0 + .world + .get_chunk(chunk, Dimension::Overworld) + .expect("saved chunk should exist"); + assert_eq!( + saved_chunk.entities.len(), + 2, + "both entities should be persisted" + ); + } + state.0.world.get_cache().clear(); + + { + let mut receiver = world + .get_mut::(player_entity) + .expect("player chunk receiver should exist"); + receiver.loading.clear(); + receiver.unloading.clear(); + receiver.dirty.clear(); + receiver.loaded.clear(); + } + { + let mut player_pos = world + .get_mut::(player_entity) + .expect("player should still exist"); + *player_pos = fox_position; + } + + chunk_calc_schedule.run(&mut world); + + let mut load_schedule = Schedule::default(); + load_schedule.add_systems((emit_load_messages_for_known_chunks, load_fox, load_pig).chain()); + load_schedule.run(&mut world); + + let mut fox_query = world.query::<(&Identity, Has)>(); + let loaded_foxes: Vec<_> = fox_query + .iter(&world) + .filter(|(_, is_fox)| *is_fox) + .map(|(identity, _)| identity.uuid) + .collect(); + let mut pig_query = world.query::<(&Identity, Has)>(); + let loaded_pigs: Vec<_> = pig_query + .iter(&world) + .filter(|(_, is_pig)| *is_pig) + .map(|(identity, _)| identity.uuid) + .collect(); + + assert_eq!(loaded_foxes, vec![expected_fox_uuid]); + assert_eq!(loaded_pigs, vec![expected_pig_uuid]); +} + +#[test] +fn chunk_stays_loaded_while_a_second_player_keeps_it_visible() { + let mut world = World::new(); + temper_messages::register_messages(&mut world); + + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state.clone()); + + let fox_position = Position::new(8.0, 64.0, 8.0); + let chunk = fox_position.chunk(); + let fox_bundle = FoxBundle::new(fox_position); + let fox_uuid = fox_bundle.identity.uuid; + + let moving_player = spawn_test_player(&mut world, fox_position, chunk); + let _anchored_player = spawn_test_player(&mut world, fox_position, chunk); + + world.spawn(( + fox_bundle, + Fox, + HasGravity, + HasCollisions, + HasWaterDrag, + LastChunkPos::new(chunk), + )); + + { + let chunk_ref = state + .0 + .world + .get_or_generate_chunk(chunk, Dimension::Overworld) + .expect("test chunk should be cached"); + chunk_ref.entities.clear(); + chunk_ref.mark_dirty(); + } + + { + let mut player_pos = world + .get_mut::(moving_player) + .expect("moving player should exist"); + *player_pos = Position::new(2048.0, 64.0, 2048.0); + } + + let mut chunk_calc_schedule = Schedule::default(); + chunk_calc_schedule + .add_systems((emit_chunk_calc_for(moving_player), chunk_calculator::handle).chain()); + chunk_calc_schedule.run(&mut world); + + let mut unload_schedule = Schedule::default(); + unload_schedule + .add_systems((entity_unloader::handle, save_fox, chunk_unloader::handle).chain()); + unload_schedule.run(&mut world); + + let mut fox_query = world.query::<(&Identity, Has)>(); + let live_foxes: Vec<_> = fox_query + .iter(&world) + .filter(|(_, is_fox)| *is_fox) + .map(|(identity, _)| identity.uuid) + .collect(); + + assert_eq!( + live_foxes, + vec![fox_uuid], + "fox should stay live while another player still keeps the chunk visible" + ); + assert!( + state + .0 + .world + .get_cache() + .contains_key(&(chunk, Dimension::Overworld)), + "chunk should stay cached while a second player still has it loaded" + ); +} diff --git a/src/game_systems/tests/mobs/cross_chunk_persistence.rs b/src/game_systems/tests/mobs/cross_chunk_persistence.rs new file mode 100644 index 00000000..f0a4fafe --- /dev/null +++ b/src/game_systems/tests/mobs/cross_chunk_persistence.rs @@ -0,0 +1,140 @@ +use background::cross_chunk_border; +use bevy_ecs::prelude::*; +use mobs::ground::{load_fox, save_fox}; +use physics::chunk_boundary; +use temper_components::entity_identity::Identity; +use temper_components::last_chunk_pos::LastChunkPos; +use temper_components::player::position::Position; +use temper_core::dimension::Dimension; +use temper_entities::entity_types::EntityTypeEnum; +use temper_entities::markers::entity_types::Fox; +use temper_entities::markers::{HasCollisions, HasGravity, HasWaterDrag}; +use temper_entities::FoxBundle; +use temper_messages::load_chunk_entities::LoadChunkEntities; +use temper_messages::save_chunk_entities::SaveChunkEntities; +use temper_state::create_test_state; + +fn emit_save_for( + chunk: temper_core::pos::ChunkPos, +) -> impl FnMut(MessageWriter) { + move |mut writer: MessageWriter| { + writer.write(SaveChunkEntities(chunk)); + } +} + +fn emit_load_for( + chunk: temper_core::pos::ChunkPos, +) -> impl FnMut(MessageWriter) { + move |mut writer: MessageWriter| { + writer.write(LoadChunkEntities(chunk)); + } +} + +#[test] +fn mob_crossing_a_chunk_border_reloads_from_its_new_chunk() { + let mut world = World::new(); + temper_messages::register_messages(&mut world); + + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state.clone()); + + let old_position = Position::new(15.5, 64.0, 8.0); + let new_position = Position::new(16.5, 64.0, 8.0); + let old_chunk = old_position.chunk(); + let new_chunk = new_position.chunk(); + let fox_bundle = FoxBundle::new(old_position); + let expected_identity = fox_bundle.identity.clone(); + + let fox_entity = world + .spawn(( + fox_bundle, + Fox, + HasGravity, + HasCollisions, + HasWaterDrag, + LastChunkPos::new(old_chunk), + )) + .id(); + + { + let old_chunk_ref = state + .0 + .world + .get_or_generate_chunk(old_chunk, Dimension::Overworld) + .expect("old chunk should be cached"); + old_chunk_ref.entities.clear(); + old_chunk_ref.mark_dirty(); + } + + let mut initial_save_schedule = Schedule::default(); + initial_save_schedule.add_systems((emit_save_for(old_chunk), save_fox).chain()); + initial_save_schedule.run(&mut world); + + { + let mut position = world + .get_mut::(fox_entity) + .expect("fox should still be alive"); + *position = new_position; + } + + let mut boundary_schedule = Schedule::default(); + boundary_schedule.add_systems( + ( + chunk_boundary::handle, + cross_chunk_border::cross_chunk_border, + ) + .chain(), + ); + boundary_schedule.run(&mut world); + + { + let old_chunk_ref = state + .0 + .world + .get_chunk(old_chunk, Dimension::Overworld) + .expect("old chunk should exist"); + assert!( + !old_chunk_ref.entities.contains_key(&expected_identity.uuid), + "fox should no longer be stored in the old chunk after crossing the border" + ); + } + { + let new_chunk_ref = state + .0 + .world + .get_chunk(new_chunk, Dimension::Overworld) + .expect("new chunk should exist"); + let stored = new_chunk_ref + .entities + .get(&expected_identity.uuid) + .expect("fox should be stored in its new chunk after crossing the border"); + assert_eq!(stored.value().0, EntityTypeEnum::Fox); + } + + let mut refresh_save_schedule = Schedule::default(); + refresh_save_schedule.add_systems((emit_save_for(new_chunk), save_fox).chain()); + refresh_save_schedule.run(&mut world); + + world.despawn(fox_entity); + + let mut load_schedule = Schedule::default(); + load_schedule.add_systems((emit_load_for(new_chunk), load_fox).chain()); + load_schedule.run(&mut world); + + let mut fox_query = world.query::<(&Identity, &Position, &LastChunkPos, Has)>(); + let loaded_foxes: Vec<_> = fox_query + .iter(&world) + .filter(|(_, _, _, is_fox)| *is_fox) + .collect(); + assert_eq!( + loaded_foxes.len(), + 1, + "fox should load once from the new chunk" + ); + + let (identity, position, last_chunk, is_fox) = loaded_foxes[0]; + assert!(is_fox, "reloaded entity should still have the Fox marker"); + assert_eq!(identity.uuid, expected_identity.uuid); + assert_eq!(position.coords, new_position.coords); + assert_eq!(last_chunk.0, new_chunk); +} diff --git a/src/game_systems/tests/mobs/entity_persistence.rs b/src/game_systems/tests/mobs/entity_persistence.rs new file mode 100644 index 00000000..dc0fe913 --- /dev/null +++ b/src/game_systems/tests/mobs/entity_persistence.rs @@ -0,0 +1,217 @@ +use bevy_ecs::prelude::*; +use mobs::ground::{load_fox, load_pig, save_fox, save_pig}; +use temper_components::entity_identity::Identity; +use temper_components::last_chunk_pos::LastChunkPos; +use temper_components::last_synced_position::LastSyncedPosition; +use temper_components::player::position::Position; +use temper_core::dimension::Dimension; +use temper_entities::entity_types::EntityTypeEnum; +use temper_entities::markers::entity_types::{Fox, Pig}; +use temper_entities::markers::{HasCollisions, HasGravity, HasWaterDrag}; +use temper_entities::{FoxBundle, PigBundle}; +use temper_messages::load_chunk_entities::LoadChunkEntities; +use temper_messages::save_chunk_entities::SaveChunkEntities; +use temper_state::create_test_state; + +fn emit_save_for( + chunk: temper_core::pos::ChunkPos, +) -> impl FnMut(MessageWriter) { + move |mut writer: MessageWriter| { + writer.write(SaveChunkEntities(chunk)); + } +} + +fn emit_load_for( + chunk: temper_core::pos::ChunkPos, +) -> impl FnMut(MessageWriter) { + move |mut writer: MessageWriter| { + writer.write(LoadChunkEntities(chunk)); + } +} + +#[test] +fn pig_round_trips_through_chunk_save_and_load() { + let mut world = World::new(); + temper_messages::register_messages(&mut world); + + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state); + + let position = Position::new(5.5, 64.0, 7.5); + let chunk = position.chunk(); + let bundle = PigBundle::new(position); + let expected_identity = bundle.identity.clone(); + let expected_last_synced = bundle.last_synced_position; + + let original_entity = world + .spawn((bundle, Pig, HasGravity, HasCollisions, HasWaterDrag)) + .id(); + + let mut save_schedule = Schedule::default(); + save_schedule.add_systems((emit_save_for(chunk), save_pig).chain()); + save_schedule.run(&mut world); + + { + let state = world.resource::(); + let saved_chunk = state + .0 + .world + .get_chunk(chunk, Dimension::Overworld) + .expect("chunk should exist after save"); + let saved_entity = saved_chunk + .entities + .get(&expected_identity.uuid) + .expect("saved pig should be present in chunk storage"); + + assert_eq!(saved_entity.value().0, EntityTypeEnum::Pig); + } + + world.despawn(original_entity); + + let mut load_schedule = Schedule::default(); + load_schedule.add_systems((emit_load_for(chunk), load_pig).chain()); + load_schedule.run(&mut world); + + let mut query = world.query::<( + &Identity, + &Position, + &LastChunkPos, + &LastSyncedPosition, + Has, + Has, + Has, + Has, + )>(); + + let loaded: Vec<_> = query.iter(&world).collect(); + assert_eq!( + loaded.len(), + 1, + "exactly one pig should be loaded back into ECS" + ); + + let ( + identity, + loaded_position, + last_chunk, + last_synced, + is_pig, + has_gravity, + has_collisions, + has_water_drag, + ) = &loaded[0]; + + assert!(is_pig, "loaded entity should have the Pig marker"); + assert!(has_gravity, "loaded pig should regain HasGravity"); + assert!(has_collisions, "loaded pig should regain HasCollisions"); + assert!(has_water_drag, "loaded pig should regain HasWaterDrag"); + assert_eq!(identity.uuid, expected_identity.uuid); + assert_eq!(identity.entity_id, expected_identity.entity_id); + assert_eq!(loaded_position.coords, position.coords); + assert_eq!(last_chunk.0, chunk); + assert_eq!(last_synced.0, expected_last_synced.0); +} + +#[test] +fn fox_loads_in_a_separate_ecs_world_after_save() { + let (state, _temp_dir) = create_test_state(); + + let position = Position::new(23.5, 70.0, -10.25); + let chunk = position.chunk(); + let bundle = FoxBundle::new(position); + let expected_identity = bundle.identity.clone(); + let expected_last_synced = bundle.last_synced_position; + + { + let mut first_world = World::new(); + temper_messages::register_messages(&mut first_world); + first_world.insert_resource(state.clone()); + first_world.spawn((bundle, Fox, HasGravity, HasCollisions, HasWaterDrag)); + + let mut save_schedule = Schedule::default(); + save_schedule.add_systems((emit_save_for(chunk), save_fox).chain()); + save_schedule.run(&mut first_world); + } + + state + .0 + .world + .sync() + .expect("saved fox should be flushed to storage before restart-style load"); + state.0.world.get_cache().clear(); + + let loaded = { + let mut second_world = World::new(); + temper_messages::register_messages(&mut second_world); + second_world.insert_resource(state.clone()); + + let mut load_schedule = Schedule::default(); + load_schedule.add_systems((emit_load_for(chunk), load_fox).chain()); + load_schedule.run(&mut second_world); + + let mut query = second_world.query::<( + &Identity, + &Position, + &LastChunkPos, + &LastSyncedPosition, + Has, + Has, + Has, + Has, + )>(); + + query + .iter(&second_world) + .map( + |( + identity, + loaded_position, + last_chunk, + last_synced, + is_fox, + has_gravity, + has_collisions, + has_water_drag, + )| { + ( + identity.clone(), + *loaded_position, + *last_chunk, + *last_synced, + is_fox, + has_gravity, + has_collisions, + has_water_drag, + ) + }, + ) + .collect::>() + }; + + assert_eq!( + loaded.len(), + 1, + "exactly one fox should be loaded into the replacement ECS world" + ); + + let ( + identity, + loaded_position, + last_chunk, + last_synced, + is_fox, + has_gravity, + has_collisions, + has_water_drag, + ) = &loaded[0]; + + assert!(is_fox, "loaded entity should have the Fox marker"); + assert!(has_gravity, "loaded fox should regain HasGravity"); + assert!(has_collisions, "loaded fox should regain HasCollisions"); + assert!(has_water_drag, "loaded fox should regain HasWaterDrag"); + assert_eq!(identity.uuid, expected_identity.uuid); + assert_eq!(identity.entity_id, expected_identity.entity_id); + assert_eq!(loaded_position.coords, position.coords); + assert_eq!(last_chunk.0, chunk); + assert_eq!(last_synced.0, expected_last_synced.0); +} diff --git a/src/game_systems/tests/mobs/player_distance_reload.rs b/src/game_systems/tests/mobs/player_distance_reload.rs new file mode 100644 index 00000000..053e2a28 --- /dev/null +++ b/src/game_systems/tests/mobs/player_distance_reload.rs @@ -0,0 +1,223 @@ +use background::{chunk_unloader, entity_unloader}; +use bevy_ecs::prelude::*; +use mobs::ground::{load_fox, save_fox}; +use player::chunk_calculator; +use temper_components::entity_identity::Identity; +use temper_components::last_chunk_pos::LastChunkPos; +use temper_components::last_synced_position::LastSyncedPosition; +use temper_components::player::chunk_receiver::ChunkReceiver; +use temper_components::player::client_information::ClientInformationComponent; +use temper_components::player::player_marker::PlayerMarker; +use temper_components::player::position::Position; +use temper_core::dimension::Dimension; +use temper_core::pos::ChunkPos; +use temper_entities::markers::entity_types::Fox; +use temper_entities::markers::{HasCollisions, HasGravity, HasWaterDrag}; +use temper_entities::FoxBundle; +use temper_messages::chunk_calc::ChunkCalc; +use temper_messages::load_chunk_entities::LoadChunkEntities; +use temper_state::create_test_state; + +fn emit_chunk_calc_for(entity: Entity) -> impl FnMut(MessageWriter) { + move |mut writer: MessageWriter| { + writer.write(ChunkCalc(entity)); + } +} + +fn emit_load_messages_for_known_chunks( + state: Res, + mut query: Query<&mut ChunkReceiver>, + mut writer: MessageWriter, +) { + for mut receiver in query.iter_mut() { + while let Some((x, z)) = receiver.loading.pop_front() { + let chunk = ChunkPos::new(x, z); + receiver.loaded.insert((x, z)); + + if state + .0 + .world + .chunk_exists(chunk, Dimension::Overworld) + .expect("chunk existence check should succeed") + { + writer.write(LoadChunkEntities(chunk)); + } + } + } +} + +#[test] +fn player_can_unload_entities_by_moving_away_and_reload_them_after_returning() { + let mut world = World::new(); + temper_messages::register_messages(&mut world); + + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state.clone()); + + let fox_position = Position::new(8.0, 64.0, 8.0); + let fox_chunk = fox_position.chunk(); + let fox_bundle = FoxBundle::new(fox_position); + let expected_identity = fox_bundle.identity.clone(); + let expected_last_synced = fox_bundle.last_synced_position; + + let mut receiver = ChunkReceiver::default(); + receiver.loaded.insert((fox_chunk.x(), fox_chunk.z())); + + let player_entity = world + .spawn(( + Position::new(8.0, 64.0, 8.0), + receiver, + ClientInformationComponent { + view_distance: 2, + ..Default::default() + }, + PlayerMarker, + )) + .id(); + + world.spawn(( + fox_bundle, + Fox, + HasGravity, + HasCollisions, + HasWaterDrag, + LastChunkPos::new(fox_chunk), + )); + + { + let chunk = state + .0 + .world + .get_or_generate_chunk(fox_chunk, Dimension::Overworld) + .expect("fox chunk should be cached before unload"); + chunk.entities.clear(); + chunk.mark_dirty(); + } + + { + let mut player_pos = world + .get_mut::(player_entity) + .expect("player should exist"); + *player_pos = Position::new(2048.0, 64.0, 2048.0); + } + + let mut chunk_calc_schedule = Schedule::default(); + chunk_calc_schedule + .add_systems((emit_chunk_calc_for(player_entity), chunk_calculator::handle).chain()); + chunk_calc_schedule.run(&mut world); + + let mut unload_schedule = Schedule::default(); + unload_schedule + .add_systems((entity_unloader::handle, save_fox, chunk_unloader::handle).chain()); + unload_schedule.run(&mut world); + + let mut fox_query = world.query::>(); + let live_foxes = fox_query + .iter(&world) + .filter(|entity| entity.contains::()) + .count(); + assert_eq!( + live_foxes, 0, + "fox should despawn when its chunk is unloaded" + ); + assert!( + !state + .0 + .world + .get_cache() + .contains_key(&(fox_chunk, Dimension::Overworld)), + "fox chunk should be removed from cache after unload" + ); + { + let saved_chunk = state + .0 + .world + .get_chunk(fox_chunk, Dimension::Overworld) + .expect("saved fox chunk should be readable from storage"); + assert_eq!( + saved_chunk.entities.len(), + 1, + "only the test fox should be persisted in the chunk" + ); + } + state.0.world.get_cache().clear(); + + { + let mut receiver = world + .get_mut::(player_entity) + .expect("player chunk receiver should exist"); + receiver.loading.clear(); + receiver.unloading.clear(); + receiver.dirty.clear(); + receiver.loaded.clear(); + } + { + let mut player_pos = world + .get_mut::(player_entity) + .expect("player should still exist"); + *player_pos = fox_position; + } + + chunk_calc_schedule.run(&mut world); + + { + let receiver = world + .get::(player_entity) + .expect("player chunk receiver should exist after returning"); + let queued_fox_chunks = receiver + .loading + .iter() + .filter(|(x, z)| (*x, *z) == (fox_chunk.x(), fox_chunk.z())) + .count(); + assert_eq!( + queued_fox_chunks, 1, + "player return should queue the fox chunk exactly once" + ); + } + + let mut load_schedule = Schedule::default(); + load_schedule.add_systems((emit_load_messages_for_known_chunks, load_fox).chain()); + load_schedule.run(&mut world); + + let mut fox_query = world.query::<( + &Identity, + &Position, + &LastChunkPos, + &LastSyncedPosition, + Has, + Has, + Has, + Has, + )>(); + let loaded_foxes: Vec<_> = fox_query + .iter(&world) + .filter(|(_, _, _, _, is_fox, _, _, _)| *is_fox) + .collect(); + + assert_eq!( + loaded_foxes.len(), + 1, + "fox should reload when the player returns" + ); + + let ( + identity, + loaded_position, + last_chunk, + last_synced, + is_fox, + has_gravity, + has_collisions, + has_water_drag, + ) = loaded_foxes[0]; + + assert!(is_fox, "reloaded entity should have the Fox marker"); + assert!(has_gravity, "reloaded fox should regain HasGravity"); + assert!(has_collisions, "reloaded fox should regain HasCollisions"); + assert!(has_water_drag, "reloaded fox should regain HasWaterDrag"); + assert_eq!(identity.uuid, expected_identity.uuid); + assert_eq!(identity.entity_id, expected_identity.entity_id); + assert_eq!(loaded_position.coords, fox_position.coords); + assert_eq!(last_chunk.0, fox_chunk); + assert_eq!(last_synced.0, expected_last_synced.0); +} diff --git a/src/game_systems/tests/physics.rs b/src/game_systems/tests/physics.rs new file mode 100644 index 00000000..e895187f --- /dev/null +++ b/src/game_systems/tests/physics.rs @@ -0,0 +1,4 @@ +#[path = "physics/gravity.rs"] +mod gravity; +#[path = "physics/velocity.rs"] +mod velocity; diff --git a/src/game_systems/tests/physics/gravity.rs b/src/game_systems/tests/physics/gravity.rs new file mode 100644 index 00000000..45fd1642 --- /dev/null +++ b/src/game_systems/tests/physics/gravity.rs @@ -0,0 +1,159 @@ +use bevy_ecs::prelude::*; +use bevy_math::{DVec3, Vec3A}; +use physics::gravity::handle; +use temper_components::player::grounded::OnGround; +use temper_components::player::position::Position; +use temper_components::player::velocity::Velocity; +use temper_core::block_state_id::BlockStateId; +use temper_core::dimension::Dimension; +use temper_core::pos::ChunkPos; +use temper_entities::markers::{HasGravity, HasWaterDrag}; +use temper_macros::block; +use temper_state::{create_test_state, GlobalStateResource}; + +fn create_chunk_with_water(state: &GlobalStateResource, chunk_pos: ChunkPos) { + let mut chunk = state + .0 + .world + .get_or_generate_mut(chunk_pos, Dimension::Overworld) + .expect("Failed to load or generate chunk"); + + chunk.fill(block!("water", { level: 0 })); +} + +#[test] +fn gravity_application() { + let mut world = World::new(); + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state); + + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + OnGround(false), + Position { + coords: DVec3::new(0.0, 100.0, 0.0), + }, + HasGravity, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert!(vel.vec.y < 0.0); +} + +#[test] +fn gravity_no_gravity_when_grounded() { + let mut world = World::new(); + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state); + + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + OnGround(true), + Position { + coords: DVec3::new(0.0, 100.0, 0.0), + }, + HasGravity, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert_eq!(vel.vec.y, 0.0); +} + +#[test] +fn gravity_water_entity_not_in_water() { + let mut world = World::new(); + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state); + + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + OnGround(false), + Position { + coords: DVec3::new(0.0, 100.0, 0.0), + }, + HasGravity, + HasWaterDrag, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert!(vel.vec.y < 0.0); +} + +#[test] +fn gravity_water_entity_no_gravity_when_grounded() { + let mut world = World::new(); + let (state, _temp_dir) = create_test_state(); + world.insert_resource(state); + + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + OnGround(true), + Position { + coords: DVec3::new(0.0, 100.0, 0.0), + }, + HasGravity, + HasWaterDrag, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert_eq!(vel.vec.y, 0.0); +} + +#[test] +fn gravity_water_entity_in_water_no_gravity() { + let mut world = World::new(); + let (state, _temp_dir) = create_test_state(); + + let chunk_pos = ChunkPos::new(0, 0); + create_chunk_with_water(&state, chunk_pos); + + world.insert_resource(state); + + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + OnGround(false), + Position { + coords: DVec3::new(0.0, 65.0, 0.0), + }, + HasGravity, + HasWaterDrag, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert_eq!(vel.vec.y, 0.0); +} diff --git a/src/game_systems/tests/physics/velocity.rs b/src/game_systems/tests/physics/velocity.rs new file mode 100644 index 00000000..73811c6f --- /dev/null +++ b/src/game_systems/tests/physics/velocity.rs @@ -0,0 +1,125 @@ +use bevy_ecs::message::MessageRegistry; +use bevy_ecs::prelude::*; +use bevy_math::Vec3A; +use physics::velocity::handle; +use temper_components::player::position::Position; +use temper_components::player::velocity::Velocity; +use temper_messages::entity_update::SendEntityUpdate; + +#[test] +fn velocity_updates_position() { + let mut world = World::new(); + let entity = world + .spawn(( + Velocity { + vec: Vec3A::new(1.0, 2.0, 3.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + MessageRegistry::register_message::(&mut world); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let pos = world.get::(entity).unwrap(); + assert_eq!(pos.coords, Vec3A::new(1.0, 2.0, 3.0).as_dvec3()); +} + +#[test] +fn velocity_no_update_when_unchanged() { + let mut world = World::new(); + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + + MessageRegistry::register_message::(&mut world); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + assert!(world.get::(entity).is_some()); + assert_eq!( + world.get::(entity).unwrap().coords, + Vec3A::ZERO.as_dvec3() + ); + + let reader = world.get_resource::>().unwrap(); + let mut cursor = reader.get_cursor(); + let mut messages = vec![]; + for msg in cursor.read(reader) { + messages.push(msg); + } + assert_eq!(messages.len(), 0); +} + +#[test] +fn velocity_multiple_steps() { + let mut world = World::new(); + let entity = world + .spawn(( + Velocity { + vec: Vec3A::new(0.5, 0.0, 0.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + for _ in 0..4 { + schedule.run(&mut world); + } + + let pos = world.get::(entity).unwrap(); + assert_eq!(pos.coords, Vec3A::new(2.0, 0.0, 0.0).as_dvec3()); +} + +#[test] +fn velocity_multiple_entities() { + let mut world = World::new(); + let entity1 = world + .spawn(( + Velocity { + vec: Vec3A::new(1.0, 0.0, 0.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + let entity2 = world + .spawn(( + Velocity { + vec: Vec3A::new(0.0, 1.0, 0.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + schedule.run(&mut world); + + let pos1 = world.get::(entity1).unwrap(); + let pos2 = world.get::(entity2).unwrap(); + assert_eq!(pos1.coords, Vec3A::new(1.0, 0.0, 0.0).as_dvec3()); + assert_eq!(pos2.coords, Vec3A::new(0.0, 1.0, 0.0).as_dvec3()); +} diff --git a/src/messages/Cargo.toml b/src/messages/Cargo.toml index cc219375..c1cbb99c 100644 --- a/src/messages/Cargo.toml +++ b/src/messages/Cargo.toml @@ -14,3 +14,4 @@ temper-codec = { workspace = true } temper-inventories = { workspace = true } temper-particles = { workspace = true } temper-commands = { workspace = true } +temper-entities = { workspace = true } diff --git a/src/messages/src/cross_chunk_boundary_event.rs b/src/messages/src/cross_chunk_boundary_event.rs index feaa7ff0..96e1379d 100644 --- a/src/messages/src/cross_chunk_boundary_event.rs +++ b/src/messages/src/cross_chunk_boundary_event.rs @@ -1,9 +1,10 @@ use bevy_ecs::prelude::{Entity, Message}; +use temper_core::pos::ChunkPos; -// Fired when a player crosses a chunk boundary. Assumes dimensions are the same +// Fired when an entity crosses a chunk boundary. Assumes dimensions are the same #[derive(Message)] pub struct ChunkBoundaryCrossed { - pub player: Entity, - pub old_chunk: (i32, i32), - pub new_chunk: (i32, i32), + pub entity: Entity, + pub old_chunk: ChunkPos, + pub new_chunk: ChunkPos, } diff --git a/src/messages/src/destroy_entity.rs b/src/messages/src/destroy_entity.rs new file mode 100644 index 00000000..e92c2e70 --- /dev/null +++ b/src/messages/src/destroy_entity.rs @@ -0,0 +1,4 @@ +use bevy_ecs::prelude::{Entity, Message}; + +#[derive(Message)] +pub struct DestroyEntity(pub Entity); diff --git a/src/messages/src/entity_spawn.rs b/src/messages/src/entity_spawn.rs index 7b39a9e3..1415ba3d 100644 --- a/src/messages/src/entity_spawn.rs +++ b/src/messages/src/entity_spawn.rs @@ -1,94 +1,6 @@ use bevy_ecs::prelude::{Entity, Message}; use temper_components::player::position::Position; - -/// Type of entity to spawn -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum EntityType { - // Passive entities - Allay, - Armadillo, - Axolotl, - Bat, - Camel, - Cat, - Chicken, - Cod, - Cow, - Donkey, - Frog, - GlowSquid, - Horse, - Mooshroom, - Mule, - Ocelot, - Parrot, - Pig, - Rabbit, - Salmon, - Sheep, - SkeletonHorse, - Sniffer, - SnowGolem, - Squid, - Strider, - Tadpole, - TropicalFish, - Turtle, - Villager, - WanderingTrader, - ZombieHorse, - - // Neutral entities - Bee, - CaveSpider, - Dolphin, - Drowned, - Enderman, - Fox, - Goat, - IronGolem, - Llama, - Panda, - Piglin, - PolarBear, - Pufferfish, - Spider, - TraderLlama, - Wolf, - ZombifiedPiglin, - - // Hostile entities - Blaze, - Bogged, - Breeze, - Creaking, - Creeper, - ElderGuardian, - Endermite, - Evoker, - Ghast, - Guardian, - Hoglin, - Husk, - MagmaCube, - Phantom, - PiglinBrute, - Pillager, - Ravager, - Shulker, - Silverfish, - Skeleton, - Slime, - Stray, - Vex, - Vindicator, - Warden, - Witch, - WitherSkeleton, - Zoglin, - Zombie, - ZombieVillager, -} +pub(crate) use temper_entities::entity_types::EntityTypeEnum; /// Command to spawn an entity in front of a player. /// @@ -96,7 +8,7 @@ pub enum EntityType { /// the spawn_command_processor system which calculates the spawn position. #[derive(Message)] pub struct SpawnEntityCommand { - pub entity_type: EntityType, + pub entity_type: EntityTypeEnum, pub player_entity: Entity, } @@ -106,6 +18,6 @@ pub struct SpawnEntityCommand { /// the spawn position from the player's position and rotation. #[derive(Message)] pub struct SpawnEntityEvent { - pub entity_type: EntityType, + pub entity_type: EntityTypeEnum, pub position: Position, } diff --git a/src/messages/src/lib.rs b/src/messages/src/lib.rs index a269ac83..19b3571c 100644 --- a/src/messages/src/lib.rs +++ b/src/messages/src/lib.rs @@ -28,21 +28,28 @@ pub mod entity_spawn; pub mod entity_update; pub mod particle; -pub use entity_spawn::{EntityType, SpawnEntityCommand, SpawnEntityEvent}; +pub use entity_spawn::{SpawnEntityCommand, SpawnEntityEvent}; pub mod block_break; pub mod block_interaction; pub mod cross_chunk_boundary_event; +pub mod destroy_entity; pub mod force_player_recount_event; +pub mod load_chunk_entities; pub mod packet_messages; +pub mod save_chunk_entities; pub mod teleport_player; pub mod world_change; use crate::chunk_calc::ChunkCalc; +use crate::cross_chunk_boundary_event::ChunkBoundaryCrossed; +use crate::destroy_entity::DestroyEntity; use crate::entity_update::SendEntityUpdate; use crate::force_player_recount_event::ForcePlayerRecount; +use crate::load_chunk_entities::LoadChunkEntities; use crate::packet_messages::Movement; use crate::particle::SendParticle; +use crate::save_chunk_entities::SaveChunkEntities; use crate::teleport_player::TeleportPlayer; pub use block_break::BlockBrokenEvent; pub use block_interaction::{BlockInteractMessage, BlockToggledEvent, DoorToggledEvent}; @@ -77,4 +84,8 @@ pub fn register_messages(world: &mut World) { MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); } diff --git a/src/messages/src/load_chunk_entities.rs b/src/messages/src/load_chunk_entities.rs new file mode 100644 index 00000000..e7cd77f4 --- /dev/null +++ b/src/messages/src/load_chunk_entities.rs @@ -0,0 +1,4 @@ +use bevy_ecs::prelude::Message; + +#[derive(Message)] +pub struct LoadChunkEntities(pub temper_core::pos::ChunkPos); diff --git a/src/messages/src/player_leave.rs b/src/messages/src/player_leave.rs index 467092c5..881aff3d 100644 --- a/src/messages/src/player_leave.rs +++ b/src/messages/src/player_leave.rs @@ -3,4 +3,7 @@ use temper_components::entity_identity::Identity; #[derive(Message, Clone)] #[allow(unused)] -pub struct PlayerLeft(pub Identity); +pub struct PlayerLeft { + pub identity: Identity, + pub entity: Entity, +} diff --git a/src/messages/src/save_chunk_entities.rs b/src/messages/src/save_chunk_entities.rs new file mode 100644 index 00000000..278de9c5 --- /dev/null +++ b/src/messages/src/save_chunk_entities.rs @@ -0,0 +1,5 @@ +use bevy_ecs::prelude::Message; +use temper_core::pos::ChunkPos; + +#[derive(Message)] +pub struct SaveChunkEntities(pub ChunkPos); diff --git a/src/world/format/Cargo.toml b/src/world/format/Cargo.toml index de6a4571..4dea9158 100644 --- a/src/world/format/Cargo.toml +++ b/src/world/format/Cargo.toml @@ -22,6 +22,9 @@ serde = { workspace = true } bytemuck = { workspace = true } tokio = { workspace = true } type_hash = { workspace = true } +uuid = { workspace = true } +temper-entities = { workspace = true } +dashmap = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/src/world/format/src/lib.rs b/src/world/format/src/lib.rs index 000c2c7f..1b505cb2 100644 --- a/src/world/format/src/lib.rs +++ b/src/world/format/src/lib.rs @@ -9,20 +9,27 @@ pub mod vanilla_chunk_format; use crate::errors::WorldError; use crate::heightmap::Heightmaps; use crate::section::{AIR, ChunkSection}; -use deepsize::DeepSizeOf; +use dashmap::DashMap; use serde_derive::{Deserialize, Serialize}; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; use temper_core::block_state_id::BlockStateId; use temper_core::pos::{ChunkBlockPos, ChunkHeight}; +use temper_entities::entity_types::EntityTypeEnum; use temper_macros::block; use type_hash::TypeHash; +use uuid::Uuid; use vanilla_chunk_format::VanillaChunk; -#[derive(Clone, DeepSizeOf, Serialize, Deserialize, TypeHash)] +#[derive(Clone, Serialize, Deserialize, TypeHash)] pub struct Chunk { pub sections: Box<[ChunkSection]>, height: ChunkHeight, + #[type_hash(foreign_type)] + pub entities: DashMap)>, heightmaps: Option, + dirty: Arc, } impl Chunk { @@ -51,7 +58,9 @@ impl Chunk { sections: vec![ChunkSection::new_uniform(AIR); (height.height / 16) as usize] .into_boxed_slice(), height, + entities: DashMap::new(), heightmaps: None, + dirty: Arc::new(AtomicBool::new(false)), } } @@ -76,6 +85,8 @@ impl Chunk { sections: sections.to_vec().into_boxed_slice(), height, heightmaps: None, + entities: DashMap::new(), + dirty: Arc::new(AtomicBool::new(false)), } } @@ -149,6 +160,38 @@ impl Chunk { self.sections[section as usize].set_block(pos.section_block_pos(), id); } + + /// Marks the chunk as dirty. + /// + /// This indicates that the chunk has been modified and may need to be saved or updated. + pub fn mark_dirty(&self) { + self.dirty.store(true, std::sync::atomic::Ordering::Relaxed); + } + + /// Checks if the chunk is dirty. + /// + /// A chunk is considered dirty if it has been marked as dirty or if any of its sections are dirty. + /// A dirty chunk may need to be saved or updated. + pub fn is_dirty(&self) -> bool { + self.dirty.load(std::sync::atomic::Ordering::Relaxed) + || self + .sections + .iter() + .any(|s| s.dirty.load(std::sync::atomic::Ordering::Relaxed)) + } + + /// Clears the dirty state of the chunk and all of its sections. + /// + /// This should be called after saving or updating a chunk to indicate that it is no longer dirty. + pub fn clear_dirty(&self) { + self.dirty + .store(false, std::sync::atomic::Ordering::Relaxed); + for section in &self.sections { + section + .dirty + .store(false, std::sync::atomic::Ordering::Relaxed); + } + } } impl TryFrom<&VanillaChunk> for Chunk { @@ -180,6 +223,8 @@ impl TryFrom<&VanillaChunk> for Chunk { .heightmaps .as_ref() .and_then(|v| Heightmaps::try_from(v).ok()), + entities: DashMap::new(), + dirty: Arc::new(AtomicBool::new(false)), }) } } diff --git a/src/world/format/src/section/mod.rs b/src/world/format/src/section/mod.rs index aeec1100..cb6c7dd2 100644 --- a/src/world/format/src/section/mod.rs +++ b/src/world/format/src/section/mod.rs @@ -7,6 +7,8 @@ use crate::section::uniform::UniformSection; use crate::vanilla_chunk_format::Section; use deepsize::DeepSizeOf; use serde_derive::{Deserialize, Serialize}; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; use temper_core::block_state_id::BlockStateId; use temper_core::pos::SectionBlockPos; use temper_macros::block; @@ -100,7 +102,7 @@ pub struct ChunkSection { pub(crate) inner: ChunkSectionType, pub(crate) light: SectionLightData, pub(crate) biome: BiomeData, - pub dirty: bool, + pub(crate) dirty: Arc, } impl ChunkSection { @@ -109,7 +111,7 @@ impl ChunkSection { inner: ChunkSectionType::Uniform(UniformSection::new_with(id)), light: SectionLightData::default(), biome: BiomeData::Uniform(BiomeType(5)), - dirty: true, + dirty: Arc::new(AtomicBool::new(true)), } } @@ -119,7 +121,7 @@ impl ChunkSection { inner: ChunkSectionType::Uniform(UniformSection::air()), light: SectionLightData::default(), biome: BiomeData::Uniform(BiomeType(5)), - dirty: true, + dirty: Arc::new(AtomicBool::new(true)), } } else if unique_blocks < 256 { Self { @@ -128,14 +130,14 @@ impl ChunkSection { )), light: SectionLightData::default(), biome: BiomeData::Uniform(BiomeType(5)), - dirty: true, + dirty: Arc::new(AtomicBool::new(true)), } } else { Self { inner: ChunkSectionType::Direct(DirectSection::default()), light: SectionLightData::default(), biome: BiomeData::Uniform(BiomeType(5)), - dirty: true, + dirty: Arc::new(AtomicBool::new(true)), } } } @@ -147,13 +149,13 @@ impl ChunkSection { #[inline] pub fn set_block(&mut self, pos: SectionBlockPos, id: BlockStateId) { - self.dirty = true; + self.dirty.store(true, std::sync::atomic::Ordering::Relaxed); self.inner.set_block(pos, id); } #[inline] pub fn fill(&mut self, id: BlockStateId) { - self.dirty = true; + self.dirty.store(true, std::sync::atomic::Ordering::Relaxed); self.inner.fill(id); } @@ -227,7 +229,7 @@ impl TryFrom<&Section> for ChunkSection { return Ok(Self { light: light_data, biome: BiomeData::Uniform(BiomeType(5)), - dirty: false, + dirty: Arc::new(AtomicBool::new(false)), inner: ChunkSectionType::Uniform(UniformSection::air()), }); @@ -249,14 +251,14 @@ impl TryFrom<&Section> for ChunkSection { Ok(Self { light: light_data, biome: BiomeData::Uniform(BiomeType(5)), - dirty: false, + dirty: Arc::new(AtomicBool::new(false)), inner: section_data, }) } else { Ok(Self { light: light_data, biome: BiomeData::Uniform(BiomeType(5)), - dirty: false, + dirty: Arc::new(AtomicBool::new(false)), inner: ChunkSectionType::Uniform(UniformSection::air()), }) } diff --git a/src/world/src/db_wrap.rs b/src/world/src/db_wrap.rs index 4e022736..c5eba9d2 100644 --- a/src/world/src/db_wrap.rs +++ b/src/world/src/db_wrap.rs @@ -20,8 +20,7 @@ impl World { dimension: Dimension, chunk: Chunk, ) -> Result<(), WorldError> { - let mut chunk = chunk; - chunk.sections.iter_mut().for_each(|c| c.dirty = false); + chunk.clear_dirty(); save_chunk_internal(&self.storage_backend, pos, dimension, &chunk)?; // self.cache.insert((pos, dimension.to_string()), chunk); Ok(()) @@ -104,13 +103,23 @@ impl World { for pair in self.cache.iter() { let k = pair.key(); let v = pair.value(); - if v.sections.iter().any(|c| c.dirty) { + if v.is_dirty() { trace!("Chunk at {:?} is dirty, saving.", k.0); } else { continue; } trace!("Syncing chunk: {:?}", k.0); + if !v.entities.is_empty() { + trace!( + "Chunk at {:?} has {} entities, saving.", + k.0, + v.entities.len() + ); + } else { + trace!("Chunk at {:?} has no entities, saving.", k.0); + } save_chunk_internal(&self.storage_backend, k.0, k.1, v)?; + v.clear_dirty(); } sync_internal(&self.storage_backend)