mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Ominous bottles list their Bad Omen level and duration in the tooltip
OminousBottleAmplifier.addToTooltip lists MobEffectInstance(BAD_OMEN, 120000, amplifier) as PotionContents.addPotionTooltip does: blue (NEUTRAL), "Bad Omen IV (1:40:00)". Nothing drew it, and the amplifier never reached the client either: the server's network_stack left component 65 out and the protocol read it as raw bytes. It now goes over the wire as a VarInt (OminousBottleAmplifier.STREAM_CODEC) and item_sync decodes it. Fixes #3501 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
11494e17f9
commit
7808916266
5 files changed
+175
-2
No files matched your search
@@ -1997,6 +1997,12 @@ impl Game<'_> {
|
||||
{
|
||||
push(&mut lines, text, rgba(85, 85, 255, 255));
|
||||
}
|
||||
// OminousBottleAmplifier.addToTooltip.
|
||||
if let Some((text, c)) =
|
||||
horizon_client::item_tooltip::ominous_bottle_line(stack, &display, &translate)
|
||||
{
|
||||
push(&mut lines, text, c);
|
||||
}
|
||||
// BlockItemStateProperties.addToTooltip.
|
||||
if let Some(text) = horizon_client::item_tooltip::honey_line(stack, &display, &translate) {
|
||||
push(&mut lines, text, gray);
|
||||
|
||||
@@ -465,6 +465,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// An ominous bottle's `ominous_bottle_amplifier` reaches the client
|
||||
/// through the packet's bytes, so its tooltip names the Bad Omen level.
|
||||
#[test]
|
||||
fn an_ominous_bottle_keeps_its_amplifier_across_the_wire() {
|
||||
use horizon_protocol::buf::{Codec, Reader, Writer};
|
||||
use horizon_protocol::item::Slot;
|
||||
let mut components = vec![String::new(); 80];
|
||||
components[component::OMINOUS_BOTTLE_AMPLIFIER as usize] =
|
||||
"minecraft:ominous_bottle_amplifier".into();
|
||||
let items = ["minecraft:air", "minecraft:ominous_bottle"];
|
||||
let builtin = Builtin {
|
||||
registries: [
|
||||
(
|
||||
"minecraft:item".to_owned(),
|
||||
Ids::from_names(items.iter().map(|s| s.to_string()).collect()),
|
||||
),
|
||||
(
|
||||
"minecraft:data_component_type".to_owned(),
|
||||
Ids::from_names(components),
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
};
|
||||
let dynamic = Dynamic {
|
||||
registries: Default::default(),
|
||||
};
|
||||
for amplifier in [None, Some(0), Some(3)] {
|
||||
let mut bottle = ItemStack::new("minecraft:ominous_bottle", 1);
|
||||
if let Some(a) = amplifier {
|
||||
bottle.data_mut().ominous_amplifier = Some(a);
|
||||
}
|
||||
let wire = crate::lan::guest::stack_to_network(
|
||||
|name| items.iter().position(|i| *i == name).unwrap_or(0) as i32,
|
||||
|_, _| None,
|
||||
&Some(bottle),
|
||||
);
|
||||
let mut w = Writer::new();
|
||||
Slot::write(&wire, &mut w);
|
||||
let mut r = Reader::new(&w.data);
|
||||
let read = Slot::read(&mut r).unwrap().unwrap();
|
||||
assert_eq!(r.remaining(), 0);
|
||||
let back = from_network(&read, &builtin, &dynamic);
|
||||
assert_eq!(
|
||||
back.data().and_then(|d| d.ominous_amplifier),
|
||||
amplifier,
|
||||
"{amplifier:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// What a creative client sends (`stack_to_network`) comes back whole on
|
||||
/// the server: damage, both enchantment lists, repair cost, name, trim.
|
||||
#[test]
|
||||
|
||||
@@ -424,6 +424,44 @@ pub fn unbreakable_line(
|
||||
.then(|| translate("item.unbreakable").unwrap_or_else(|| "item.unbreakable".into()))
|
||||
}
|
||||
|
||||
/// `OminousBottleAmplifier.addToTooltip`: the effect the bottle gives,
|
||||
/// `MobEffectInstance(BAD_OMEN, 120000, amplifier)`, listed as
|
||||
/// `PotionContents.addPotionTooltip` lists it, in the colour of Bad Omen's
|
||||
/// category (NEUTRAL, blue). An ominous bottle without the component has the
|
||||
/// item's default amplifier, 0.
|
||||
pub fn ominous_bottle_line(
|
||||
stack: &ItemStack,
|
||||
display: &TooltipDisplay,
|
||||
translate: &dyn Fn(&str) -> Option<String>,
|
||||
) -> Option<(String, u32)> {
|
||||
const BAD_OMEN_TICKS: i32 = 120_000;
|
||||
let amplifier = stack
|
||||
.data()
|
||||
.and_then(|d| d.ominous_amplifier)
|
||||
.or_else(|| (&*stack.item == "minecraft:ominous_bottle").then_some(0))?;
|
||||
if !display.shows("minecraft:ominous_bottle_amplifier") {
|
||||
return None;
|
||||
}
|
||||
use crate::text::{Text, format_translation};
|
||||
let text = |key: &str, fallback: &str| translate(key).unwrap_or_else(|| fallback.to_owned());
|
||||
// PotionContents.getPotionDescription.
|
||||
let mut line = Text::literal(text(
|
||||
"effect.minecraft.bad_omen",
|
||||
"effect.minecraft.bad_omen",
|
||||
));
|
||||
if amplifier > 0 {
|
||||
let potency = translate(&format!("potion.potency.{amplifier}"))
|
||||
.unwrap_or_else(|| crate::enchant::level_name(u32::from(amplifier) + 1));
|
||||
line = format_translation(
|
||||
&text("potion.withAmplifier", "%s %s"),
|
||||
&[line, Text::literal(potency)],
|
||||
);
|
||||
}
|
||||
let duration = Text::literal(crate::brewing::format_duration(BAD_OMEN_TICKS));
|
||||
let line = format_translation(&text("potion.withDuration", "%s (%s)"), &[line, duration]);
|
||||
Some((line.plain(), rgba(85, 85, 255, 255)))
|
||||
}
|
||||
|
||||
/// `PotionContents.addPotionTooltip`'s attribute block: an empty line, "When
|
||||
/// Applied:" and the modifiers of the potion's effects (blue for a gain, red
|
||||
/// for a loss); nothing for a potion whose effects add none.
|
||||
@@ -586,6 +624,7 @@ pub fn detail_lines(
|
||||
.map(|(line, _)| line),
|
||||
);
|
||||
}
|
||||
lines.extend(ominous_bottle_line(stack, &display, translate).map(|l| l.0));
|
||||
lines
|
||||
}
|
||||
|
||||
@@ -820,6 +859,62 @@ mod tests {
|
||||
assert!(!display.shows("minecraft:attribute_modifiers"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ominous_bottle_lists_bad_omen_at_its_amplifier_for_100_minutes() {
|
||||
let lang = |key: &str| {
|
||||
Some(
|
||||
match key {
|
||||
"effect.minecraft.bad_omen" => "Bad Omen",
|
||||
"potion.withAmplifier" => "%s %s",
|
||||
"potion.withDuration" => "%s (%s)",
|
||||
"potion.potency.3" => "IV",
|
||||
_ => return None,
|
||||
}
|
||||
.to_owned(),
|
||||
)
|
||||
};
|
||||
let duration = crate::brewing::format_duration(120_000);
|
||||
let blue = rgba(85, 85, 255, 255);
|
||||
// The item's default amplifier, 0: no potency.
|
||||
let mut bottle = ItemStack::new("minecraft:ominous_bottle", 1);
|
||||
let line = |s: &ItemStack| ominous_bottle_line(s, &TooltipDisplay::for_stack(s), &lang);
|
||||
assert_eq!(
|
||||
line(&bottle),
|
||||
Some((format!("Bad Omen ({duration})"), blue))
|
||||
);
|
||||
bottle.data_mut().ominous_amplifier = Some(3);
|
||||
assert_eq!(
|
||||
line(&bottle),
|
||||
Some((format!("Bad Omen IV ({duration})"), blue))
|
||||
);
|
||||
assert!(
|
||||
lines(&bottle, None, &lang).contains(&format!("Bad Omen IV ({duration})")),
|
||||
"{:?}",
|
||||
lines(&bottle, None, &lang)
|
||||
);
|
||||
// A numbered template, as some languages write it.
|
||||
let numbered = |key: &str| match key {
|
||||
"potion.withDuration" => Some("%2$s: %1$s".to_owned()),
|
||||
other => lang(other),
|
||||
};
|
||||
assert_eq!(
|
||||
ominous_bottle_line(&bottle, &TooltipDisplay::for_stack(&bottle), &numbered)
|
||||
.map(|l| l.0),
|
||||
Some(format!("{duration}: Bad Omen IV"))
|
||||
);
|
||||
bottle.data_mut().set_other(
|
||||
"minecraft:tooltip_display",
|
||||
Some(Tag::compound([(
|
||||
"hidden_components",
|
||||
Tag::List(vec![Tag::String(
|
||||
"minecraft:ominous_bottle_amplifier".into(),
|
||||
)]),
|
||||
)])),
|
||||
);
|
||||
assert_eq!(line(&bottle), None);
|
||||
assert_eq!(line(&ItemStack::new("minecraft:glass_bottle", 1)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ominous_banner_keeps_its_default_hidden_pattern_tooltip() {
|
||||
let mut banner = crate::mobs::raider::ominous_banner();
|
||||
|
||||
@@ -667,6 +667,12 @@ fn network_stack(
|
||||
horizon_protocol::item::Component::Int(i32::from(mode)),
|
||||
));
|
||||
}
|
||||
if let Some(amplifier) = d.ominous_amplifier {
|
||||
stack.added.push((
|
||||
horizon_protocol::item::component::OMINOUS_BOTTLE_AMPLIFIER,
|
||||
horizon_protocol::item::Component::Int(i32::from(amplifier)),
|
||||
));
|
||||
}
|
||||
stack.added.extend(firework_components(d));
|
||||
// The compass's lodestone target.
|
||||
if let Some(t) = d.other("minecraft:lodestone_tracker") {
|
||||
|
||||
@@ -47,6 +47,7 @@ pub mod component {
|
||||
pub const FIREWORK_EXPLOSION: i32 = 70;
|
||||
pub const FIREWORKS: i32 = 71;
|
||||
pub const BLOCK_ENTITY_DATA: i32 = 62;
|
||||
pub const OMINOUS_BOTTLE_AMPLIFIER: i32 = 65;
|
||||
pub const CONTAINER: i32 = 77;
|
||||
}
|
||||
|
||||
@@ -183,8 +184,14 @@ impl Component {
|
||||
pub(crate) fn read(kind: i32, r: &mut Reader) -> Result<Self> {
|
||||
use component::*;
|
||||
Ok(match kind {
|
||||
MAX_STACK_SIZE | MAX_DAMAGE | DAMAGE | RARITY | REPAIR_COST | MAP_ID
|
||||
| MAP_POST_PROCESSING => Self::Int(r.varint()?),
|
||||
MAX_STACK_SIZE
|
||||
| MAX_DAMAGE
|
||||
| DAMAGE
|
||||
| RARITY
|
||||
| REPAIR_COST
|
||||
| MAP_ID
|
||||
| MAP_POST_PROCESSING
|
||||
| OMINOUS_BOTTLE_AMPLIFIER => Self::Int(r.varint()?),
|
||||
WRITABLE_BOOK_CONTENT => {
|
||||
let n = r.count(100)?;
|
||||
Self::WritableBook(
|
||||
@@ -862,6 +869,15 @@ mod tests {
|
||||
book.added
|
||||
.push((component::MAP_POST_PROCESSING, Component::Int(1)));
|
||||
round::<Option<Stack>, Slot>(Some(book));
|
||||
// OminousBottleAmplifier.STREAM_CODEC: a VarInt.
|
||||
let mut bottle = Stack::new(1200, 1);
|
||||
bottle
|
||||
.added
|
||||
.push((component::OMINOUS_BOTTLE_AMPLIFIER, Component::Int(3)));
|
||||
let mut w = Writer::new();
|
||||
Slot::write(&Some(bottle.clone()), &mut w);
|
||||
assert_eq!(w.data, [1, 0xb0, 0x09, 1, 0, 65, 3]);
|
||||
round::<Option<Stack>, Slot>(Some(bottle));
|
||||
let mut quill = Stack::new(1099, 1);
|
||||
quill.added.push((
|
||||
component::WRITABLE_BOOK_CONTENT,
|
||||
|
||||
Reference in new issue
Block a user