diff --git a/Cargo.lock b/Cargo.lock index ef5e3ae2..83b0164a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3035,14 +3035,6 @@ dependencies = [ "valence_protocol", ] -[[package]] -name = "hyperion-respawn" -version = "0.1.0" -dependencies = [ - "hyperion", - "hyperion-utils", -] - [[package]] name = "hyperion-scheduled" version = "0.1.0" @@ -5732,7 +5724,6 @@ dependencies = [ "hyperion-item", "hyperion-permission", "hyperion-rank-tree", - "hyperion-respawn", "hyperion-scheduled", "hyperion-text", "hyperion-utils", @@ -5746,6 +5737,7 @@ dependencies = [ "uuid", "valence_protocol", "valence_server", + "vanilla-behaviors", ] [[package]] @@ -6580,6 +6572,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vanilla-behaviors" +version = "0.1.0" +dependencies = [ + "clap", + "fastrand 2.3.0", + "flecs_ecs", + "geometry", + "hyperion", + "hyperion-clap", + "hyperion-permission", + "hyperion-utils", + "tracing", + "valence_protocol", + "valence_server", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 2e535600..65112f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,13 +34,13 @@ members = [ 'crates/hyperion-proto', 'crates/hyperion-proxy', 'crates/hyperion-rank-tree', - 'crates/hyperion-respawn', 'crates/hyperion-scheduled', 'crates/hyperion-stats', 'crates/hyperion-text', 'crates/hyperion-utils', 'crates/simd-utils', 'crates/system-order', + 'crates/vanilla-behaviors', 'events/tag', 'tools/packet-inspector', 'tools/rust-mc-bot', @@ -203,6 +203,9 @@ path = 'crates/hyperion-text' [workspace.dependencies.hyperion-utils] path = 'crates/hyperion-utils' +[workspace.dependencies.vanilla-behaviors] +path = 'crates/vanilla-behaviors' + [workspace.dependencies.indexmap] features = ['rayon'] version = '2.7.1' @@ -219,9 +222,6 @@ version = '0.3.6' features = ['rustls-tls', 'stream'] version = '0.12.12' -[workspace.dependencies.hyperion-respawn] -path = 'crates/hyperion-respawn' - [workspace.dependencies.roaring] features = ['simd'] version = '0.10.10' diff --git a/crates/hyperion-respawn/Cargo.toml b/crates/hyperion-respawn/Cargo.toml deleted file mode 100644 index 2d233386..00000000 --- a/crates/hyperion-respawn/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "hyperion-respawn" -version = "0.1.0" -edition = "2021" -authors = ["Andrew Gazelka "] -readme = "README.md" -publish = false - -[dependencies] -hyperion = {workspace = true} -hyperion-utils = {workspace = true} - -[lints] -workspace = true diff --git a/crates/hyperion-respawn/README.md b/crates/hyperion-respawn/README.md deleted file mode 100644 index cb2e8806..00000000 --- a/crates/hyperion-respawn/README.md +++ /dev/null @@ -1 +0,0 @@ -# respawn \ No newline at end of file diff --git a/crates/hyperion-utils/src/lib.rs b/crates/hyperion-utils/src/lib.rs index 35237b4d..f13a0a66 100644 --- a/crates/hyperion-utils/src/lib.rs +++ b/crates/hyperion-utils/src/lib.rs @@ -6,6 +6,7 @@ use flecs_ecs::{ mod cached_save; mod lifetime; +pub mod structures; pub use cached_save::cached_save; pub use lifetime::*; diff --git a/crates/hyperion-utils/src/structures.rs b/crates/hyperion-utils/src/structures.rs new file mode 100644 index 00000000..a3490732 --- /dev/null +++ b/crates/hyperion-utils/src/structures.rs @@ -0,0 +1,79 @@ +use valence_protocol::math::DVec3; + +// /!\ Minecraft version dependent +pub enum DamageType { + Arrow, + BadRespawnPoint, + Cactus, + Cramming, + DragonBreath, + DryOut, + Drown, + Explosion, + Fall, + FallingAnvil, + FallingBlock, + FallingStalactite, + Fireball, + Fireworks, + FlyIntoWall, + Freeze, + Generic, + GenericKill, + HotFloor, + InFire, + InWall, + IndirectMagic, + Lava, + LightningBolt, + Magic, + MobAttack, + MobAttackNoAggro, + MobProjectile, + OnFire, + OutOfWorld, + OutsideBorder, + PlayerAttack, + PlayerExplosion, + SonicBoom, + Stalagmite, + Sting, + Starve, + SweetBerryBush, + Thorns, + Thrown, + Trident, + UnattributedFireball, + Wither, + WitherSkull, +} + +pub struct DamageCause { + pub damage_type: DamageType, + pub position: Option, + pub source_entity: i32, + pub direct_source: i32, +} + +impl DamageCause { + #[must_use] + pub const fn new(damage_type: DamageType) -> Self { + Self { + damage_type, + position: Option::None, + source_entity: -1, + direct_source: -1, + } + } + + pub const fn with_position(&mut self, position: DVec3) -> &mut Self { + self.position = Option::Some(position); + self + } + + pub const fn with_entities(&mut self, source: i32, direct_source: i32) -> &mut Self { + self.source_entity = source; + self.direct_source = direct_source; + self + } +} diff --git a/crates/hyperion/src/egress/player_join/mod.rs b/crates/hyperion/src/egress/player_join/mod.rs index 6251dc4e..6f1d30a4 100644 --- a/crates/hyperion/src/egress/player_join/mod.rs +++ b/crates/hyperion/src/egress/player_join/mod.rs @@ -2,15 +2,12 @@ use std::{borrow::Cow, collections::BTreeSet, ops::Index}; use anyhow::Context; use flecs_ecs::prelude::*; -use glam::DVec3; use hyperion_crafting::{Action, CraftingRegistry, RecipeBookState}; use hyperion_utils::EntityExt; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use tracing::{info, instrument}; use valence_protocol::{ - ByteAngle, GameMode, Ident, PacketEncoder, RawBytes, VarInt, Velocity, - game_mode::OptGameMode, - ident, + ByteAngle, Ident, PacketEncoder, RawBytes, VarInt, Velocity, ident, packets::play::{ self, GameJoinS2c, player_position_look_s2c::PlayerPositionLookFlags, @@ -21,7 +18,7 @@ use valence_registry::{BiomeRegistry, RegistryCodec}; use valence_server::entity::EntityKind; use valence_text::IntoText; -use crate::simulation::{MovementTracking, PacketState, Pitch}; +use crate::simulation::{Gamemode, PacketState, Pitch}; mod list; pub use list::*; @@ -46,6 +43,7 @@ use crate::{ reason = "todo: we should refactor at some point" )] #[instrument(skip_all, fields(name = name))] +#[allow(clippy::type_complexity)] pub fn player_join_world( entity: &EntityView<'_>, compose: &Compose, @@ -67,9 +65,11 @@ pub fn player_join_world( &Pitch, &PlayerSkin, &EntityFlags, + &Gamemode, )>, crafting_registry: &CraftingRegistry, config: &Config, + gamemode: &Gamemode, ) -> anyhow::Result<()> { static CACHED_DATA: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -77,16 +77,6 @@ pub fn player_join_world( let id = entity.minecraft_id(); - entity.set(MovementTracking { - received_movement_packets: 0, - last_tick_flying: false, - last_tick_position: **position, - fall_start_y: position.y, - server_velocity: DVec3::ZERO, - sprinting: false, - was_on_ground: false, - }); - let registry_codec = registry_codec_raw(); let codec = RegistryCodec::default(); @@ -111,11 +101,11 @@ pub fn player_join_world( enable_respawn_screen: false, dimension_name: dimension_name.into(), hashed_seed: 0, - game_mode: GameMode::Survival, + game_mode: gamemode.current, is_flat: false, last_death_location: None, portal_cooldown: 60.into(), - previous_game_mode: OptGameMode(Some(GameMode::Survival)), + previous_game_mode: gamemode.previous, dimension_type_name: ident!("minecraft:overworld").into(), is_debug: false, }; @@ -194,7 +184,7 @@ pub fn player_join_world( let _enter = scope.enter(); query .iter_stage(world) - .each(|(uuid, name, _, _, _, _skin, _)| { + .each(|(uuid, name, _, _, _, _skin, _, gamemode)| { // todo: in future, do not clone let entry = PlayerListEntry { @@ -205,7 +195,7 @@ pub fn player_join_world( chat_data: None, listed: true, ping: 20, - game_mode: GameMode::Creative, + game_mode: gamemode.current, display_name: Some(name.to_string().into_cow_text()), }; @@ -240,9 +230,8 @@ pub fn player_join_world( let mut metadata = MetadataChanges::default(); - query - .iter_stage(world) - .each_iter(|it, idx, (uuid, _, position, yaw, pitch, _, flags)| { + query.iter_stage(world).each_iter( + |it, idx, (uuid, _, position, yaw, pitch, _, flags, _)| { let mut result = || { let query_entity = it.entity(idx); @@ -275,7 +264,8 @@ pub fn player_join_world( if let Err(e) = result() { query_errors.push(e); } - }); + }, + ); if !query_errors.is_empty() { return Err(anyhow::anyhow!( @@ -305,7 +295,7 @@ pub fn player_join_world( chat_data: None, listed: true, ping: 20, - game_mode: GameMode::Survival, + game_mode: gamemode.current, display_name: Some(name.to_string().into_cow_text()), }]; @@ -504,6 +494,7 @@ impl Module for PlayerJoinModule { &Pitch, &PlayerSkin, &EntityFlags, + &Gamemode, )>(); let query = SendableQuery(query); @@ -574,8 +565,16 @@ impl Module for PlayerJoinModule { let entity = world.entity_from_id(entity); - entity.get::<(&Uuid, &Name, &Position, &Yaw, &Pitch, &ConnectionId)>( - |(uuid, name, position, yaw, pitch, &stream_id)| { + entity.try_get::<( + &Uuid, + &Name, + &Position, + &Yaw, + &Pitch, + &ConnectionId, + &Gamemode, + )>( + |(uuid, name, position, yaw, pitch, &stream_id, gamemode)| { let query = &query; let query = &query.0; entity.set_name(name); @@ -597,6 +596,7 @@ impl Module for PlayerJoinModule { query, crafting_registry, config, + gamemode, ) { entity.set(PendingRemove::new(e.to_string())); } diff --git a/crates/hyperion/src/simulation/handlers.rs b/crates/hyperion/src/simulation/handlers.rs index 665d94be..d4a8ca14 100644 --- a/crates/hyperion/src/simulation/handlers.rs +++ b/crates/hyperion/src/simulation/handlers.rs @@ -89,7 +89,8 @@ fn change_position_or_correct_client( .set(PendingTeleportation::new(pose.position)); } query - .view + .id + .entity_view(query.world) .get::<(&mut MovementTracking, &Yaw)>(|(tracking, yaw)| { tracking.received_movement_packets += 1; let y_delta = proposed.y - pose.y; @@ -150,8 +151,7 @@ pub fn is_grounded(position: &Vec3, blocks: &Blocks) -> bool { // Check if the block at the calculated position is not air !blocks .get_block(IVec3::new(block_x, block_y, block_z)) - .unwrap() - .is_air() + .is_some_and(|block| block.is_air()) } fn has_block_collision(position: &Vec3, size: EntitySize, blocks: &Blocks) -> bool { @@ -345,14 +345,20 @@ fn client_command( query.size.height = 1.8; } ClientCommand::StartSprinting => { - query.view.get::<&mut MovementTracking>(|tracking| { - tracking.sprinting = true; - }); + query + .id + .entity_view(query.world) + .get::<&mut MovementTracking>(|tracking| { + tracking.sprinting = true; + }); } ClientCommand::StopSprinting => { - query.view.get::<&mut MovementTracking>(|tracking| { - tracking.sprinting = false; - }); + query + .id + .entity_view(query.world) + .get::<&mut MovementTracking>(|tracking| { + tracking.sprinting = false; + }); } ClientCommand::StartJumpWithHorse | ClientCommand::StopJumpWithHorse diff --git a/crates/hyperion/src/simulation/mod.rs b/crates/hyperion/src/simulation/mod.rs index 9eed6fa8..15af7751 100644 --- a/crates/hyperion/src/simulation/mod.rs +++ b/crates/hyperion/src/simulation/mod.rs @@ -14,11 +14,14 @@ use uuid; use valence_generated::block::BlockState; use valence_protocol::{ ByteAngle, VarInt, + game_mode::OptGameMode, packets::play::{ - self, PlayerAbilitiesS2c, player_abilities_s2c::PlayerAbilitiesFlags, + self, PlayerAbilitiesS2c, game_state_change_s2c::GameEventKind, + player_abilities_s2c::PlayerAbilitiesFlags, player_position_look_s2c::PlayerPositionLookFlags, }, }; +use valence_server::GameMode; use crate::{ Global, @@ -610,7 +613,7 @@ impl Default for FlyingSpeed { } } -#[derive(Component, Default, Debug, Copy, Clone)] +#[derive(Component, Debug, Copy, Clone)] pub struct MovementTracking { pub fall_start_y: f32, pub last_tick_flying: bool, @@ -621,6 +624,20 @@ pub struct MovementTracking { pub was_on_ground: bool, } +impl Default for MovementTracking { + fn default() -> Self { + Self { + fall_start_y: -300., + last_tick_flying: false, + last_tick_position: Vec3::ZERO, + received_movement_packets: 0, + server_velocity: DVec3::ZERO, + sprinting: false, + was_on_ground: true, + } + } +} + #[derive(Component, Default, Debug, Copy, Clone)] #[meta] pub struct Flight { @@ -628,6 +645,41 @@ pub struct Flight { pub is_flying: bool, } +#[derive(Component, Default, Copy, Clone, Debug)] +#[meta] +pub struct LastDamaged { + pub tick: i64, + /// The amount of inflicted damages + pub amount: f32, +} + +#[derive(Component, Default, Copy, Clone, Debug)] +#[meta] +/// The Game Mode Component not to confuse with [`GameMode`] from valence +pub struct Gamemode { + pub current: GameMode, + /// Tells the client wich gamemode to select when using the F3+F4 shortcut + pub previous: OptGameMode, +} + +#[derive(Component, Default, Copy, Clone, Debug)] +#[meta] +pub struct BurningState { + pub immune: bool, + pub fire_ticks_left: i32, + /// TODO move to `MovementTracker` as it is also useful to calculate fall damage and velocity + pub in_lava: bool, +} + +impl BurningState { + pub const fn burn_for_seconds(&mut self, seconds: i32) { + let duration = 20 * seconds; + if duration > self.fire_ticks_left { + self.fire_ticks_left = duration; + } + } +} + #[derive(Component)] pub struct SimModule; @@ -665,6 +717,9 @@ impl Module for SimModule { world.component::(); world.component::(); world.component::().meta(); + world.component::().meta(); + world.component::(); + world.component::().meta(); world.component::().meta(); @@ -709,6 +764,9 @@ impl Module for SimModule { world .component::() .add_trait::<(flecs::With, hyperion_inventory::CursorItem)>(); + world + .component::() + .add_trait::<(flecs::With, MovementTracking)>(); observer!( world, @@ -803,6 +861,15 @@ impl Module for SimModule { world .component::() .add_trait::<(flecs::With, FlyingSpeed)>(); + world + .component::() + .add_trait::<(flecs::With, LastDamaged)>(); + world + .component::() + .add_trait::<(flecs::With, Gamemode)>(); + world + .component::() + .add_trait::<(flecs::With, BurningState)>(); observer!( world, @@ -828,36 +895,65 @@ impl Module for SimModule { observer!( world, flecs::OnSet, &FlyingSpeed, - &Compose($), &ConnectionId, &Flight + &Compose($), &ConnectionId, &Flight, &PacketState ) - .each_iter(|it, _, (flying_speed, compose, connection, flight)| { - let system = it.system(); + .each_iter( + |it, _, (flying_speed, compose, connection, flight, state)| { + if !matches!(state, PacketState::Play) { + return; + } + let system = it.system(); - let pkt = PlayerAbilitiesS2c { - flags: PlayerAbilitiesFlags::default() - .with_allow_flying(flight.allow) - .with_flying(flight.is_flying), - flying_speed: flying_speed.speed, - fov_modifier: 0.0, - }; + let pkt = PlayerAbilitiesS2c { + flags: PlayerAbilitiesFlags::default() + .with_allow_flying(flight.allow) + .with_flying(flight.is_flying), + flying_speed: flying_speed.speed, + fov_modifier: 0.0, + }; - compose.unicast(&pkt, *connection, system).unwrap(); - }); + compose.unicast(&pkt, *connection, system).unwrap(); + }, + ); observer!( world, flecs::OnSet, &Flight, - &Compose($), &ConnectionId, &FlyingSpeed + &Compose($), &ConnectionId, &FlyingSpeed, &PacketState + ) + .each_iter( + |it, _, (flight, compose, connection, flying_speed, state)| { + if !matches!(state, PacketState::Play) { + return; + } + let system = it.system(); + + let pkt = play::PlayerAbilitiesS2c { + flags: PlayerAbilitiesFlags::default() + .with_allow_flying(flight.allow) + .with_flying(flight.is_flying), + flying_speed: flying_speed.speed, + fov_modifier: 0., + }; + + compose.unicast(&pkt, *connection, system).unwrap(); + }, + ); + + observer!( + world, + flecs::OnSet, &Gamemode, + &Compose($), &ConnectionId, &PacketState ) - .each_iter(|it, _, (flight, compose, connection, flying_speed)| { + .each_iter(|it, _, (gamemode, compose, connection, state)| { + if !matches!(state, PacketState::Play) { + return; + } let system = it.system(); - let pkt = play::PlayerAbilitiesS2c { - flags: PlayerAbilitiesFlags::default() - .with_allow_flying(flight.allow) - .with_flying(flight.is_flying), - flying_speed: flying_speed.speed, - fov_modifier: 0., + let pkt = play::GameStateChangeS2c { + kind: GameEventKind::ChangeGameMode, + value: f32::from(gamemode.current as i8), }; compose.unicast(&pkt, *connection, system).unwrap(); diff --git a/crates/hyperion-respawn/.gitignore b/crates/vanilla-behaviors/.gitignore similarity index 100% rename from crates/hyperion-respawn/.gitignore rename to crates/vanilla-behaviors/.gitignore diff --git a/crates/vanilla-behaviors/Cargo.toml b/crates/vanilla-behaviors/Cargo.toml new file mode 100644 index 00000000..d0b4eb63 --- /dev/null +++ b/crates/vanilla-behaviors/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "vanilla-behaviors" +version = "0.1.0" +edition = "2021" +authors = ["Andrew Gazelka "] +readme = "README.md" +publish = false + +[dependencies] +hyperion = {workspace = true} +hyperion-clap = { workspace = true } +hyperion-permission = { workspace = true } +hyperion-utils = {workspace = true} +valence_protocol = { workspace = true } +valence_server = { workspace = true } +flecs_ecs = { workspace = true } +fastrand = { workspace = true } +tracing = { workspace = true } +geometry = { workspace = true } +clap = { workspace = true } + +[lints] +workspace = true diff --git a/crates/vanilla-behaviors/README.md b/crates/vanilla-behaviors/README.md new file mode 100644 index 00000000..fa6229c4 --- /dev/null +++ b/crates/vanilla-behaviors/README.md @@ -0,0 +1,3 @@ +# Vanilla Behaviors + +Collection of modules and commands that provide vanilla-like features. \ No newline at end of file diff --git a/crates/vanilla-behaviors/src/command.rs b/crates/vanilla-behaviors/src/command.rs new file mode 100644 index 00000000..78f9e026 --- /dev/null +++ b/crates/vanilla-behaviors/src/command.rs @@ -0,0 +1 @@ +pub mod gamemode; diff --git a/crates/vanilla-behaviors/src/command/gamemode.rs b/crates/vanilla-behaviors/src/command/gamemode.rs new file mode 100644 index 00000000..48b8a969 --- /dev/null +++ b/crates/vanilla-behaviors/src/command/gamemode.rs @@ -0,0 +1,48 @@ +use clap::Parser; +use flecs_ecs::core::{Entity, EntityView, EntityViewGet, WorldGet, WorldProvider}; +use hyperion::{ + net::{agnostic, Compose, ConnectionId, DataBundle}, + simulation::Gamemode, +}; +use hyperion_clap::{CommandPermission, MinecraftCommand}; +use valence_server::GameMode; + +#[derive(Parser, CommandPermission, Debug)] +#[command(name = "gamemode")] +#[command_permission(group = "Moderator")] +pub struct GamemodeCommand { + #[arg(value_enum)] + mode: hyperion_clap::GameMode, +} + +impl MinecraftCommand for GamemodeCommand { + fn execute(self, system: EntityView<'_>, caller: Entity) { + let world = system.world(); + world.get::<&Compose>(|compose| { + caller + .entity_view(world) + .get::<(&mut Gamemode, &ConnectionId)>(|(gamemode, stream)| { + let new_mode = match self.mode { + hyperion_clap::GameMode::Survival => GameMode::Survival, + hyperion_clap::GameMode::Adventure => GameMode::Adventure, + hyperion_clap::GameMode::Spectator => GameMode::Spectator, + hyperion_clap::GameMode::Creative => GameMode::Creative, + }; + + let chat_packet = if new_mode == gamemode.current { + agnostic::chat("ยง4Nothing changed") + } else { + agnostic::chat(format!("Changed gamemode to {new_mode:?}")) + }; + + gamemode.current = new_mode; + caller.entity_view(world).modified::(); + + let mut bundle = DataBundle::new(compose, system); + bundle.add_packet(&chat_packet).unwrap(); + + bundle.unicast(*stream).unwrap(); + }); + }); + } +} diff --git a/crates/vanilla-behaviors/src/lib.rs b/crates/vanilla-behaviors/src/lib.rs new file mode 100644 index 00000000..941d1b47 --- /dev/null +++ b/crates/vanilla-behaviors/src/lib.rs @@ -0,0 +1,69 @@ +use flecs_ecs::core::EntityView; +use hyperion::{ + flecs_ecs::{self, core::EntityViewGet}, + net::Compose, + simulation::{metadata::living_entity::Health, LastDamaged, Player, Position}, +}; +use hyperion_utils::{structures::DamageCause, EntityExt}; +use tracing::warn; +use valence_protocol::{packets::play, VarInt}; +use valence_server::GameMode; + +pub mod command; +pub mod module; + +#[must_use] +pub const fn is_invincible(gamemode: &GameMode) -> bool { + matches!(gamemode, GameMode::Creative | GameMode::Spectator) +} + +pub fn damage_player( + entity: &EntityView<'_>, + amount: f32, + damage_cause: DamageCause, + compose: &Compose, + system: EntityView<'_>, +) -> bool { + if entity.has::() { + entity.get::<(&mut Health, &mut LastDamaged, &Position)>(|(health, last_damaged, pos)| { + if !health.is_dead() { + let mut applied_damages = 0.; + + if compose.global().tick - last_damaged.tick >= 10 { + applied_damages = amount; + last_damaged.tick = compose.global().tick; + } else if compose.global().tick - last_damaged.tick < 10 + && last_damaged.amount < amount + { + applied_damages = amount - last_damaged.amount; + } + + if applied_damages > 0. { + last_damaged.amount = amount; + health.damage(applied_damages); + + let pkt_damage_event = play::EntityDamageS2c { + entity_id: VarInt(entity.minecraft_id()), + source_type_id: VarInt(damage_cause.damage_type as i32), + source_cause_id: VarInt(damage_cause.source_entity + 1), + source_direct_id: VarInt(damage_cause.direct_source + 1), + source_pos: damage_cause.position, + }; + + if compose + .broadcast_local(&pkt_damage_event, pos.to_chunk(), system) + .send() + .is_err() + { + warn!("Failed to brodcast EntityDamageS2c locally!"); + } + return true; + } + } + false + }) + } else { + warn!("Trying to call a Player only function on an non player entity"); + false + } +} diff --git a/crates/vanilla-behaviors/src/module.rs b/crates/vanilla-behaviors/src/module.rs new file mode 100644 index 00000000..e515e748 --- /dev/null +++ b/crates/vanilla-behaviors/src/module.rs @@ -0,0 +1,2 @@ +pub mod natural_damage; +pub mod respawn; diff --git a/crates/vanilla-behaviors/src/module/natural_damage.rs b/crates/vanilla-behaviors/src/module/natural_damage.rs new file mode 100644 index 00000000..e24c85fd --- /dev/null +++ b/crates/vanilla-behaviors/src/module/natural_damage.rs @@ -0,0 +1,249 @@ +use std::ops::ControlFlow; + +use flecs_ecs::{ + core::{EntityViewGet, QueryBuilderImpl, TermBuilderImpl, World}, + macros::{system, Component}, + prelude::{Module, SystemAPI}, +}; +use geometry::aabb::Aabb; +use hyperion::{ + glam::Vec3, + net::{agnostic, Compose}, + simulation::{ + aabb, block_bounds, blocks::Blocks, event::HitGroundEvent, metadata::entity::EntityFlags, + BurningState, EntitySize, Gamemode, MovementTracking, Player, Position, + }, + storage::EventQueue, + BlockKind, +}; +use hyperion_utils::structures::{DamageCause, DamageType}; +use tracing::warn; +use valence_protocol::Sound; +use valence_server::block::{PropName, PropValue}; + +use crate::{damage_player, is_invincible}; + +#[derive(Component)] +pub struct NaturalDamageModule {} + +impl Module for NaturalDamageModule { + fn module(world: &World) { + system!("fall damage", world, &mut EventQueue($), &Compose($)).each_iter( + |it, _, (event_queue, compose)| { + let world = it.world(); + let system = it.system(); + + for event in event_queue.drain() { + if event.fall_distance <= 3. { + continue; + } + + let entity = event.client.entity_view(world); + // TODO account for armor/effects + let damage = event.fall_distance.floor() - 3.; + + if damage <= 0. { + continue; + } + + entity.get::<(&Position, &Gamemode)>(|(position, gamemode)| { + if is_invincible(&gamemode.current) { + return; + } + + damage_player( + &entity, + damage, + DamageCause::new(DamageType::Fall), + compose, + system, + ); + + let sound = agnostic::sound( + if event.fall_distance > 7. { + Sound::EntityPlayerBigFall.to_ident() + } else { + Sound::EntityPlayerSmallFall.to_ident() + }, + **position, + ) + .volume(1.) + .pitch(1.) + .seed(fastrand::i64(..)) + .build(); + + compose + .broadcast_local(&sound, position.to_chunk(), system) + .send() + .unwrap(); + }); + } + }, + ); + + #[allow(clippy::excessive_nesting)] + system!("natural block damage", world, &Compose($), &Blocks($), &Position, &EntitySize, &Gamemode, &MovementTracking, &mut BurningState, &EntityFlags) + .with::() + .each_iter(|it, row, (compose, blocks, position, size, gamemode, movement, burning, flags)| { + if is_invincible(&gamemode.current) { + return; + } + + let system = it.system(); + let entity = it.entity(row); + + let (min, max) = block_bounds(**position, *size); + let min = min.with_y(min.y-1); + let bounding_box = aabb(**position, *size); + let mut in_fire_source = false; + // water, powder snow, bubble column + TODO rain + let mut in_extinguisher = false; + + if burning.fire_ticks_left > 0 { + if burning.immune { + burning.fire_ticks_left = (burning.fire_ticks_left - 4).max(0); + }else { + if !burning.in_lava && burning.fire_ticks_left % 20 == 0 { + damage_player(&entity, 1., DamageCause::new(DamageType::OnFire), compose, system); + } + burning.fire_ticks_left -= 1; + } + } + + burning.in_lava = false; + + blocks.get_blocks(min, max, |pos, block| { + let pos = Vec3::new(pos.x as f32, pos.y as f32, pos.z as f32); + let kind = block.to_kind(); + + if !is_harmful_block(kind) { + return ControlFlow::Continue(()); + } + + for aabb in block.collision_shapes() { + let aabb = Aabb::new(aabb.min().as_vec3(), aabb.max().as_vec3()); + let aabb = aabb.move_by(pos); + + if bounding_box.collides(&aabb) { + match kind { + BlockKind::Cactus => { + damage_player(&entity, 1., DamageCause::new(DamageType::Cactus), compose, system); + } + BlockKind::MagmaBlock => { + if position.y > pos.y && !burning.immune { + damage_player(&entity, 1., DamageCause::new(DamageType::HotFloor), compose, system); + } + } + _ => {} + } + return ControlFlow::Break(()); + } + } + + let aabb = Aabb::new(Vec3::ZERO, Vec3::ONE).move_by(pos); + + if Aabb::overlap(&aabb, &bounding_box).is_some() { + match kind { + BlockKind::Fire => { + in_fire_source = true; + if !burning.immune { + damage_player(&entity, 1., DamageCause::new(DamageType::InFire), compose, system); + burning.fire_ticks_left += 1; + if burning.fire_ticks_left == 0 { + burning.burn_for_seconds(8); + } + } + } + BlockKind::SoulFire => { + in_fire_source = true; + if !burning.immune { + damage_player(&entity, 2., DamageCause::new(DamageType::InFire), compose, system); + burning.fire_ticks_left += 1; + if burning.fire_ticks_left == 0 { + burning.burn_for_seconds(8); + } + } + } + BlockKind::Lava => { + in_fire_source = true; + burning.in_lava = true; + if !burning.immune { + if damage_player(&entity, 4., DamageCause::new(DamageType::Lava), compose, system) { + let sound = agnostic::sound( + Sound::EntityGenericBurn.to_ident(), + **position, + ) + .volume(0.4) + .pitch(2.) // 2.0F + this.random.nextFloat() * 0.4F + .seed(fastrand::i64(..)) + .build(); + + if compose.broadcast_local(&sound, position.to_chunk(), system).send().is_err() { + warn!("Failed to send burn sound to players"); + } + } + burning.burn_for_seconds(15); + } + } + BlockKind::SweetBerryBush => { + let grown = block.get(PropName::Age).is_some_and(|x| x != PropValue::_0); + let delta_x = (f64::from(position.x) - f64::from(movement.last_tick_position.x)).abs(); + let delta_y = (f64::from(position.y) - f64::from(movement.last_tick_position.y)).abs(); + + if grown && (delta_x >= 0.003_000_000_026_077_032 || delta_y >= 0.003_000_000_026_077_032) { + damage_player(&entity, 1., DamageCause::new(DamageType::SweetBerryBush), compose, system); + } + } + BlockKind::Water | BlockKind::BubbleColumn | BlockKind::PowderSnow => { + in_extinguisher = true; + } + _ => {} + } + } + ControlFlow::Continue(()) + }); + + if burning.fire_ticks_left > 0 && in_extinguisher { + if !in_fire_source { + let sound = agnostic::sound( + Sound::EntityGenericExtinguishFire.to_ident(), + **position, + ) + .volume(0.7) + .pitch(1.) + .seed(fastrand::i64(..)) + .build(); + + if compose.broadcast_local(&sound, position.to_chunk(), system).send().is_err() { + warn!("Failed to send extinguish sound to players"); + } + } + burning.fire_ticks_left = -20; // -1 for every entities except players + }else if !in_fire_source && burning.fire_ticks_left <= 0 { + burning.fire_ticks_left = -20; // -1 for every entities except players + } + + if burning.fire_ticks_left > 0 { + entity.set(*flags | EntityFlags::ON_FIRE); + } else { + entity.set(*flags & !EntityFlags::ON_FIRE); + } + }); + } +} + +#[must_use] +pub const fn is_harmful_block(kind: BlockKind) -> bool { + matches!( + kind, + BlockKind::Lava + | BlockKind::Cactus + | BlockKind::MagmaBlock + | BlockKind::SweetBerryBush + | BlockKind::SoulFire + | BlockKind::Fire + | BlockKind::Water + | BlockKind::BubbleColumn + | BlockKind::PowderSnow + ) +} diff --git a/crates/hyperion-respawn/src/lib.rs b/crates/vanilla-behaviors/src/module/respawn.rs similarity index 91% rename from crates/hyperion-respawn/src/lib.rs rename to crates/vanilla-behaviors/src/module/respawn.rs index b3c55dbf..63b50ccb 100644 --- a/crates/hyperion-respawn/src/lib.rs +++ b/crates/vanilla-behaviors/src/module/respawn.rs @@ -7,17 +7,16 @@ use hyperion::{ }, net::{ConnectionId, DataBundle}, protocol::{ - game_mode::OptGameMode, packets::play::{self, PlayerAbilitiesS2c}, BlockPos, ByteAngle, GlobalPos, VarInt, }, - server::{abilities::PlayerAbilitiesFlags, ident, GameMode}, + server::{abilities::PlayerAbilitiesFlags, ident}, simulation::{ event::{ClientStatusCommand, ClientStatusEvent}, handlers::PacketSwitchQuery, metadata::{entity::Pose, living_entity::Health}, packet::HandlerRegistry, - Flight, FlyingSpeed, Pitch, Position, Uuid, Xp, Yaw, + BurningState, Flight, FlyingSpeed, Gamemode, Pitch, Position, Uuid, Xp, Yaw, }, }; use hyperion_utils::{EntityExt, LifetimeHandle}; @@ -49,6 +48,8 @@ impl Module for RespawnModule { &Xp, &Flight, &FlyingSpeed, + &Gamemode, + &mut BurningState, )>( |( connection, @@ -61,8 +62,11 @@ impl Module for RespawnModule { xp, flight, flying_speed, + gamemode, + burning, )| { health.heal(20.); + burning.fire_ticks_left = -20; *pose = Pose::Standing; client.modified::(); // this is so observers detect the change @@ -77,8 +81,8 @@ impl Module for RespawnModule { dimension_type_name: ident!("minecraft:overworld").into(), dimension_name: ident!("minecraft:overworld").into(), hashed_seed: 0, - game_mode: GameMode::Survival, - previous_game_mode: OptGameMode::default(), + game_mode: gamemode.current, + previous_game_mode: gamemode.previous, is_debug: false, is_flat: false, copy_metadata: false, diff --git a/events/tag/Cargo.toml b/events/tag/Cargo.toml index 1463ef23..114ef78d 100644 --- a/events/tag/Cargo.toml +++ b/events/tag/Cargo.toml @@ -26,11 +26,11 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } # tracing-tracy = { workspace = true } uuid = { workspace = true } -hyperion-respawn = { workspace = true } valence_protocol = { workspace = true } valence_server = { workspace = true } serde = { version = "1.0", features = ["derive"] } envy = "0.4" +vanilla-behaviors = { workspace = true } [dev-dependencies] tracing = { workspace = true, features = ["release_max_level_info"] } diff --git a/events/tag/src/command.rs b/events/tag/src/command.rs index a11d353e..8312ac11 100644 --- a/events/tag/src/command.rs +++ b/events/tag/src/command.rs @@ -1,5 +1,6 @@ use flecs_ecs::core::World; use hyperion_clap::{MinecraftCommand, hyperion_command::CommandRegistry}; +use vanilla_behaviors::command::gamemode::GamemodeCommand; use crate::command::{ bow::BowCommand, chest::ChestCommand, class::ClassCommand, fly::FlyCommand, gui::GuiCommand, @@ -33,4 +34,5 @@ pub fn register(registry: &mut CommandRegistry, world: &World) { VanishCommand::register(registry, world); XpCommand::register(registry, world); ChestCommand::register(registry, world); + GamemodeCommand::register(registry, world); } diff --git a/events/tag/src/lib.rs b/events/tag/src/lib.rs index 647420ed..ecb48b57 100644 --- a/events/tag/src/lib.rs +++ b/events/tag/src/lib.rs @@ -9,7 +9,7 @@ use flecs_ecs::prelude::*; use hyperion::{GameServerEndpoint, HyperionCore, simulation::Player}; use hyperion_clap::hyperion_command::CommandRegistry; use hyperion_gui::Gui; -use module::{block::BlockModule, damage::DamageModule, vanish::VanishModule}; +use module::{block::BlockModule, vanish::VanishModule}; mod module; @@ -18,6 +18,7 @@ use hyperion::{glam::IVec3, simulation::Position, spatial}; use hyperion_rank_tree::Team; use module::{attack::AttackModule, level::LevelModule, regeneration::RegenerationModule}; use spatial::SpatialIndex; +use vanilla_behaviors::module::{natural_damage::NaturalDamageModule, respawn::RespawnModule}; use crate::{ module::{bow::BowModule, chat::ChatModule, spawn::SpawnModule, stats::StatsModule}, @@ -72,7 +73,7 @@ impl Module for TagModule { world.import::(); world.import::(); world.import::(); - world.import::(); + world.import::(); world.import::(); world.import::(); world.import::(); @@ -83,7 +84,7 @@ impl Module for TagModule { world.import::(); world.import::(); world.import::(); - world.import::(); + world.import::(); world.get::<&mut CommandRegistry>(|registry| { command::register(registry, world); diff --git a/events/tag/src/module.rs b/events/tag/src/module.rs index 880f99b9..4f940e26 100644 --- a/events/tag/src/module.rs +++ b/events/tag/src/module.rs @@ -2,7 +2,6 @@ pub mod attack; pub mod block; pub mod bow; pub mod chat; -pub mod damage; pub mod level; pub mod regeneration; pub mod spawn; diff --git a/events/tag/src/module/damage.rs b/events/tag/src/module/damage.rs deleted file mode 100644 index 296413a6..00000000 --- a/events/tag/src/module/damage.rs +++ /dev/null @@ -1,75 +0,0 @@ -use flecs_ecs::{ - core::{EntityViewGet, QueryBuilderImpl, TermBuilderImpl, World}, - macros::{Component, system}, - prelude::{Module, SystemAPI}, -}; -use hyperion::{ - net::{Compose, ConnectionId, agnostic}, - simulation::{Position, event::HitGroundEvent, metadata::living_entity::Health}, - storage::EventQueue, -}; -use hyperion_utils::EntityExt; -use valence_protocol::{VarInt, packets::play}; -use valence_server::ident; - -#[derive(Component)] -pub struct DamageModule {} - -impl Module for DamageModule { - fn module(world: &World) { - system!("apply natural damages", world, &mut EventQueue($), &Compose($)) - .each_iter(|it, _, (event_queue, compose)| { - let world = it.world(); - let system = it.system(); - - for event in event_queue.drain() { - if event.fall_distance <= 3. { - continue; - } - - let entity = event.client.entity_view(world); - // TODO account for armor/effects and gamemode - let damage = event.fall_distance.floor() - 3.; - - if damage <= 0. { - continue; - } - - entity.get::<(&mut Health, &ConnectionId, &Position)>( - |(health, connection, position)| { - health.damage(damage); - - let pkt_damage_event = play::EntityDamageS2c { - entity_id: VarInt(entity.minecraft_id()), - source_cause_id: VarInt(0), - source_direct_id: VarInt(0), - source_type_id: VarInt(10), // 10 = fall damage - source_pos: Option::None, - }; - - let sound = agnostic::sound( - if event.fall_distance > 7. { - ident!("minecraft:entity.player.big_fall") - } else { - ident!("minecraft:entity.player.small_fall") - }, - **position, - ) - .volume(1.) - .pitch(1.) - .seed(fastrand::i64(..)) - .build(); - - compose - .unicast(&pkt_damage_event, *connection, system) - .unwrap(); - compose - .broadcast_local(&sound, position.to_chunk(), system) - .send() - .unwrap(); - }, - ); - } - }); - } -} diff --git a/events/tag/src/module/regeneration.rs b/events/tag/src/module/regeneration.rs index c76a11e7..c2b9c908 100644 --- a/events/tag/src/module/regeneration.rs +++ b/events/tag/src/module/regeneration.rs @@ -1,12 +1,12 @@ use flecs_ecs::{ - core::{ComponentOrPairId, QueryBuilderImpl, TermBuilderImpl, World, flecs}, + core::{QueryBuilderImpl, TermBuilderImpl, World}, macros::{Component, system}, prelude::Module, }; use hyperion::{ Prev, net::Compose, - simulation::{Player, metadata::living_entity::Health}, + simulation::{LastDamaged, metadata::living_entity::Health}, util::TracingExt, }; use tracing::info_span; @@ -14,23 +14,11 @@ use tracing::info_span; #[derive(Component)] pub struct RegenerationModule; -#[derive(Component, Default, Copy, Clone, Debug)] -#[meta] -pub struct LastDamaged { - pub tick: i64, -} - const MAX_HEALTH: f32 = 20.0; impl Module for RegenerationModule { #[allow(clippy::excessive_nesting)] fn module(world: &World) { - world.component::().meta(); - - world - .component::() - .add_trait::<(flecs::With, LastDamaged)>(); // todo: how does this even call Default? (IndraDb) - system!( "regenerate", world, diff --git a/events/tag/src/module/spawn.rs b/events/tag/src/module/spawn.rs index 334fa036..82a2d329 100644 --- a/events/tag/src/module/spawn.rs +++ b/events/tag/src/module/spawn.rs @@ -19,7 +19,7 @@ use rustc_hash::FxHashMap; pub struct SpawnModule; const RADIUS: i32 = 0; -const SPAWN_MIN_Y: i16 = 3; +const SPAWN_MIN_Y: i16 = 4; const SPAWN_MAX_Y: i16 = 100; fn position_in_radius() -> IVec2 {