mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #7685 from levzzz5154/modapi/entity-state
Native mods: an entity's state flags through EntityRef (#7387)
This commit is contained in:
3 files changed
+313
No files matched your search
@@ -242,6 +242,7 @@ mod native_entities;
|
||||
mod native_entity_data;
|
||||
mod native_entity_motion;
|
||||
mod native_entity_refs;
|
||||
mod native_entity_state;
|
||||
mod native_explosions;
|
||||
mod native_falling;
|
||||
mod native_fluid_render;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! An entity's state flags for native server mods, through an `EntityRef`
|
||||
//! (docs/native-mods.md, "Entity and level handles", #7387): sneaking and
|
||||
//! sprinting, water and lava, fire and air, silence, invisibility and pose.
|
||||
//! Players and mobs have them all; the other kinds have the ones Horizon
|
||||
//! keeps for them (an armor stand's fire and invisibility, an item's fire),
|
||||
//! and `None` for the rest, as for a stale handle or on a client.
|
||||
use super::{Game, commands::Target};
|
||||
use horizon_client::mobs::{Mob, Species, sync};
|
||||
use horizon_client::mod_api::EntityRef;
|
||||
use horizon_client::motion::PlayerMotion;
|
||||
use horizon_client::pose::Pose;
|
||||
|
||||
/// `Pose`'s serialized names by id (`Pose.STANDING` is 0).
|
||||
const POSES: [&str; 18] = [
|
||||
"standing",
|
||||
"fall_flying",
|
||||
"sleeping",
|
||||
"swimming",
|
||||
"spin_attack",
|
||||
"crouching",
|
||||
"long_jumping",
|
||||
"dying",
|
||||
"croaking",
|
||||
"using_tongue",
|
||||
"sitting",
|
||||
"roaring",
|
||||
"sniffing",
|
||||
"emerging",
|
||||
"digging",
|
||||
"sliding",
|
||||
"shooting",
|
||||
"inhaling",
|
||||
];
|
||||
|
||||
/// `Entity.getMaxAirSupply`: a dolphin's 4800, an axolotl's 6000, else 300.
|
||||
fn max_air(species: Option<Species>) -> i32 {
|
||||
match species {
|
||||
Some(Species::Dolphin) => 4800,
|
||||
Some(Species::Axolotl) => horizon_client::mobs::axolotl::MAX_AIR,
|
||||
_ => 300,
|
||||
}
|
||||
}
|
||||
|
||||
/// A player's pose as `Pose` names it.
|
||||
fn player_pose(pose: Pose) -> &'static str {
|
||||
match pose {
|
||||
Pose::Standing => "standing",
|
||||
Pose::Crouching => "crouching",
|
||||
Pose::Swimming => "swimming",
|
||||
Pose::FallFlying => "fall_flying",
|
||||
Pose::SpinAttack => "spin_attack",
|
||||
Pose::Sleeping => "sleeping",
|
||||
}
|
||||
}
|
||||
|
||||
/// What a handle names, as the flags read it.
|
||||
enum Body<'a> {
|
||||
Player(&'a PlayerMotion),
|
||||
Mob(&'a Mob),
|
||||
Other(Target),
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// The player's body or the mob a handle names, or its target.
|
||||
fn native_entity_body(&self, entity: EntityRef) -> Option<Body<'_>> {
|
||||
Some(match self.native_entity_target(entity)? {
|
||||
Target::Player => Body::Player(self.player.as_ref()?),
|
||||
Target::Guest(conn) => Body::Player(self.command_guest(conn)?.motion.as_deref()?),
|
||||
Target::Mob(id) => Body::Mob(self.mobs.iter().find(|m| m.id == id)?),
|
||||
t => Body::Other(t),
|
||||
})
|
||||
}
|
||||
/// Whether the player holds the sneak key (`Entity.isShiftKeyDown`);
|
||||
/// false for the other kinds. API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_shift_key_down(&self, entity: EntityRef) -> Option<bool> {
|
||||
Some(match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => p.shift_key_down,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
/// Whether the entity crouches (`Entity.isCrouching`: the crouching
|
||||
/// pose). API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_crouching(&self, entity: EntityRef) -> Option<bool> {
|
||||
Some(self.native_entity_pose(entity)? == "crouching")
|
||||
}
|
||||
/// Whether the player sprints (`Entity.isSprinting`); false for the
|
||||
/// other kinds. API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_sprinting(&self, entity: EntityRef) -> Option<bool> {
|
||||
Some(match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => p.sprinting,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
/// Whether the entity touched water at its last fluid update
|
||||
/// (`Entity.isInWater`): players and mobs. API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_in_water(&self, entity: EntityRef) -> Option<bool> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => Some(p.fluids.was_touching_water),
|
||||
Body::Mob(m) => Some(m.fluids.in_water),
|
||||
Body::Other(_) => None,
|
||||
}
|
||||
}
|
||||
/// Whether the entity is in lava (`Entity.isInLava`): players and mobs.
|
||||
/// API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_in_lava(&self, entity: EntityRef) -> Option<bool> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => Some(p.is_in_lava()),
|
||||
Body::Mob(m) => Some(m.fluids.in_lava),
|
||||
Body::Other(_) => None,
|
||||
}
|
||||
}
|
||||
/// Whether the entity's type never burns (`EntityType.fireImmune`):
|
||||
/// players and mobs. API @API:entity_state@.
|
||||
pub(super) fn native_entity_fire_immune(&self, entity: EntityRef) -> Option<bool> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Player(_) => Some(false),
|
||||
Body::Mob(m) => Some(horizon_client::mobs::spawn_rules::fire_immune(m.species())),
|
||||
Body::Other(_) => None,
|
||||
}
|
||||
}
|
||||
/// The ticks the entity burns for (`Entity.getRemainingFireTicks`):
|
||||
/// players, mobs, armor stands and items. API @API:entity_state@.
|
||||
pub(super) fn native_entity_fire_ticks(&self, entity: EntityRef) -> Option<i32> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => Some(p.fire_ticks),
|
||||
Body::Mob(m) => Some(m.fire_ticks),
|
||||
Body::Other(Target::Stand(id)) => {
|
||||
Some(self.armor_stands.iter().find(|s| s.id == id)?.fire_ticks)
|
||||
}
|
||||
Body::Other(Target::Item(id)) => {
|
||||
Some(self.dropped.iter().find(|i| i.id == id)?.fire_ticks)
|
||||
}
|
||||
Body::Other(_) => None,
|
||||
}
|
||||
}
|
||||
/// The entity's air (`Entity.getAirSupply`): players and mobs. API
|
||||
/// @API:entity_state@.
|
||||
pub(super) fn native_entity_air_supply(&self, entity: EntityRef) -> Option<i32> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => Some(p.air),
|
||||
Body::Mob(m) => Some(m.air),
|
||||
Body::Other(_) => None,
|
||||
}
|
||||
}
|
||||
/// The most air the entity holds (`Entity.getMaxAirSupply`): 300, a
|
||||
/// dolphin's 4800, an axolotl's 6000. API @API:entity_state@.
|
||||
pub(super) fn native_entity_max_air_supply(&self, entity: EntityRef) -> Option<i32> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Mob(m) => Some(max_air(Some(m.species()))),
|
||||
_ => Some(max_air(None)),
|
||||
}
|
||||
}
|
||||
/// Whether the mob makes no sounds (`Entity.isSilent`, its `Silent`
|
||||
/// tag); false for a player. API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_silent(&self, entity: EntityRef) -> Option<bool> {
|
||||
match self.native_entity_body(entity)? {
|
||||
Body::Player(_) => Some(false),
|
||||
Body::Mob(m) => Some(m.silent),
|
||||
Body::Other(_) => None,
|
||||
}
|
||||
}
|
||||
/// Silences the mob or lets it make sounds again (`Entity.setSilent`).
|
||||
/// False for the other kinds. API @API:entity_state@.
|
||||
pub(super) fn native_entity_set_silent(&mut self, entity: EntityRef, silent: bool) -> bool {
|
||||
let Some(Target::Mob(id)) = self.native_entity_target(entity) else {
|
||||
return false;
|
||||
};
|
||||
match self.mobs.iter_mut().find(|m| m.id == id) {
|
||||
Some(m) => {
|
||||
m.silent = silent;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
/// Whether the entity's model is hidden (`Entity.isInvisible`): the
|
||||
/// invisibility effect on a player or a mob, a mob's or an armor stand's
|
||||
/// flag.
|
||||
/// API @API:entity_state@.
|
||||
pub(super) fn native_entity_is_invisible(&self, entity: EntityRef) -> Option<bool> {
|
||||
match self.native_entity_target(entity)? {
|
||||
Target::Player => Some(self.potions.effects.get("invisibility").is_some()),
|
||||
Target::Guest(conn) => Some(
|
||||
self.command_guest(conn)?
|
||||
.effects
|
||||
.get("invisibility")
|
||||
.is_some(),
|
||||
),
|
||||
Target::Mob(id) => {
|
||||
let m = self.mobs.iter().find(|m| m.id == id)?;
|
||||
Some(m.invisible || m.has_effect("minecraft:invisibility"))
|
||||
}
|
||||
Target::Stand(id) => Some(self.armor_stands.iter().find(|s| s.id == id)?.invisible),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The entity's pose (`Entity.getPose`) as `Pose` names it: `standing`,
|
||||
/// `crouching`, `swimming`, `sleeping`, a warden's `roaring`... Kinds
|
||||
/// with no pose stand. API @API:entity_state@.
|
||||
pub(super) fn native_entity_pose(&self, entity: EntityRef) -> Option<&'static str> {
|
||||
Some(match self.native_entity_body(entity)? {
|
||||
Body::Player(p) => player_pose(p.pose),
|
||||
Body::Mob(m) => POSES.get(usize::from(sync::pose_id(m))).copied()?,
|
||||
Body::Other(_) => POSES[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn poses_keep_vanillas_ids() {
|
||||
assert_eq!(POSES[usize::from(sync::SITTING)], "sitting");
|
||||
assert_eq!(POSES[usize::from(sync::INHALING)], "inhaling");
|
||||
for pose in [
|
||||
Pose::Standing,
|
||||
Pose::Crouching,
|
||||
Pose::Swimming,
|
||||
Pose::FallFlying,
|
||||
Pose::SpinAttack,
|
||||
Pose::Sleeping,
|
||||
] {
|
||||
let id = POSES.iter().position(|p| *p == player_pose(pose));
|
||||
assert_eq!(id.map(|i| Pose::from_id(i as i32)), Some(pose));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_air_follows_the_species() {
|
||||
assert_eq!(max_air(None), 300);
|
||||
assert_eq!(max_air(Some(Species::Dolphin)), 4800);
|
||||
assert_eq!(max_air(Some(Species::Axolotl)), 6000);
|
||||
assert_eq!(max_air(Some(Species::Zombie)), 300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{
|
||||
"name": "entity_is_shift_key_down",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_shift_key_down",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_is_crouching",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_crouching",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_is_sprinting",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_sprinting",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_is_in_water",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_in_water",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_is_in_lava",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_in_lava",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_fire_immune",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_fire_immune",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_fire_ticks",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_fire_ticks",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_air_supply",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_air_supply",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_max_air_supply",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_max_air_supply",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_is_silent",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_silent",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_set_silent",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_set_silent",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_is_invisible",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_is_invisible",
|
||||
"receiver": "entity"
|
||||
},
|
||||
{
|
||||
"name": "entity_pose",
|
||||
"target": "horizon_client::game::native_entity_state::Game<'_>::native_entity_pose",
|
||||
"receiver": "entity"
|
||||
}
|
||||
],
|
||||
"rust_summary": "an entity's state flags through an `EntityRef`: sneaking, sprinting, water and lava, fire, air, silence, invisibility and pose.",
|
||||
"doc_summary": "`entity.is_shift_key_down(host)`, `is_crouching`, `is_sprinting`, `is_in_water`, `is_in_lava`, `fire_immune`, `fire_ticks`, `air_supply`, `max_air_supply`, `is_silent`, `set_silent(host, silent)`, `is_invisible` and `pose` (`standing`, `crouching`...). Players and mobs have them all; other kinds return `None` for what Horizon doesn't keep for them. Bundles need \"hooks\": @API:entity_state@.",
|
||||
"comments": [
|
||||
"crates/horizon-client/src/game/native_entity_state.rs"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user