mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76e216cd1f | ||
|
|
5779d8d909 | ||
|
|
e9c6751a1b | ||
|
|
1944dd45e4 | ||
|
|
b910cdd257 | ||
|
|
3934833369 | ||
|
|
621473f60e | ||
|
|
2b2dc1929f | ||
|
|
826147d716 | ||
|
|
0b4fa473e7 | ||
|
|
b85ce82160 |
No files matched your search
@@ -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,
|
||||
|
||||
@@ -8166,6 +8166,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);
|
||||
|
||||
@@ -4647,7 +4647,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)
|
||||
|
||||
@@ -458,6 +458,7 @@ impl Game<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn civil_dates_follow_the_gregorian_calendar() {
|
||||
@@ -465,4 +466,27 @@ mod tests {
|
||||
assert_eq!(civil(19_716), (2023, 12, 25));
|
||||
assert_eq!(civil(11_016), (2000, 2, 29));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shield_is_used_for_the_vanilla_s_time() {
|
||||
// Item.getUseDuration: an item with `blocks_attacks` (a shield) is
|
||||
// used for 72000 ticks, so its use animation (the arm held up)
|
||||
// runs instead of ending at once (#2289).
|
||||
let mut shield = Components::new();
|
||||
shield.insert("minecraft:blocks_attacks".into(), json!({}));
|
||||
assert_eq!(use_duration(&shield), 72_000);
|
||||
// Food is used for as long as it takes to eat it.
|
||||
let mut food = Components::new();
|
||||
food.insert(
|
||||
"minecraft:consumable".into(),
|
||||
json!({"consume_seconds": 1.6}),
|
||||
);
|
||||
assert_eq!(use_duration(&food), 32);
|
||||
// The bow keeps its own use time.
|
||||
let mut bow = Components::new();
|
||||
bow.insert("minecraft:item_model".into(), json!("minecraft:bow"));
|
||||
assert_eq!(use_duration(&bow), 72_000);
|
||||
// Anything else is not used at all.
|
||||
assert_eq!(use_duration(&Components::new()), 0);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use horizon_client::lan::{
|
||||
hub::Hub,
|
||||
};
|
||||
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 {
|
||||
@@ -258,18 +258,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);
|
||||
}
|
||||
|
||||
/// Gives the entities Horizon keeps without an id one (`Entity.id`, from
|
||||
/// the same counter as the rest) before they are first tracked.
|
||||
pub(super) fn lan_assign_ids(&mut self) {
|
||||
@@ -308,6 +390,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));
|
||||
@@ -359,7 +444,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);
|
||||
@@ -378,6 +468,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) {
|
||||
|
||||
@@ -620,7 +620,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 {
|
||||
|
||||
@@ -4049,6 +4049,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)),
|
||||
@@ -319,6 +346,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();
|
||||
@@ -330,6 +358,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::{
|
||||
hud::{HudBatch, rgba},
|
||||
items::ItemStack,
|
||||
mesher::Vertex,
|
||||
text::Text,
|
||||
text_display::{Align, 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,
|
||||
@@ -47,9 +66,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(),
|
||||
@@ -77,6 +100,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);
|
||||
@@ -149,6 +190,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,
|
||||
@@ -158,14 +289,12 @@ impl Game<'_> {
|
||||
) {
|
||||
self.text_display_vertices = 0;
|
||||
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);
|
||||
@@ -240,17 +369,7 @@ impl Game<'_> {
|
||||
d.flags & 1 != 0,
|
||||
);
|
||||
}
|
||||
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);
|
||||
let through = d.flags & 2 != 0;
|
||||
let out = if through {
|
||||
&mut *see_through
|
||||
@@ -285,7 +404,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)),
|
||||
|
||||
@@ -893,6 +893,8 @@ pub fn setup(
|
||||
#[cfg(test)]
|
||||
mod flight_pose_tests {
|
||||
use super::*;
|
||||
use horizon_core::geometry::Vec3;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn blocking_arm_follows_the_head_with_limits_in_both_hands() {
|
||||
@@ -1036,4 +1038,62 @@ mod flight_pose_tests {
|
||||
humanoid(&mut dry, &state, &Hum::new(ArmPose::Empty, ArmPose::Empty));
|
||||
assert_eq!(dry.get("head")[3], 30.0f32.to_radians());
|
||||
}
|
||||
|
||||
/// A player: a humanoid model with a shield in its main hand.
|
||||
fn player() -> Mob {
|
||||
let mut kind = crate::mobs::sensor_scan_tests::kind();
|
||||
let k = Arc::make_mut(&mut kind);
|
||||
k.anim = Anim::Humanoid { zombie_arms: false };
|
||||
Mob::new(1, kind, Vec3::ZERO, 0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_raised_shield_holds_the_arm_across_the_body() {
|
||||
// AvatarRenderer.getArmPose: the used item's use animation decides
|
||||
// the arm's pose, and a shield (an item with `blocks_attacks`) uses
|
||||
// the BLOCK animation, so the arm is held up across the body
|
||||
// (#2289).
|
||||
let state = State {
|
||||
pos: 0.0,
|
||||
speed: 0.0,
|
||||
age: 0.0,
|
||||
y_rot: 0.0,
|
||||
x_rot: 0.0,
|
||||
swing: 0.0,
|
||||
baby: false,
|
||||
partial: 0.0,
|
||||
};
|
||||
let mut p = Poses {
|
||||
map: ["head", "right_arm", "left_arm"]
|
||||
.into_iter()
|
||||
.map(|n| (n.to_string(), [0.0; 9]))
|
||||
.collect(),
|
||||
dummy: [0.0; 9],
|
||||
skipped: Default::default(),
|
||||
hidden: Default::default(),
|
||||
};
|
||||
let mut m = player();
|
||||
m.extra.main_hand = Some("minecraft:shield".into());
|
||||
m.extra.using_item = 20;
|
||||
m.extra.blocking = true;
|
||||
setup(&m, &state, &mut p, &HashMap::new());
|
||||
// HumanoidModel.setupAnim's BLOCK pose.
|
||||
assert!((p.get("right_arm")[3] + 0.942_477_9).abs() < 1e-6);
|
||||
assert!((p.get("right_arm")[4] + FRAC_PI_6).abs() < 1e-6);
|
||||
// An item that does not block attacks leaves the arm holding it.
|
||||
let mut m = player();
|
||||
m.extra.main_hand = Some("minecraft:stick".into());
|
||||
m.extra.using_item = 20;
|
||||
let mut p = Poses {
|
||||
map: ["head", "right_arm", "left_arm"]
|
||||
.into_iter()
|
||||
.map(|n| (n.to_string(), [0.0; 9]))
|
||||
.collect(),
|
||||
dummy: [0.0; 9],
|
||||
skipped: Default::default(),
|
||||
hidden: Default::default(),
|
||||
};
|
||||
setup(&m, &state, &mut p, &HashMap::new());
|
||||
assert!((p.get("right_arm")[3] + PI / 10.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -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")?,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user