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:
d3grandoandClaude Opus 5.5 committed 2026-10-29 15:32:08 +07:00
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);
+50
View File
@@ -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]
+95
View File
@@ -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();
+6
View File
@@ -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") {
+18 -2
View File
@@ -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,