mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5591ac7f | ||
|
|
581de7038e | ||
|
|
8145434642 | ||
|
|
39b2275f71 | ||
|
|
d3c6f0d937 | ||
|
|
baa379d717 | ||
|
|
28a65af5fc | ||
|
|
d22aecd526 | ||
|
|
01b4e5cbc4 | ||
|
|
a91952a8c0 | ||
|
|
558904775f | ||
|
|
62c0834880 | ||
|
|
a30b9525d1 | ||
|
|
f9494ef93e | ||
|
|
ad644778bc | ||
|
|
1944dd45e4 | ||
|
|
b910cdd257 | ||
|
|
3934833369 | ||
|
|
621473f60e | ||
|
|
2b2dc1929f |
No files matched your search
@@ -883,6 +883,24 @@ fn relight_layer(
|
||||
let Some(old) = level(world, sky, p) else {
|
||||
continue;
|
||||
};
|
||||
// `BlockLightEngine.checkNode`: a block emitting at least the light
|
||||
// already there keeps it and pulls its neighbours' light in. Clearing
|
||||
// it would darken and relight everything it lit (flowing lava next to
|
||||
// lava: thousands of cells per block).
|
||||
if !sky && !force && old > 0 {
|
||||
let emission = world
|
||||
.state(p.0, p.1, p.2)
|
||||
.map_or(0, |s| tables.cells[s as usize].emission);
|
||||
if emission >= old {
|
||||
if emission > old {
|
||||
set_level(world, sky, p, emission);
|
||||
changed.push(p);
|
||||
}
|
||||
decrease.push_back((p, 0));
|
||||
increase.push_back(p);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if old > 0 {
|
||||
set_level(world, sky, p, 0);
|
||||
changed.push(p);
|
||||
@@ -1089,6 +1107,49 @@ mod tests {
|
||||
assert!(same(&w, (1, 0)));
|
||||
assert_eq!(block(&w, 14, 0, 8), 13);
|
||||
}
|
||||
/// Emitters placed in light at most as bright as theirs (flowing lava
|
||||
/// next to lava) only brighten: nothing they lit is darkened and lit
|
||||
/// again, and the result is the full relight's.
|
||||
#[test]
|
||||
fn emitters_placed_in_their_own_light_only_brighten() {
|
||||
let mut w = world(|c| {
|
||||
c.set(8, 0, 8, TORCH);
|
||||
});
|
||||
let block = |w: &HashMap<(i32, i32), Arc<Chunk>>, p: Pos| {
|
||||
w[&(p.0 >> 4, p.2 >> 4)].light(p.0 & 15, p.1, p.2 & 15) & 15
|
||||
};
|
||||
for (x, y, z) in [(9, 0, 8), (10, 0, 8), (10, 1, 8), (11, 1, 9), (8, 0, 9)] {
|
||||
let around: Vec<Pos> = (-15..=15)
|
||||
.flat_map(|dx| {
|
||||
(-3..=3).flat_map(move |dy| (-15..=15).map(move |dz| (x + dx, y + dy, z + dz)))
|
||||
})
|
||||
.collect();
|
||||
let before: Vec<u8> = around.iter().map(|&p| block(&w, p)).collect();
|
||||
Arc::make_mut(w.get_mut(&(0, 0)).unwrap()).set(x, y, z, TORCH);
|
||||
let mut changed = LightChanges::default();
|
||||
relight(&mut w, &tables(), true, &[(x, y, z)], false, &mut changed);
|
||||
for (&p, &was) in around.iter().zip(&before) {
|
||||
assert!(block(&w, p) >= was, "{p:?} darkened from {was}");
|
||||
}
|
||||
}
|
||||
// An emitter checked again with its block light unchanged changes
|
||||
// nothing, so no section is remeshed.
|
||||
let mut changed = LightChanges::default();
|
||||
relight(&mut w, &tables(), false, &[(8, 0, 8)], false, &mut changed);
|
||||
assert!(changed.is_empty());
|
||||
let region: [[Option<&Chunk>; 3]; 3] = std::array::from_fn(|dz| {
|
||||
std::array::from_fn(|dx| w.get(&(dx as i32 - 1, dz as i32 - 1)).map(|c| &**c))
|
||||
});
|
||||
for (si, s) in light_chunk(®ion, &tables(), true).iter().enumerate() {
|
||||
for i in 0..4096 {
|
||||
assert_eq!(
|
||||
s.get(i),
|
||||
w[&(0, 0)].sections[si].light(i),
|
||||
"section {si} index {i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn random_border_relights_match_full_relight() {
|
||||
// Edits around every border of chunk (0, 0), at the top and bottom of
|
||||
|
||||
@@ -234,6 +234,8 @@ fn goal_name(m: &Mob, name: &str) -> String {
|
||||
"FreezeWhenLookedAt" => "EndermanFreezeWhenLookedAt",
|
||||
"LookForPlayer" => "EndermanLookForPlayerGoal",
|
||||
"HealRaiders" => "NearestHealableRaiderTargetGoal",
|
||||
"MountPanic" => "MountPanicGoal",
|
||||
"LlamaHurtBy" => "LlamaHurtByTargetGoal",
|
||||
other => return other.to_owned(),
|
||||
};
|
||||
vanilla.to_owned()
|
||||
|
||||
@@ -164,6 +164,8 @@ pub struct ArrowState {
|
||||
/// `AbstractArrow.firedFromWeapon`: the crossbow or bow it was shot from.
|
||||
pub fired_from_weapon: Option<crate::items::ItemStack>,
|
||||
pub no_physics: bool,
|
||||
/// `Arrow.ID_EFFECT_COLOR` as a client has it from its server (-1: none).
|
||||
pub effect_color: i32,
|
||||
/// `AbstractArrow.getPierceLevel`.
|
||||
pub pierce: u8,
|
||||
/// `piercingIgnoreEntityIds`: the entities a piercing arrow has gone
|
||||
@@ -214,6 +216,7 @@ impl ArrowState {
|
||||
pickup_item: crate::items::ItemStack::new("minecraft:arrow", 1),
|
||||
fired_from_weapon: None,
|
||||
no_physics: false,
|
||||
effect_color: -1,
|
||||
pierce: 0,
|
||||
pierced: Vec::new(),
|
||||
fire_ticks: 0,
|
||||
|
||||
@@ -8322,6 +8322,7 @@ impl Game<'_> {
|
||||
self.guest_model_geometry(camera, alpha, &mut out);
|
||||
self.mark_entity(out.len(), "minecraft:evoker_fangs");
|
||||
self.fangs_geometry(camera, alpha, &mut out);
|
||||
self.block_item_display_geometry(camera, alpha, &mut out);
|
||||
let mut see_through = std::mem::take(&mut self.see_through_vertices);
|
||||
see_through.clear();
|
||||
self.name_tag_geometry(camera, alpha, &mut out, &mut see_through);
|
||||
|
||||
@@ -4651,7 +4651,15 @@ impl Game<'_> {
|
||||
let Some(model) = self.arrow_model.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Some(r) = atlas.sprites.get("entity/projectiles/arrow").copied() else {
|
||||
// SpectralArrowRenderer, and TippableArrowRenderer's `getColor() > 0`.
|
||||
let texture = if a.is_spectral_arrow() {
|
||||
"entity/projectiles/arrow_spectral"
|
||||
} else if a.arrow.as_ref().is_some_and(|s| s.effect_color > 0) {
|
||||
"entity/projectiles/arrow_tipped"
|
||||
} else {
|
||||
"entity/projectiles/arrow"
|
||||
};
|
||||
let Some(r) = atlas.sprites.get(texture).copied() else {
|
||||
continue;
|
||||
};
|
||||
let m = glam::Mat4::from_translation(c)
|
||||
|
||||
@@ -174,6 +174,8 @@ pub(super) struct GuestSwap {
|
||||
combat: super::combat::CombatTracker,
|
||||
/// The guest whose turn this one nests in, if any.
|
||||
acting: (Option<u32>, Option<String>, Option<u128>),
|
||||
/// That turn's `known_movement`.
|
||||
known_movement: Vec3,
|
||||
/// The packets queued for that guest.
|
||||
packets: Vec<play::Clientbound>,
|
||||
/// The entity events queued for that guest.
|
||||
@@ -401,6 +403,8 @@ impl Game<'_> {
|
||||
let levitation = std::mem::replace(&mut self.adv.levitation, play.levitation);
|
||||
let marks = std::mem::replace(&mut self.adv.marks, std::mem::take(&mut play.marks));
|
||||
let death_cause = std::mem::replace(&mut self.ui.death_cause, play.death_cause.take());
|
||||
let [x, y, z] = g.known_movement;
|
||||
let known_movement = std::mem::replace(&mut self.known_movement, Vec3::new(x, y, z));
|
||||
let acting = (
|
||||
self.lan_acting.replace(conn),
|
||||
self.lan_acting_name.replace(g.name.clone()),
|
||||
@@ -475,6 +479,7 @@ impl Game<'_> {
|
||||
marks,
|
||||
death_cause,
|
||||
acting,
|
||||
known_movement,
|
||||
packets,
|
||||
entity_events,
|
||||
swings,
|
||||
@@ -537,6 +542,7 @@ impl Game<'_> {
|
||||
death_cause,
|
||||
combat,
|
||||
acting,
|
||||
known_movement,
|
||||
packets,
|
||||
entity_events,
|
||||
swings,
|
||||
@@ -582,6 +588,7 @@ impl Game<'_> {
|
||||
);
|
||||
}
|
||||
(self.lan_acting, self.lan_acting_name, self.lan_acting_uuid) = acting;
|
||||
self.known_movement = known_movement;
|
||||
play.death_cause = std::mem::replace(&mut self.ui.death_cause, death_cause);
|
||||
// An ender chest the guest opened: the menu shows their inventory
|
||||
// itself (lan_open_container), not a stand-in block entity.
|
||||
@@ -1250,14 +1257,7 @@ impl Game<'_> {
|
||||
let hand = if p.hand == 1 { Hand::Off } else { Hand::Main };
|
||||
let g = &mut host.hub.guests[gi];
|
||||
(g.yaw, g.pitch) = (p.yaw, p.pitch);
|
||||
// ServerPlayer.getKnownMovement: the move their client just sent,
|
||||
// which a throw adds to the projectile.
|
||||
let movement = g
|
||||
.moves
|
||||
.last()
|
||||
.map_or(Vec3::ZERO, |m| Vec3::new(m.0[0], m.0[1], m.0[2]));
|
||||
let swung = self.as_guest(host, conn, |g| {
|
||||
g.known_movement = movement;
|
||||
g.guest_use = Some(GuestUse::Item(hand));
|
||||
let swings = g.swings_started;
|
||||
let _ = g.use_item();
|
||||
@@ -1784,14 +1784,8 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
let at = g.position;
|
||||
// ServerPlayer.getKnownMovement: the move their client just sent.
|
||||
let movement = g
|
||||
.moves
|
||||
.last()
|
||||
.map_or(Vec3::ZERO, |m| Vec3::new(m.0[0], m.0[1], m.0[2]));
|
||||
let sound = self
|
||||
.guest_hit_by(host, conn, |g| {
|
||||
g.known_movement = movement;
|
||||
if g.cannot_attack_with_item(g.attack_ticks as i32, 5) {
|
||||
None
|
||||
} else {
|
||||
@@ -2241,13 +2235,7 @@ impl Game<'_> {
|
||||
let using = play.using.is_some();
|
||||
let delayed = play.destroy_delay > 0 || play.use_delay > 0 || play.attack_ticks < 100;
|
||||
if digging.is_some() || using || delayed {
|
||||
// ServerPlayer.getKnownMovement: the move their client just sent.
|
||||
let movement = host.hub.guests[gi]
|
||||
.moves
|
||||
.last()
|
||||
.map_or(Vec3::ZERO, |m| Vec3::new(m.0[0], m.0[1], m.0[2]));
|
||||
self.as_guest(host, conn, |g| {
|
||||
g.known_movement = movement;
|
||||
g.attack_ticks = g.attack_ticks.saturating_add(1);
|
||||
if let Some((pos, face)) = digging {
|
||||
g.forced_hit = Some(Hit {
|
||||
|
||||
@@ -10,7 +10,7 @@ use horizon_client::lan::{
|
||||
playerdata::ints_uuid,
|
||||
};
|
||||
use horizon_protocol::entity_data::{Value, serializer};
|
||||
use horizon_world::nbt::Tag;
|
||||
use horizon_world::{nbt::Tag, registry::BlockStateId};
|
||||
|
||||
fn state(id: u32, kind: &str, position: [f64; 3], yaw: f32, on_ground: bool) -> EntityState {
|
||||
EntityState {
|
||||
@@ -287,18 +287,100 @@ impl Game<'_> {
|
||||
}
|
||||
out.push(e);
|
||||
}
|
||||
// DATA_ITEM, `ItemStack.EMPTY` by default.
|
||||
for o in self.ominous_spawners.iter().filter(|o| o.id != 0) {
|
||||
let mut e = state(
|
||||
o.id,
|
||||
"minecraft:ominous_item_spawner",
|
||||
o.position,
|
||||
0.0,
|
||||
false,
|
||||
);
|
||||
e.meta.push((
|
||||
8,
|
||||
serializer::ITEM_STACK,
|
||||
Value::Item(hub.stack(&Some(o.item.clone()))),
|
||||
));
|
||||
out.push(e);
|
||||
}
|
||||
self.lan_projectile_states(host_entity, hub, out);
|
||||
for o in &self.others {
|
||||
let p = o.position;
|
||||
let mut e = state(o.id, &o.kind, [p.x, p.y, p.z], o.yaw, false);
|
||||
e.pitch = o.pitch;
|
||||
e.head_yaw = o.yaw;
|
||||
if let Some(tag) = self.persist.raw.get(&o.id) {
|
||||
self.lan_display_data(tag, hub, &mut e);
|
||||
}
|
||||
out.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// The synched data of a text, block or item display kept unsimulated,
|
||||
/// from its compound (`Display.readAdditionalSaveData`), with the
|
||||
/// defaults to send when a value goes back to one.
|
||||
fn lan_display_data(&self, tag: &Tag, hub: &mut Hub, e: &mut EntityState) {
|
||||
use horizon_client::text_display::{display_data, text_display_data};
|
||||
let empty = Tag::Compound(Default::default());
|
||||
let (meta, defaults) = match e.kind.as_str() {
|
||||
"minecraft:text_display" => (
|
||||
text_display_data(tag, false),
|
||||
text_display_data(&empty, true),
|
||||
),
|
||||
"minecraft:block_display" => {
|
||||
// DATA_BLOCK_STATE_ID, air by default.
|
||||
let state = |t: Option<BlockStateId>| {
|
||||
(
|
||||
23,
|
||||
serializer::BLOCK_STATE,
|
||||
Value::Id(t.map_or(0, |s| s.0 as i32)),
|
||||
)
|
||||
};
|
||||
let air = self.pack.assets.registry.default_state("minecraft:air");
|
||||
let shown = tag.get("block_state").and_then(|t| self.state_from_nbt(t));
|
||||
(vec![state(shown.or(air))], vec![state(air)])
|
||||
}
|
||||
"minecraft:item_display" => {
|
||||
// DATA_ITEM_STACK and DATA_ITEM_DISPLAY_ID (`ItemDisplayContext`).
|
||||
let item = tag
|
||||
.get("item")
|
||||
.and_then(horizon_client::save::item_from_nbt);
|
||||
let context = match tag.get("item_display") {
|
||||
Some(Tag::String(s)) => match s.to_string_lossy().as_str() {
|
||||
"thirdperson_lefthand" => 1,
|
||||
"thirdperson_righthand" => 2,
|
||||
"firstperson_lefthand" => 3,
|
||||
"firstperson_righthand" => 4,
|
||||
"head" => 5,
|
||||
"gui" => 6,
|
||||
"ground" => 7,
|
||||
"fixed" => 8,
|
||||
"on_shelf" => 9,
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
};
|
||||
(
|
||||
vec![
|
||||
(23, serializer::ITEM_STACK, Value::Item(hub.stack(&item))),
|
||||
(24, serializer::BYTE, Value::Byte(context)),
|
||||
],
|
||||
vec![
|
||||
(23, serializer::ITEM_STACK, Value::Item(None)),
|
||||
(24, serializer::BYTE, Value::Byte(0)),
|
||||
],
|
||||
)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
e.meta = display_data(tag, false);
|
||||
e.meta.extend(meta);
|
||||
e.defaults = display_data(&empty, true);
|
||||
e.defaults.extend(defaults);
|
||||
}
|
||||
|
||||
/// `ClientboundAddEntityPacket(Entity, ServerEntity, int)`: the entity's
|
||||
/// own UUID (`Entity.getUUID`), the one `/data`, saves and UUID
|
||||
/// selectors use, so a client can name it back (the crosshair UUID that
|
||||
@@ -347,6 +429,9 @@ impl Game<'_> {
|
||||
self.rockets.iter_mut().for_each(|r| give(&mut r.id));
|
||||
self.crystals.iter_mut().for_each(|c| give(&mut c.id));
|
||||
self.bolts.iter_mut().for_each(|b| give(&mut b.id));
|
||||
self.ominous_spawners
|
||||
.iter_mut()
|
||||
.for_each(|o| give(&mut o.id));
|
||||
self.potions.thrown_ids().for_each(&mut give);
|
||||
self.end.eye_ids().for_each(&mut give);
|
||||
self.breath_clouds.iter_mut().for_each(|b| give(&mut b.id));
|
||||
@@ -398,7 +483,12 @@ impl Game<'_> {
|
||||
}
|
||||
for p in self.creatures.projectiles.iter().filter(|p| p.id != 0) {
|
||||
use horizon_client::projectiles::ProjectileKind as K;
|
||||
let kind = p.kind.id();
|
||||
// A spectral arrow is its own entity type.
|
||||
let kind = if p.is_spectral_arrow() {
|
||||
"minecraft:spectral_arrow"
|
||||
} else {
|
||||
p.kind.id()
|
||||
};
|
||||
let mut e = state(p.id, kind, at(p.position), p.yaw, false);
|
||||
e.pitch = p.pitch;
|
||||
e.velocity = at(p.velocity);
|
||||
@@ -417,6 +507,11 @@ impl Game<'_> {
|
||||
a.map_or(0, |a| a.pierce),
|
||||
a.is_some_and(|a| a.in_ground) || p.stuck,
|
||||
));
|
||||
// Arrow.ID_EFFECT_COLOR, -1 by default.
|
||||
let color = p.arrow_effect_color();
|
||||
if color != -1 {
|
||||
e.meta.push((11, serializer::INT, Value::Int(color)));
|
||||
}
|
||||
}
|
||||
// ThrownTrident.ID_LOYALTY (none for a mob's) and ID_FOIL, on
|
||||
// AbstractArrow's flags.
|
||||
|
||||
@@ -21,6 +21,8 @@ const TICKS_SCALING: f32 = 50.0;
|
||||
const ROTATION_SPEED: f32 = 40.0;
|
||||
|
||||
pub(super) struct OminousItemSpawner {
|
||||
/// The entity id a server tracks it by (0 until it is tracked).
|
||||
pub(super) id: u32,
|
||||
pub(super) item: ItemStack,
|
||||
pub(super) position: [f64; 3],
|
||||
pub(super) spawn_item_after: i64,
|
||||
@@ -65,6 +67,7 @@ impl Game<'_> {
|
||||
delay: i32,
|
||||
) {
|
||||
self.ominous_spawners.push(OminousItemSpawner {
|
||||
id: 0,
|
||||
item,
|
||||
position,
|
||||
spawn_item_after: i64::from(delay),
|
||||
@@ -82,6 +85,7 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
self.ominous_spawners.push(OminousItemSpawner {
|
||||
id: 0,
|
||||
item,
|
||||
position: [at.x, at.y, at.z],
|
||||
spawn_item_after,
|
||||
@@ -121,6 +125,32 @@ impl Game<'_> {
|
||||
kept.append(&mut self.ominous_spawners);
|
||||
self.ominous_spawners = kept;
|
||||
}
|
||||
/// `handleAddEntity` for a spawner a server sent: its item comes with the
|
||||
/// entity data (`ItemStack.EMPTY` until then, which draws nothing).
|
||||
pub(super) fn add_remote_ominous_item_spawner(&mut self, id: u32, position: [f64; 3]) {
|
||||
self.ominous_spawners.push(OminousItemSpawner {
|
||||
id,
|
||||
item: ItemStack::new("minecraft:air", 1),
|
||||
position,
|
||||
spawn_item_after: 0,
|
||||
tick_count: 0,
|
||||
uuid: [0; 4],
|
||||
raw: None,
|
||||
});
|
||||
}
|
||||
/// `OminousItemSpawner.tick` on a client: `tickCount` and `tickClient`,
|
||||
/// the particles every 5 game ticks; only the server spawns the item.
|
||||
pub(super) fn tick_remote_ominous_item_spawners(&mut self) {
|
||||
let at: Vec<[f64; 3]> = self.ominous_spawners.iter().map(|s| s.position).collect();
|
||||
for s in &mut self.ominous_spawners {
|
||||
s.tick_count += 1;
|
||||
}
|
||||
if self.world.game_time % 5 == 0 {
|
||||
for position in at {
|
||||
self.ominous_spawner_particles(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `spawnItem`: a projectile shot down, or an item entity; level event
|
||||
/// 3021 and the entity_place game event.
|
||||
fn spawn_ominous_item(&mut self, s: &OminousItemSpawner) {
|
||||
|
||||
@@ -631,7 +631,7 @@ impl Game<'_> {
|
||||
*self.persist.uuids.entry(id).or_insert_with(random_uuid)
|
||||
}
|
||||
/// `BlockState.CODEC`: a block name or `{id, properties}` (or legacy `{Name, Properties}`).
|
||||
fn state_from_nbt(&self, tag: &Tag) -> Option<BlockStateId> {
|
||||
pub(super) fn state_from_nbt(&self, tag: &Tag) -> Option<BlockStateId> {
|
||||
let registry = self.pack.assets.registry;
|
||||
let state = match tag.get("Name") {
|
||||
Some(Tag::String(name)) => BlockState {
|
||||
|
||||
@@ -4241,6 +4241,7 @@ impl Game<'_> {
|
||||
fn tick_remote_entities(&mut self, running: bool) {
|
||||
if running {
|
||||
self.tick_text_displays();
|
||||
self.tick_remote_ominous_item_spawners();
|
||||
}
|
||||
// EntityFluidInteraction.update for the entities this window only displays: the
|
||||
// server never sends the fluid contact, and the renderers read it (cod, salmon and
|
||||
|
||||
@@ -35,11 +35,18 @@ impl Game<'_> {
|
||||
m.extra.leash = Some(Holder::Knot(block));
|
||||
}
|
||||
}
|
||||
} else if kind == "minecraft:text_display" {
|
||||
} else if let Some(shows) = match kind {
|
||||
"minecraft:text_display" => Some(super::text_displays::Shows::Text),
|
||||
"minecraft:block_display" => Some(super::text_displays::Shows::Block),
|
||||
"minecraft:item_display" => Some(super::text_displays::Shows::Item),
|
||||
_ => None,
|
||||
} {
|
||||
self.text_displays.insert(
|
||||
id as i32,
|
||||
super::text_displays::RemoteTextDisplay::new(position, yaw),
|
||||
super::text_displays::RemoteTextDisplay::new(shows, position, yaw),
|
||||
);
|
||||
} else if kind == "minecraft:ominous_item_spawner" {
|
||||
self.add_remote_ominous_item_spawner(id, position);
|
||||
} else if let Some(cart) = super::minecarts::CartKind::from_id(kind) {
|
||||
let mut c = super::minecarts::Minecart::new(id, cart, at);
|
||||
c.yaw = yaw;
|
||||
@@ -119,13 +126,33 @@ impl Game<'_> {
|
||||
|
||||
/// `handleSetEntityData` for an object: what changes how it looks.
|
||||
pub(super) fn remote_object_data(&mut self, id: i32, values: &[(u8, i32, Value)]) {
|
||||
if let Some(display) = self.text_displays.get_mut(&id) {
|
||||
display.metadata(values);
|
||||
if self.text_displays.contains_key(&id) {
|
||||
// ItemDisplay.DATA_ITEM_STACK, read with the server's registries.
|
||||
let item = values
|
||||
.iter()
|
||||
.find_map(|(index, _, value)| match (index, value) {
|
||||
(23, Value::Item(stack)) => Some(self.to_stack(stack)),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(display) = self.text_displays.get_mut(&id) {
|
||||
display.show(values, item);
|
||||
display.metadata(values);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let id = id as u32;
|
||||
for (index, _, value) in values {
|
||||
if self.item_frames.iter().any(|f| f.id == id) {
|
||||
if let (8, Value::Item(stack)) = (index, value)
|
||||
&& self.ominous_spawners.iter().any(|s| s.id == id)
|
||||
{
|
||||
// OminousItemSpawner.DATA_ITEM.
|
||||
if let (Some(item), Some(s)) = (
|
||||
self.to_stack(stack),
|
||||
self.ominous_spawners.iter_mut().find(|s| s.id == id),
|
||||
) {
|
||||
s.item = item;
|
||||
}
|
||||
} else if self.item_frames.iter().any(|f| f.id == id) {
|
||||
// HangingEntity.DATA_DIRECTION, ItemFrame.DATA_ITEM and DATA_ROTATION.
|
||||
let item = match (index, value) {
|
||||
(9, Value::Item(stack)) => Some(self.to_stack(stack)),
|
||||
@@ -320,6 +347,7 @@ impl Game<'_> {
|
||||
/// Every object a server sent (a new connection, level or dimension).
|
||||
pub(super) fn clear_remote_objects(&mut self) {
|
||||
self.text_displays.clear();
|
||||
self.ominous_spawners.clear();
|
||||
self.remote_knots.clear();
|
||||
self.carts.clear();
|
||||
self.boats.clear();
|
||||
@@ -331,6 +359,7 @@ impl Game<'_> {
|
||||
|
||||
pub(super) fn remove_remote_object(&mut self, id: u32) {
|
||||
self.text_displays.remove(&(id as i32));
|
||||
self.ominous_spawners.retain(|s| s.id != id);
|
||||
self.remote_knots.remove(&id);
|
||||
self.carts.retain(|c| c.id != id);
|
||||
self.boats.retain(|b| b.id != id);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! and `handleSetEntityData`): the server simulates them, the client draws
|
||||
//! them where it says and interpolates their moves.
|
||||
use super::{FallStep, FallingBlock, Game, fireworks::fireworks_of, view_vector};
|
||||
use horizon_client::items::ItemStack;
|
||||
use horizon_client::projectiles::{Hit, Projectile, ProjectileKind, ProjectileWorld};
|
||||
use horizon_client::{
|
||||
collisions::SceneCollisions,
|
||||
@@ -113,6 +114,10 @@ fn projectile_kind(kind: &str) -> Option<ProjectileKind> {
|
||||
fn remote_arrow_tick(p: &mut Projectile, w: &impl ProjectileWorld) {
|
||||
p.previous = p.position;
|
||||
p.age += 1;
|
||||
// AbstractArrow.tick: `inGroundTime` counts the ticks in a block.
|
||||
if let Some(a) = p.arrow.as_mut() {
|
||||
a.in_ground_time = if p.stuck { a.in_ground_time + 1 } else { 0 };
|
||||
}
|
||||
if p.stuck {
|
||||
return;
|
||||
}
|
||||
@@ -154,6 +159,51 @@ pub(super) fn has_smoke_trail(kind: &ProjectileKind) -> bool {
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// `Arrow.tick` and `SpectralArrow.tick` on a client: a tipped arrow
|
||||
/// sheds two entity-effect swirls in its colour while flying, one every
|
||||
/// fifth tick in a block; a spectral arrow an effect sparkle at its
|
||||
/// position while flying.
|
||||
fn arrow_effect_particles(&mut self) {
|
||||
// The arrow's box: `EntityType.ARROW` is 0.5 x 0.5.
|
||||
const SIZE: f64 = 0.5;
|
||||
for p in &self.creatures.projectiles {
|
||||
let Some(a) = p.arrow.as_ref().filter(|_| p.is_arrow()) else {
|
||||
continue;
|
||||
};
|
||||
let at = p.position;
|
||||
if p.is_spectral_arrow() {
|
||||
if !p.stuck {
|
||||
let options = ParticleOptions::simple(ParticleType::Effect);
|
||||
self.particles.add(&options, [at.x, at.y, at.z], [0.0; 3]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let color = a.effect_color;
|
||||
let count = match (p.stuck, a.in_ground_time % 5) {
|
||||
(true, 0) => 1,
|
||||
(true, _) => 0,
|
||||
(false, _) => 2,
|
||||
};
|
||||
if color == -1 {
|
||||
continue;
|
||||
}
|
||||
// ColorParticleOption.create(ENTITY_EFFECT, colour): ARGB as floats.
|
||||
let channel = |shift: u32| (color as u32 >> shift & 255) as f32 / 255.0;
|
||||
let options = ParticleOptions::colored(
|
||||
ParticleType::EntityEffect,
|
||||
[channel(16), channel(8), channel(0), channel(24)],
|
||||
);
|
||||
for _ in 0..count {
|
||||
// getRandomX(0.5), getRandomY, getRandomZ(0.5).
|
||||
let r = self.particles.random();
|
||||
let x = at.x + SIZE * (2.0 * r.next_double() - 1.0) * 0.5;
|
||||
let y = at.y + SIZE * r.next_double();
|
||||
let z = at.z + SIZE * (2.0 * r.next_double() - 1.0) * 0.5;
|
||||
self.particles.add(&options, [x, y, z], [0.0; 3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `AbstractHurtingProjectile.createParticleTrail`: one smoke puff half a
|
||||
/// block above each position.
|
||||
pub(super) fn smoke_trails(&mut self, at: impl IntoIterator<Item = Vec3>) {
|
||||
@@ -253,6 +303,12 @@ impl Game<'_> {
|
||||
.llama_spit_burst(position, [velocity.x, velocity.y, velocity.z]);
|
||||
}
|
||||
let mut p = Projectile::new(k, at, None);
|
||||
// SpectralArrow: its own type; its pickup item says so.
|
||||
if kind == "minecraft:spectral_arrow"
|
||||
&& let Some(a) = p.arrow.as_mut()
|
||||
{
|
||||
a.pickup_item = ItemStack::new("minecraft:spectral_arrow", 1);
|
||||
}
|
||||
p.id = id;
|
||||
p.velocity = velocity;
|
||||
p.yaw = yaw;
|
||||
@@ -370,6 +426,12 @@ impl Game<'_> {
|
||||
(ProjectileKind::Arrow { .. }, 10, Value::Bool(in_ground)) => {
|
||||
p.stuck = *in_ground;
|
||||
}
|
||||
// Arrow.ID_EFFECT_COLOR.
|
||||
(ProjectileKind::Arrow { .. }, 11, Value::Int(color)) => {
|
||||
if let Some(a) = p.arrow.as_mut() {
|
||||
a.effect_color = *color;
|
||||
}
|
||||
}
|
||||
// WitherSkull.DATA_DANGEROUS.
|
||||
(ProjectileKind::WitherSkull { dangerous }, 8, Value::Bool(d)) => {
|
||||
*dangerous = *d;
|
||||
@@ -471,6 +533,7 @@ impl Game<'_> {
|
||||
}
|
||||
self.creatures.projectiles = projectiles;
|
||||
self.tridents = tridents;
|
||||
self.arrow_effect_particles();
|
||||
let trails: Vec<Vec3> = self
|
||||
.creatures
|
||||
.projectiles
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
//! Server text displays: metadata, interpolation, billboard transforms and world text.
|
||||
use super::Game;
|
||||
use horizon_assets::item_model::DisplayContext;
|
||||
use horizon_client::{
|
||||
gfx::TEXT_POLYGON_OFFSET_OVERLAY,
|
||||
hud::HudBatch,
|
||||
items::ItemStack,
|
||||
mesher::Vertex,
|
||||
text_display::{self, Lines, TextDisplayData},
|
||||
};
|
||||
use horizon_protocol::entity_data::Value;
|
||||
use horizon_world::registry::BlockStateId;
|
||||
|
||||
/// What a display shows besides its transformation: its text (in `data`),
|
||||
/// a block state (`BlockDisplay`) or an item and how it is displayed
|
||||
/// (`ItemDisplay`).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Shows {
|
||||
Text,
|
||||
Block,
|
||||
Item,
|
||||
}
|
||||
|
||||
pub(super) struct RemoteTextDisplay {
|
||||
pub data: TextDisplayData,
|
||||
shows: Shows,
|
||||
/// `BlockDisplay.DATA_BLOCK_STATE_ID`.
|
||||
block: Option<BlockStateId>,
|
||||
/// `ItemDisplay.DATA_ITEM_STACK` and `DATA_ITEM_DISPLAY_ID`.
|
||||
item: Option<ItemStack>,
|
||||
item_display: u8,
|
||||
from: TextDisplayData,
|
||||
shown: TextDisplayData,
|
||||
position: glam::DVec3,
|
||||
@@ -41,9 +60,13 @@ fn renders(id: u8) -> bool {
|
||||
}
|
||||
|
||||
impl RemoteTextDisplay {
|
||||
pub fn new(position: [f64; 3], yaw: f32) -> Self {
|
||||
pub fn new(shows: Shows, position: [f64; 3], yaw: f32) -> Self {
|
||||
Self {
|
||||
data: Default::default(),
|
||||
shows,
|
||||
block: None,
|
||||
item: None,
|
||||
item_display: 0,
|
||||
from: Default::default(),
|
||||
shown: Default::default(),
|
||||
position: position.into(),
|
||||
@@ -71,6 +94,24 @@ impl RemoteTextDisplay {
|
||||
(((self.age - self.start) as f32 + partial) / self.duration as f32).clamp(0.0, 1.0)
|
||||
}
|
||||
}
|
||||
/// The entries after `Display`'s: a block state, or an item with its
|
||||
/// display context (the stack comes converted by the caller).
|
||||
pub fn show(&mut self, values: &[(u8, i32, Value)], item: Option<Option<ItemStack>>) {
|
||||
for (id, _, value) in values {
|
||||
match (self.shows, id, value) {
|
||||
(Shows::Block, 23, Value::Id(state)) => {
|
||||
self.block = Some(BlockStateId(u32::try_from(*state).unwrap_or(0)));
|
||||
}
|
||||
(Shows::Item, 24, Value::Byte(context)) => self.item_display = *context as u8,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(item) = item
|
||||
&& self.shows == Shows::Item
|
||||
{
|
||||
self.item = item;
|
||||
}
|
||||
}
|
||||
pub fn metadata(&mut self, values: &[(u8, i32, Value)]) {
|
||||
self.start_dirty |= values.iter().any(|v| v.0 == 8);
|
||||
self.duration_dirty |= values.iter().any(|v| v.0 == 9);
|
||||
@@ -143,6 +184,96 @@ impl Game<'_> {
|
||||
display.tick();
|
||||
}
|
||||
}
|
||||
/// The camera's yaw and pitch as `DisplayRenderer` reads them (the
|
||||
/// front view turns around).
|
||||
fn display_camera_angles(&self) -> [f32; 2] {
|
||||
if self.camera_type == super::perspective::CameraType::ThirdPersonFront {
|
||||
[self.yaw + 180.0, -self.pitch]
|
||||
} else {
|
||||
[self.yaw, self.pitch]
|
||||
}
|
||||
}
|
||||
/// `DisplayRenderer.getBlockLightLevel` and `getSkyLightLevel`: the
|
||||
/// brightness override, else the light at the entity.
|
||||
fn display_light(
|
||||
world: &horizon_client::world::World,
|
||||
d: &TextDisplayData,
|
||||
position: glam::DVec3,
|
||||
) -> u32 {
|
||||
if d.brightness == -1 {
|
||||
let world_light = world.light(
|
||||
position.x.floor() as i32,
|
||||
position.y.floor() as i32,
|
||||
position.z.floor() as i32,
|
||||
);
|
||||
horizon_client::block_entity_render::vertex_light(world_light)
|
||||
} else {
|
||||
((d.brightness as u32 >> 20 & 15) * 16) | (((d.brightness as u32 >> 4 & 15) * 16) << 8)
|
||||
}
|
||||
}
|
||||
/// `DisplayRenderer.BlockDisplayRenderer` and `ItemDisplayRenderer`: the
|
||||
/// block model or the item under the display's transformation, lit like
|
||||
/// any entity and never tinted by a hurt overlay.
|
||||
pub(super) fn block_item_display_geometry(
|
||||
&mut self,
|
||||
camera: glam::DVec3,
|
||||
partial: f64,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let angles = self.display_camera_angles();
|
||||
let scale = self.view_scale();
|
||||
let mut displays = std::mem::take(&mut self.text_displays);
|
||||
for shows in [Shows::Block, Shows::Item] {
|
||||
let kind = if shows == Shows::Block {
|
||||
"minecraft:block_display"
|
||||
} else {
|
||||
"minecraft:item_display"
|
||||
};
|
||||
self.mark_entity(out.len(), kind);
|
||||
for (id, display) in displays.iter_mut().filter(|(_, d)| d.shows == shows) {
|
||||
display.last_progress = display.progress(partial as f32);
|
||||
let d = display.shown.blend(&display.from, display.last_progress);
|
||||
let position = display.previous.lerp(display.position, partial);
|
||||
let range = f64::from(d.view_range) * 64.0 * scale;
|
||||
if position.distance_squared(camera) >= range * range {
|
||||
continue;
|
||||
}
|
||||
let rotation = std::array::from_fn(|i| {
|
||||
display.old_rotation[i]
|
||||
+ angle(display.rotation[i] - display.old_rotation[i]) * partial as f32
|
||||
});
|
||||
let matrix = glam::Mat4::from_translation((position - camera).as_vec3())
|
||||
* d.matrix(rotation, angles);
|
||||
if !matrix.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let light = Self::display_light(&self.world, &d, position);
|
||||
if let Some(state) = display.block {
|
||||
// BlockModelResolver.update: model seed 42.
|
||||
self.block_model_vertices(state, 42, matrix, light, true, out);
|
||||
} else if let Some(stack) = &display.item {
|
||||
let context = match display.item_display {
|
||||
1 => DisplayContext::ThirdPersonLeftHand,
|
||||
2 => DisplayContext::ThirdPersonRightHand,
|
||||
3 => DisplayContext::FirstPersonLeftHand,
|
||||
4 => DisplayContext::FirstPersonRightHand,
|
||||
5 => DisplayContext::Head,
|
||||
6 => DisplayContext::Gui,
|
||||
7 => DisplayContext::Ground,
|
||||
8 => DisplayContext::Fixed,
|
||||
9 => DisplayContext::OnShelf,
|
||||
_ => DisplayContext::None,
|
||||
};
|
||||
// updateForNonLiving: seeded by the entity id.
|
||||
let state = self.resolve_item(stack, context, None, *id);
|
||||
let pose = matrix * glam::Mat4::from_rotation_y(std::f32::consts::PI);
|
||||
self.submit_item_world(&state, pose, light, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mark_entity(out.len(), "");
|
||||
self.text_displays = displays;
|
||||
}
|
||||
pub(super) fn text_display_geometry(
|
||||
&mut self,
|
||||
camera: glam::DVec3,
|
||||
@@ -153,14 +284,12 @@ impl Game<'_> {
|
||||
self.text_display_vertices = 0;
|
||||
self.mark_translucent_entity(normal.len(), "minecraft:text_display");
|
||||
let atlas = &self.gui.atlas;
|
||||
let front = self.camera_type == super::perspective::CameraType::ThirdPersonFront;
|
||||
let angles = if front {
|
||||
[self.yaw + 180.0, -self.pitch]
|
||||
} else {
|
||||
[self.yaw, self.pitch]
|
||||
};
|
||||
let angles = self.display_camera_angles();
|
||||
let scale = self.view_scale();
|
||||
for display in self.text_displays.values_mut() {
|
||||
if display.shows != Shows::Text {
|
||||
continue;
|
||||
}
|
||||
display.last_progress = display.progress(partial as f32);
|
||||
let d = display.shown.blend(&display.from, display.last_progress);
|
||||
let position = display.previous.lerp(display.position, partial);
|
||||
@@ -189,17 +318,7 @@ impl Game<'_> {
|
||||
lines,
|
||||
self.settings.text_background(0.25),
|
||||
);
|
||||
let world_light = self.world.light(
|
||||
position.x.floor() as i32,
|
||||
position.y.floor() as i32,
|
||||
position.z.floor() as i32,
|
||||
);
|
||||
let light = if d.brightness == -1 {
|
||||
horizon_client::block_entity_render::vertex_light(world_light)
|
||||
} else {
|
||||
((d.brightness as u32 >> 20 & 15) * 16)
|
||||
| (((d.brightness as u32 >> 4 & 15) * 16) << 8)
|
||||
};
|
||||
let light = Self::display_light(&self.world, &d, position);
|
||||
// Font.DisplayMode: SEE_THROUGH over everything, else POLYGON_OFFSET.
|
||||
let through = d.flags & 2 != 0;
|
||||
let (out, layer) = if through {
|
||||
@@ -235,7 +354,7 @@ mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn metadata_and_position_interpolation_keep_their_separate_clocks() {
|
||||
let mut d = RemoteTextDisplay::new([0.0; 3], 0.0);
|
||||
let mut d = RemoteTextDisplay::new(Shows::Text, [0.0; 3], 0.0);
|
||||
d.tick();
|
||||
d.metadata(&[
|
||||
(8, 1, Value::Int(0)),
|
||||
|
||||
@@ -227,6 +227,9 @@ pub struct Guest {
|
||||
pub moves: Vec<([f64; 3], bool)>,
|
||||
/// Whether the guest was on the ground before the first of `moves`.
|
||||
pub moves_from_ground: bool,
|
||||
/// `ServerPlayer.lastKnownClientMovement`: the last move accepted from
|
||||
/// their client, zero after a client tick without one.
|
||||
pub known_movement: [f64; 3],
|
||||
pub dead: bool,
|
||||
/// `LivingEntity.deathTime`: ticks since the guest died.
|
||||
pub death_time: u32,
|
||||
@@ -357,6 +360,7 @@ impl Guest {
|
||||
ticks: 0,
|
||||
moves: Vec::new(),
|
||||
moves_from_ground: false,
|
||||
known_movement: [0.0; 3],
|
||||
dead: false,
|
||||
death_time: 0,
|
||||
placed: false,
|
||||
|
||||
@@ -2189,7 +2189,14 @@ impl Hub {
|
||||
}
|
||||
g.max_unacknowledged = 10;
|
||||
}
|
||||
S::ClientTickEnd(_) => self.guests[i].position_this_tick = false,
|
||||
S::ClientTickEnd(_) => {
|
||||
// handleClientTickEnd: no move this tick, no known movement.
|
||||
let g = &mut self.guests[i];
|
||||
if !g.position_this_tick {
|
||||
g.known_movement = [0.0; 3];
|
||||
}
|
||||
g.position_this_tick = false;
|
||||
}
|
||||
// ServerGamePacketListenerImpl.handleRecipeBookChangeSettingsPacket.
|
||||
S::RecipeBookChangeSettings(p) => {
|
||||
if let Some(book) = crate::recipe_book::BookType::ALL.get(p.book_type as usize)
|
||||
@@ -2381,6 +2388,7 @@ impl Hub {
|
||||
g.moves_from_ground = g.on_ground;
|
||||
}
|
||||
g.moves.push((moved, flags & 1 != 0));
|
||||
g.known_movement = moved;
|
||||
g.yaw = yaw;
|
||||
g.pitch = pitch.clamp(-90.0, 90.0);
|
||||
g.head_yaw = g.yaw;
|
||||
|
||||
@@ -2313,6 +2313,10 @@ impl Mob {
|
||||
{
|
||||
aquatic::spawn_ink(self);
|
||||
}
|
||||
// AbstractHorse.hurtServer: the rear roll after the hurt.
|
||||
if equine::is_equine(self.species()) {
|
||||
equine::after_hurt_server(self);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// A splash or cloud's harming effect on this mob: the potion's
|
||||
|
||||
@@ -507,6 +507,13 @@ pub fn tick(
|
||||
if m.species() == Species::Endermite {
|
||||
endermite_lifetime(m);
|
||||
}
|
||||
// AbstractHorse.tick runs the stand counter down with or without AI.
|
||||
if super::equine::is_equine(m.species())
|
||||
&& !super::camel::is_camel(m.species())
|
||||
&& !super::llama::is_llama(m.species())
|
||||
{
|
||||
super::equine::stand_tick(m);
|
||||
}
|
||||
} else {
|
||||
ai_step(m, w, level_random, events);
|
||||
}
|
||||
|
||||
@@ -304,15 +304,27 @@ impl Goal for RunAroundLikeCrazy {
|
||||
make_mad(m, cx.events);
|
||||
}
|
||||
}
|
||||
/// `AbstractHorse.standIfPossible`: `setStanding(20)` (which also stops the
|
||||
/// eating) when `canPerformRearing`.
|
||||
fn stand_if_possible(m: &mut Mob) {
|
||||
if can_rear(m.species()) {
|
||||
m.extra.counter = 20;
|
||||
m.extra.flag = false;
|
||||
}
|
||||
}
|
||||
/// `AbstractHorse.hurtServer`, after the hurt went through: one hit in three
|
||||
/// rears the horse. Llamas and camels make the draw too, but never rear.
|
||||
pub fn after_hurt_server(m: &mut Mob) {
|
||||
if m.random.next_int_bound(3).unwrap_or(1) == 0 {
|
||||
stand_if_possible(m);
|
||||
}
|
||||
}
|
||||
/// `AbstractHorse.makeMad`: rear with the angry sound (`makeSound`).
|
||||
pub fn make_mad(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
if m.extra.counter > 0 {
|
||||
return;
|
||||
}
|
||||
if can_rear(m.species()) {
|
||||
m.extra.counter = 20;
|
||||
m.extra.flag = false;
|
||||
}
|
||||
stand_if_possible(m);
|
||||
// getAngrySound: null (silent) for the skeleton horse.
|
||||
let sound = match m.species() {
|
||||
Species::SkeletonHorse => return,
|
||||
@@ -663,10 +675,15 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.extra.flag = false;
|
||||
}
|
||||
}
|
||||
stand_tick(m);
|
||||
animate(m);
|
||||
}
|
||||
/// `AbstractHorse.tick`: the stand counter runs down to `clearStanding`. It is
|
||||
/// in `tick`, not `aiStep`, so a horse with no AI stops rearing too.
|
||||
pub fn stand_tick(m: &mut Mob) {
|
||||
if m.extra.counter > 0 {
|
||||
m.extra.counter -= 1;
|
||||
}
|
||||
animate(m);
|
||||
}
|
||||
/// `AbstractHorse.tick`'s eat, stand and mouth animations, which follow the
|
||||
/// eating and standing flags (`flag` and `counter > 0`).
|
||||
@@ -856,10 +873,13 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mobs::Damage;
|
||||
use crate::mobs::finalize::base_value;
|
||||
use crate::mobs::sensor_scan_tests::kind;
|
||||
use horizon_core::geometry::Vec3;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -889,4 +909,50 @@ mod tests {
|
||||
finalize(&mut m);
|
||||
assert_eq!(base_value(&m, Attr::MovementSpeed), m.attrs.speed);
|
||||
}
|
||||
|
||||
fn equine(id: u32, species: Species, name: &str) -> Mob {
|
||||
let mut kind = kind();
|
||||
let k = Arc::make_mut(&mut kind);
|
||||
k.id = name.into();
|
||||
k.species = species;
|
||||
k.max_health = 30.0;
|
||||
Mob::new(id, kind, Vec3::new(f64::from(id), 64.0, 0.0), 0.0)
|
||||
}
|
||||
/// Hits `n` fresh mobs of a species once each; how many reared.
|
||||
fn rears(species: Species, name: &str, n: u32) -> u32 {
|
||||
(1..=n)
|
||||
.filter(|&id| {
|
||||
let mut m = equine(id, species, name);
|
||||
assert!(m.damage(1.0, Damage::player([0.0, 64.0, 0.0])));
|
||||
m.extra.counter == 20
|
||||
})
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hurt_horse_rears_one_hit_in_three() {
|
||||
// AbstractHorse.hurtServer: nextInt(3) == 0 -> standIfPossible (#3432).
|
||||
let n = rears(Species::Horse, "minecraft:horse", 300);
|
||||
assert!((60..=140).contains(&n), "{n} of 300 hits reared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rear_ends_after_twenty_ticks() {
|
||||
// AbstractHorse.tick: standCounter runs down, then clearStanding.
|
||||
let mut m = equine(1, Species::Horse, "minecraft:horse");
|
||||
stand_if_possible(&mut m);
|
||||
for _ in 0..19 {
|
||||
stand_tick(&mut m);
|
||||
}
|
||||
assert!(m.extra.counter > 0);
|
||||
stand_tick(&mut m);
|
||||
assert_eq!(m.extra.counter, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llamas_and_camels_never_rear_when_hurt() {
|
||||
// Llama and Camel: canPerformRearing is false.
|
||||
assert_eq!(rears(Species::Llama, "minecraft:llama", 60), 0);
|
||||
assert_eq!(rears(Species::Camel, "minecraft:camel", 60), 0);
|
||||
}
|
||||
}
|
||||
@@ -269,6 +269,38 @@ impl Projectile {
|
||||
pub fn is_arrow(&self) -> bool {
|
||||
matches!(self.kind, ProjectileKind::Arrow { .. })
|
||||
}
|
||||
/// Whether it is a `SpectralArrow` (its own entity type): a spectral
|
||||
/// arrow's pickup item, or the glowing effect a dispenser shoots with.
|
||||
pub fn is_spectral_arrow(&self) -> bool {
|
||||
let ProjectileKind::Arrow { effect, .. } = &self.kind else {
|
||||
return false;
|
||||
};
|
||||
effect.is_some_and(|(id, _)| id == "minecraft:glowing")
|
||||
|| self
|
||||
.arrow
|
||||
.as_ref()
|
||||
.is_some_and(|a| &*a.pickup_item.item == "minecraft:spectral_arrow")
|
||||
}
|
||||
/// `Arrow.ID_EFFECT_COLOR` (`updateColor`): the opaque colour of the
|
||||
/// potion the arrow carries, from its tipped pickup item or the effect a
|
||||
/// stray, bogged or parched adds; -1 for none.
|
||||
pub fn arrow_effect_color(&self) -> i32 {
|
||||
let ProjectileKind::Arrow { effect, .. } = &self.kind else {
|
||||
return -1;
|
||||
};
|
||||
if self.is_spectral_arrow() {
|
||||
return -1;
|
||||
}
|
||||
let rgb = match (effect, &self.arrow) {
|
||||
(Some((id, _)), _) => crate::brewing::effect(id).map(|e| e.color),
|
||||
(None, Some(a)) if &*a.pickup_item.item == "minecraft:tipped_arrow" => a
|
||||
.pickup_item
|
||||
.potion()
|
||||
.map(|p| crate::brewing::potion_color(Some(p))),
|
||||
_ => None,
|
||||
};
|
||||
rgb.map_or(-1, |rgb| (rgb | 0xFF00_0000) as i32)
|
||||
}
|
||||
pub fn is_fireball(&self) -> bool {
|
||||
matches!(
|
||||
self.kind,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! Display/TextDisplay synced data (26.3), checked against vanilla accessors
|
||||
//! and ClientboundSetEntityDataPacket's own stream codec.
|
||||
use crate::text::Text;
|
||||
use horizon_protocol::entity_data::Value;
|
||||
use crate::{
|
||||
save::{float, number},
|
||||
text::Text,
|
||||
};
|
||||
use horizon_protocol::entity_data::{Value, serializer};
|
||||
use horizon_world::nbt::Tag;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -154,6 +158,197 @@ impl TextDisplayData {
|
||||
}
|
||||
}
|
||||
|
||||
/// One synched data entry: kept when it differs from its default, or always
|
||||
/// with `all` (the defaults a client is sent when a value goes back to one).
|
||||
fn entry(
|
||||
out: &mut Vec<(u8, i32, Value)>,
|
||||
all: bool,
|
||||
id: u8,
|
||||
kind: i32,
|
||||
value: Value,
|
||||
default: Value,
|
||||
) {
|
||||
if all || value != default {
|
||||
out.push((id, kind, if all { default } else { value }));
|
||||
}
|
||||
}
|
||||
|
||||
/// `Display.readAdditionalSaveData`: the synched data entries (ids 8 to 22)
|
||||
/// of a saved display, or with `all` their defaults. A matrix `transformation`
|
||||
/// (`MATRIX4F`) is not decomposed: it stays the identity.
|
||||
pub fn display_data(tag: &Tag, all: bool) -> Vec<(u8, i32, Value)> {
|
||||
let int = |key: &str| number(tag.get(key)).map(|v| v as i32);
|
||||
let real = |key: &str| float(tag.get(key)).map(|v| v as f32);
|
||||
let mut out = Vec::new();
|
||||
let mut set = |id: u8, kind: i32, value: Value, default: Value| {
|
||||
entry(&mut out, all, id, kind, value, default);
|
||||
};
|
||||
let defaults = TextDisplayData::default();
|
||||
// The start delay, the transformation duration, the teleport duration (0 to 59).
|
||||
let ints = [
|
||||
(8, "start_interpolation", 0, i32::MIN, i32::MAX),
|
||||
(9, "interpolation_duration", 0, i32::MIN, i32::MAX),
|
||||
(10, "teleport_duration", 0, 0, 59),
|
||||
(22, "glow_color_override", -1, i32::MIN, i32::MAX),
|
||||
];
|
||||
for (id, key, default, low, high) in ints {
|
||||
let value = int(key).unwrap_or(default).clamp(low, high);
|
||||
set(id, serializer::INT, Value::Int(value), Value::Int(default));
|
||||
}
|
||||
// `Transformation.CODEC`: all four parts, else the identity.
|
||||
let t = tag.get("transformation").and_then(transformation);
|
||||
let (translation, left, scale, right) = t.unwrap_or((
|
||||
defaults.translation,
|
||||
defaults.left_rotation,
|
||||
defaults.scale,
|
||||
defaults.right_rotation,
|
||||
));
|
||||
let parts = [
|
||||
(
|
||||
11,
|
||||
serializer::VECTOR3,
|
||||
translation.to_vec(),
|
||||
defaults.translation.to_vec(),
|
||||
),
|
||||
(
|
||||
12,
|
||||
serializer::VECTOR3,
|
||||
scale.to_vec(),
|
||||
defaults.scale.to_vec(),
|
||||
),
|
||||
(
|
||||
13,
|
||||
serializer::QUATERNION,
|
||||
left.to_vec(),
|
||||
defaults.left_rotation.to_vec(),
|
||||
),
|
||||
(
|
||||
14,
|
||||
serializer::QUATERNION,
|
||||
right.to_vec(),
|
||||
defaults.right_rotation.to_vec(),
|
||||
),
|
||||
];
|
||||
for (id, kind, value, default) in parts {
|
||||
set(id, kind, Value::Floats(value), Value::Floats(default));
|
||||
}
|
||||
let billboard = match tag.get("billboard") {
|
||||
Some(Tag::String(s)) => match s.to_string_lossy().as_str() {
|
||||
"vertical" => 1,
|
||||
"horizontal" => 2,
|
||||
"center" => 3,
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
};
|
||||
set(15, serializer::BYTE, Value::Byte(billboard), Value::Byte(0));
|
||||
// `Brightness.pack`: block light in bits 4 to 7, sky light in bits 20 to 23.
|
||||
let brightness = tag.get("brightness").and_then(|b| {
|
||||
let light = |key: &str| number(b.get(key)).filter(|v| (0..=15).contains(v));
|
||||
Some((light("block")? as i32) << 4 | (light("sky")? as i32) << 20)
|
||||
});
|
||||
set(
|
||||
16,
|
||||
serializer::INT,
|
||||
Value::Int(brightness.unwrap_or(-1)),
|
||||
Value::Int(-1),
|
||||
);
|
||||
for (id, key, default) in [
|
||||
(17, "view_range", 1.0),
|
||||
(18, "shadow_radius", 0.0),
|
||||
(19, "shadow_strength", 1.0),
|
||||
(20, "width", 0.0),
|
||||
(21, "height", 0.0),
|
||||
] {
|
||||
let value = real(key).unwrap_or(default);
|
||||
set(
|
||||
id,
|
||||
serializer::FLOAT,
|
||||
Value::Float(value),
|
||||
Value::Float(default),
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `TextDisplay.readAdditionalSaveData`: the text, line width, background,
|
||||
/// opacity and style flags (ids 23 to 27), or with `all` their defaults.
|
||||
pub fn text_display_data(tag: &Tag, all: bool) -> Vec<(u8, i32, Value)> {
|
||||
let int = |key: &str| number(tag.get(key)).map(|v| v as i32);
|
||||
let mut out = Vec::new();
|
||||
let mut set = |id: u8, kind: i32, value: Value, default: Value| {
|
||||
entry(&mut out, all, id, kind, value, default);
|
||||
};
|
||||
let empty = Tag::String("".into());
|
||||
set(
|
||||
23,
|
||||
serializer::COMPONENT,
|
||||
Value::Text(tag.get("text").unwrap_or(&empty).clone()),
|
||||
Value::Text(empty.clone()),
|
||||
);
|
||||
set(
|
||||
24,
|
||||
serializer::INT,
|
||||
Value::Int(int("line_width").unwrap_or(200)),
|
||||
Value::Int(200),
|
||||
);
|
||||
let background = Value::Int(int("background").unwrap_or(0x4000_0000));
|
||||
set(25, serializer::INT, background, Value::Int(0x4000_0000));
|
||||
let opacity = Value::Byte(int("text_opacity").unwrap_or(-1) as i8);
|
||||
set(26, serializer::BYTE, opacity, Value::Byte(-1));
|
||||
// Shadow 1, see-through 2, default background 4, then the alignment: left 8, right 16.
|
||||
let flag = |key: &str, bit: i8| {
|
||||
if int(key).is_some_and(|v| v != 0) {
|
||||
bit
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
let align = match tag.get("alignment") {
|
||||
Some(Tag::String(s)) => match s.to_string_lossy().as_str() {
|
||||
"left" => 8,
|
||||
"right" => 16,
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
};
|
||||
let flags = flag("shadow", 1) | flag("see_through", 2) | flag("default_background", 4) | align;
|
||||
set(27, serializer::BYTE, Value::Byte(flags), Value::Byte(0));
|
||||
out
|
||||
}
|
||||
|
||||
/// `Transformation.CODEC`'s translation, left rotation, scale and right
|
||||
/// rotation; None unless all four are there (the matrix form is not read).
|
||||
fn transformation(tag: &Tag) -> Option<([f32; 3], [f32; 4], [f32; 3], [f32; 4])> {
|
||||
let list = |tag: Option<&Tag>| -> Option<Vec<f32>> {
|
||||
match tag? {
|
||||
Tag::List(l) => l.iter().map(|v| float(Some(v)).map(|v| v as f32)).collect(),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let vector = |key: &str| -> Option<[f32; 3]> { list(tag.get(key))?.try_into().ok() };
|
||||
// `ExtraCodecs.QUATERNIONF`: four components (normalized) or an axis and an angle.
|
||||
let rotation = |key: &str| -> Option<[f32; 4]> {
|
||||
let value = tag.get(key)?;
|
||||
if let Some(q) = list(Some(value)) {
|
||||
let [x, y, z, w]: [f32; 4] = q.try_into().ok()?;
|
||||
let length = (x * x + y * y + z * z + w * w).sqrt();
|
||||
return Some([x / length, y / length, z / length, w / length]);
|
||||
}
|
||||
let angle = float(value.get("angle"))? as f32;
|
||||
let [x, y, z]: [f32; 3] = list(value.get("axis"))?.try_into().ok()?;
|
||||
let (sin, cos) = (angle * 0.5).sin_cos();
|
||||
let inverse = 1.0 / (x * x + y * y + z * z).sqrt();
|
||||
Some([x * inverse * sin, y * inverse * sin, z * inverse * sin, cos])
|
||||
};
|
||||
Some((
|
||||
vector("translation")?,
|
||||
rotation("left_rotation")?,
|
||||
vector("scale")?,
|
||||
rotation("right_rotation")?,
|
||||
))
|
||||
}
|
||||
|
||||
/// `TextDisplay.CachedInfo`: each line and its width, and the widest.
|
||||
pub struct Lines {
|
||||
pub lines: Vec<(Text, f32)>,
|
||||
|
||||
@@ -310,6 +310,11 @@ def scenarios():
|
||||
('witch', 176), ('vindicator', 177), ('zombie', 178)]:
|
||||
add(f'{species}_deep_water', 160, ['fill -4 -64 -4 4 -61 4 minecraft:water',
|
||||
f'mob minecraft:{species} 0.5 -62 0.5 0 {seed} persistent'])
|
||||
# AbstractHorse.hurtServer (#3432): every hurt draws nextInt(3), and a 0 rears the horse
|
||||
# (standIfPossible). A llama makes the draw too but never rears (canPerformRearing). A horse
|
||||
# scenario waits for #2720: a rearing horse is immobile (AbstractHorse.isImmobile).
|
||||
add('llama_hurt', 200, ['player 4.5 -60 0.5', 'mob minecraft:llama 0.5 -60 0.5 0 182 persistent']
|
||||
+ [f'hurt {t} 0' for t in range(10, 190, 25)])
|
||||
# Axolotls (#64): idle swimming, hunting a cod, and playing dead after hits, in a pool sunk into
|
||||
# the ground so no water flows out of it (the probe doesn't tick fluids).
|
||||
POOL = 'fill -5 -63 -5 5 -61 5 minecraft:water'
|
||||
|
||||
Reference in new issue
Block a user