diff --git a/crates/horizon-client/src/entity_nbt.rs b/crates/horizon-client/src/entity_nbt.rs index 2e1d2dfed..c65f35f5b 100644 --- a/crates/horizon-client/src/entity_nbt.rs +++ b/crates/horizon-client/src/entity_nbt.rs @@ -491,7 +491,36 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag { crate::mobs::villager::write_nbt(m, map); } "minecraft:wandering_trader" => put_offers(map, m), - "minecraft:axolotl" => write_axolotl_memories(m, map), + "minecraft:axolotl" => { + write_axolotl_memories(m, map); + map.insert( + "Variant".into(), + Tag::Int(i32::from(m.extra.variant.min(4))), + ); + } + // Rabbit.addAdditionalSaveData (the legacy id; 99 is the killer bunny). + "minecraft:rabbit" => { + map.insert("RabbitType".into(), Tag::Int(i32::from(m.extra.variant))); + } + "minecraft:parrot" => { + map.insert( + "Variant".into(), + Tag::Int(i32::from(m.extra.variant.min(4))), + ); + } + "minecraft:salmon" => { + let size = SALMON_SIZES[usize::from(m.extra.variant).min(2)]; + map.insert("type".into(), string(size)); + } + // TropicalFish.addAdditionalSaveData: the packed variant. + "minecraft:tropical_fish" => { + let pattern = m.extra.variant.min(11); + let packed = i32::from(pattern >= 6) + | i32::from(pattern % 6) << 8 + | i32::from(m.extra.variant2 & 15) << 16 + | i32::from(m.extra.variant3 & 15) << 24; + map.insert("Variant".into(), Tag::Int(packed)); + } "minecraft:allay" => { // InventoryCarrier: the fetched stack; the liked player memory. let mut inv = Vec::new(); @@ -1092,6 +1121,7 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap>, id: u32) - if m.species() == crate::mobs::Species::Axolotl { restore_axolotl_memories(&mut m, tag); } + read_variant(&mut m, tag); m.fire_ticks = number(tag.get("Fire")).unwrap_or(0) as i32; m.air = number(tag.get("Air")).unwrap_or(300) as i32; if m.species() == crate::mobs::Species::Allay { @@ -2273,6 +2303,41 @@ fn write_axolotl_memories(m: &Mob, map: &mut Compound) { } } } +/// The variant tags `Axolotl`, `Rabbit`, `Parrot`, `Salmon` and +/// `TropicalFish` read; a missing tag is the default variant (a medium +/// salmon, otherwise index 0). +fn read_variant(m: &mut Mob, tag: &Tag) { + use crate::mobs::Species as S; + let int = |key| number(tag.get(key)).unwrap_or(0) as i32; + match m.species() { + S::Axolotl | S::Parrot => m.extra.variant = int("Variant").clamp(0, 4) as u8, + // Rabbit.Variant.LEGACY_CODEC: an unknown id is brown. + S::Rabbit => { + m.extra.variant = match int("RabbitType") { + v @ (0..=5 | 99) => v as u8, + _ => 0, + } + } + S::Salmon => { + m.extra.variant = text(tag.get("type")) + .and_then(|t| SALMON_SIZES.iter().position(|v| *v == t)) + .unwrap_or(1) as u8; + } + S::TropicalFish => { + let packed = int("Variant"); + let (size, index) = (packed & 0xFF, (packed >> 8) & 0xFF); + // TropicalFish.Pattern.byId: an unknown id is a kob. + m.extra.variant = if size <= 1 && index < 6 { + (size * 6 + index) as u8 + } else { + 0 + }; + m.extra.variant2 = ((packed >> 16) & 15) as u8; + m.extra.variant3 = ((packed >> 24) & 15) as u8; + } + _ => {} + } +} fn restore_axolotl_memories(m: &mut Mob, tag: &Tag) { let memories = tag.get("Brain").and_then(|b| b.get("memories")); let ttl = memories diff --git a/crates/horizon-client/src/mobs/render.rs b/crates/horizon-client/src/mobs/render.rs index f34d29ec6..46eb14cc1 100644 --- a/crates/horizon-client/src/mobs/render.rs +++ b/crates/horizon-client/src/mobs/render.rs @@ -177,8 +177,71 @@ pub fn body(m: &Mob) -> (String, String) { "frog#main".into(), format!("entity/frog/frog_{}", climate(m)), ), - S::Rabbit => ( - baby_or(m, "rabbit#main", "rabbit_baby#main"), + // RabbitRenderer.getTextureLocation: the Toast name tag, else the + // variant (the killer bunny's id is 99). + S::Rabbit => { + let name = if m.custom_name.as_deref() == Some("Toast") { + "toast" + } else { + match m.extra.variant { + 1 => "white", + 2 => "black", + 3 => "white_splotched", + 4 => "gold", + 5 => "salt", + 99 => "caerbannog", + _ => "brown", + } + }; + ( + baby_or(m, "rabbit#main", "rabbit_baby#main"), + b(&format!("entity/rabbit/rabbit_{name}")), + ) + } + // ParrotRenderer.getVariantTexture. + S::Parrot => { + let name = ["red_blue", "blue", "green", "yellow_blue", "grey"] + [usize::from(m.extra.variant).min(4)]; + (m.kind.model_key(), format!("entity/parrot/parrot_{name}")) + } + // AxolotlRenderer.getTextureLocation. + S::Axolotl => { + let name = + ["lucy", "wild", "gold", "cyan", "blue"][usize::from(m.extra.variant).min(4)]; + ( + baby_or(m, "axolotl#main", "axolotl_baby#main"), + b(&format!("entity/axolotl/axolotl_{name}")), + ) + } + // SalmonRenderer.submit: the model of the salmon's size. + S::Salmon => { + let model = match m.extra.variant { + 0 => "salmon_small#main", + 2 => "salmon_large#main", + _ => "salmon#main", + }; + (model.into(), "entity/fish/salmon".into()) + } + // The baby models and textures of AgeableMobRenderer's other + // subclasses. + S::Goat => ( + baby_or(m, "goat#main", "goat_baby#main"), + b(&m.kind.texture), + ), + S::PolarBear => ( + baby_or(m, "polar_bear#main", "polar_bear_baby#main"), + b(&m.kind.texture), + ), + S::Ocelot => ( + baby_or(m, "ocelot#main", "ocelot_baby#main"), + b(&m.kind.texture), + ), + S::Turtle => ( + baby_or(m, "turtle#main", "turtle_baby#main"), + b(&m.kind.texture), + ), + S::Armadillo => ( + baby_or(m, "armadillo#main", "armadillo_baby#main"), b(&m.kind.texture), ), S::Pig => { @@ -241,7 +304,7 @@ pub fn body(m: &Mob) -> (String, String) { ), S::ZombieVillager => ( baby_or(m, "zombie_villager#main", "zombie_villager_baby#main"), - "entity/zombie_villager/zombie_villager".into(), + b("entity/zombie_villager/zombie_villager"), ), S::Villager => ( baby_or(m, "villager#main", "villager_baby#main"),