mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #4862 from levzzz5154/parity/K3-hash
Hash block predicates with nbt or partial predicates and books with filtered text
This commit is contained in:
9 files changed
+514
-222
No files matched your search
@@ -3,179 +3,8 @@
|
||||
//! number formatting.
|
||||
use crate::text::{self, Style, Text};
|
||||
use horizon_world::nbt::Tag;
|
||||
|
||||
/// `Double.toString`.
|
||||
pub fn java_double(v: f64) -> String {
|
||||
java_float_text(v, &format!("{v}"), &format!("{v:e}"))
|
||||
}
|
||||
/// `Float.toString`.
|
||||
pub fn java_float(v: f32) -> String {
|
||||
java_float_text(f64::from(v), &format!("{v}"), &format!("{v:e}"))
|
||||
}
|
||||
fn java_float_text(v: f64, plain: &str, sci: &str) -> String {
|
||||
if v.is_nan() {
|
||||
return "NaN".into();
|
||||
}
|
||||
if v.is_infinite() {
|
||||
return if v > 0.0 { "Infinity" } else { "-Infinity" }.into();
|
||||
}
|
||||
if v == 0.0 {
|
||||
return if v.is_sign_negative() { "-0.0" } else { "0.0" }.into();
|
||||
}
|
||||
let a = v.abs();
|
||||
if (1.0e-3..1.0e7).contains(&a) {
|
||||
if plain.contains('.') {
|
||||
plain.to_owned()
|
||||
} else {
|
||||
format!("{plain}.0")
|
||||
}
|
||||
} else {
|
||||
let (mantissa, exponent) = sci.split_once('e').unwrap_or((sci, "0"));
|
||||
let mantissa = if mantissa.contains('.') {
|
||||
mantissa.to_owned()
|
||||
} else {
|
||||
format!("{mantissa}.0")
|
||||
};
|
||||
format!("{mantissa}E{exponent}")
|
||||
}
|
||||
}
|
||||
|
||||
/// `SnbtGrammar.escapeControlCharacters`.
|
||||
fn escape_control(c: char) -> Option<&'static str> {
|
||||
Some(match c {
|
||||
'\u{8}' => "b",
|
||||
'\u{c}' => "f",
|
||||
'\n' => "n",
|
||||
'\r' => "r",
|
||||
'\t' => "t",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// `StringTag.quoteAndEscape`.
|
||||
pub fn quote_and_escape(input: &str) -> String {
|
||||
let mut out = String::from(" ");
|
||||
let mut quote = '\0';
|
||||
for c in input.chars() {
|
||||
if c == '\\' {
|
||||
out.push_str("\\\\");
|
||||
} else if c != '"' && c != '\'' {
|
||||
match escape_control(c) {
|
||||
Some(e) => {
|
||||
out.push('\\');
|
||||
out.push_str(e);
|
||||
}
|
||||
None => out.push(c),
|
||||
}
|
||||
} else {
|
||||
if quote == '\0' {
|
||||
quote = if c == '"' { '\'' } else { '"' };
|
||||
}
|
||||
if quote == c {
|
||||
out.push('\\');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
if quote == '\0' {
|
||||
quote = '"';
|
||||
}
|
||||
out.replace_range(0..1, "e.to_string());
|
||||
out.push(quote);
|
||||
out
|
||||
}
|
||||
fn unquoted_key(key: &str) -> bool {
|
||||
if key.eq_ignore_ascii_case("true") || key.eq_ignore_ascii_case("false") {
|
||||
return false;
|
||||
}
|
||||
let mut chars = key.chars();
|
||||
let first_ok = chars
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '.' || c == '_');
|
||||
// `[A-Za-z._]+[A-Za-z0-9._+-]*`
|
||||
first_ok && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '+' | '-'))
|
||||
}
|
||||
fn string_of(t: &horizon_world::nbt::JavaString) -> String {
|
||||
t.to_string_lossy()
|
||||
}
|
||||
|
||||
/// `Tag.toString` (StringTagVisitor).
|
||||
pub fn to_snbt(tag: &Tag) -> String {
|
||||
let mut out = String::new();
|
||||
visit(tag, &mut out);
|
||||
out
|
||||
}
|
||||
fn visit(tag: &Tag, out: &mut String) {
|
||||
match tag {
|
||||
Tag::End => out.push_str("END"),
|
||||
Tag::Byte(v) => out.push_str(&format!("{v}b")),
|
||||
Tag::Short(v) => out.push_str(&format!("{v}s")),
|
||||
Tag::Int(v) => out.push_str(&v.to_string()),
|
||||
Tag::Long(v) => out.push_str(&format!("{v}L")),
|
||||
Tag::Float(v) => {
|
||||
out.push_str(&java_float(f32::from_bits(*v)));
|
||||
out.push('f');
|
||||
}
|
||||
Tag::Double(v) => {
|
||||
out.push_str(&java_double(f64::from_bits(*v)));
|
||||
out.push('d');
|
||||
}
|
||||
Tag::String(s) => out.push_str("e_and_escape(&string_of(s))),
|
||||
Tag::ByteArray(a) => {
|
||||
out.push_str("[B;");
|
||||
out.push_str(
|
||||
&a.iter()
|
||||
.map(|v| format!("{}B", *v as i8))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
out.push(']');
|
||||
}
|
||||
Tag::IntArray(a) => {
|
||||
out.push_str("[I;");
|
||||
out.push_str(&a.iter().map(i32::to_string).collect::<Vec<_>>().join(","));
|
||||
out.push(']');
|
||||
}
|
||||
Tag::LongArray(a) => {
|
||||
out.push_str("[L;");
|
||||
out.push_str(
|
||||
&a.iter()
|
||||
.map(|v| format!("{v}L"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
out.push(']');
|
||||
}
|
||||
Tag::List(l) => {
|
||||
out.push('[');
|
||||
for (i, t) in l.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
visit(t, out);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Tag::Compound(c) => {
|
||||
out.push('{');
|
||||
let mut entries: Vec<(String, &Tag)> =
|
||||
c.iter().map(|(k, v)| (string_of(k), v)).collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (i, (k, v)) in entries.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
if unquoted_key(k) {
|
||||
out.push_str(k);
|
||||
} else {
|
||||
out.push_str("e_and_escape(k));
|
||||
}
|
||||
out.push(':');
|
||||
visit(v, out);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
use horizon_world::snbt_text::string_of;
|
||||
pub use horizon_world::snbt_text::{java_double, java_float, quote_and_escape, to_snbt};
|
||||
|
||||
// ---- the coloured form -----------------------------------------------------
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ fn components_tag(s: &Stack, builtin: &Builtin, synced: &Dynamic) -> Option<Tag>
|
||||
Component::Text(t) | Component::Nbt(t) => t.clone(),
|
||||
Component::Ident(v) => Tag::String(v.as_str().into()),
|
||||
Component::Lore(lines) => Tag::List(lines.clone()),
|
||||
Component::WritableBook(pages) => Tag::compound([(
|
||||
Component::WritableBook { pages, .. } => Tag::compound([(
|
||||
"pages",
|
||||
Tag::List(
|
||||
pages
|
||||
|
||||
@@ -880,7 +880,10 @@ fn network_stack(
|
||||
if !d.writable_pages.is_empty() {
|
||||
stack.added.push((
|
||||
horizon_protocol::item::component::WRITABLE_BOOK_CONTENT,
|
||||
horizon_protocol::item::Component::WritableBook(d.writable_pages.clone()),
|
||||
horizon_protocol::item::Component::WritableBook {
|
||||
pages: d.writable_pages.clone(),
|
||||
filtered: Vec::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
if let Some(w) = &d.written {
|
||||
@@ -900,6 +903,7 @@ fn network_stack(
|
||||
.collect()
|
||||
},
|
||||
resolved: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
));
|
||||
|
||||
@@ -59,15 +59,20 @@ pub mod component {
|
||||
pub const PAINTING_VARIANT: i32 = 109;
|
||||
}
|
||||
|
||||
/// A written book's content (`WrittenBookContent.STREAM_CODEC`): the
|
||||
/// filtered variants of the title and pages are read and dropped.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
/// A written book's content (`WrittenBookContent.STREAM_CODEC`). The raw title
|
||||
/// and pages are what a book shows; the filtered variants (`Filterable.filtered`,
|
||||
/// which a server sends when chat filtering is on) are kept only so that the
|
||||
/// component hashes as the server holds it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct WrittenBook {
|
||||
pub title: String,
|
||||
pub title_filtered: Option<String>,
|
||||
pub author: String,
|
||||
pub generation: i32,
|
||||
/// Each page's text component.
|
||||
pub pages: Vec<Tag>,
|
||||
/// Each page's filtered variant: empty (none filtered) or one per page.
|
||||
pub pages_filtered: Vec<Option<Tag>>,
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
@@ -102,8 +107,12 @@ pub enum Component {
|
||||
hide: bool,
|
||||
hidden: Vec<i32>,
|
||||
},
|
||||
/// `WritableBookContent`: the pages (raw text).
|
||||
WritableBook(Vec<String>),
|
||||
/// `WritableBookContent`: the pages (raw text) and their filtered variants
|
||||
/// (empty, or one per page).
|
||||
WritableBook {
|
||||
pages: Vec<String>,
|
||||
filtered: Vec<Option<String>>,
|
||||
},
|
||||
WrittenBook(WrittenBook),
|
||||
/// `ChargedProjectiles`: a loaded crossbow's ammunition, as templates.
|
||||
Stacks(Vec<Stack>),
|
||||
@@ -179,14 +188,20 @@ impl FireworkExplosion {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Filterable.streamCodec`: the raw value, then an optional filtered one
|
||||
/// (dropped).
|
||||
fn filterable<T>(r: &mut Reader, value: impl Fn(&mut Reader) -> Result<T>) -> Result<T> {
|
||||
/// `Filterable.streamCodec`: the raw value, then an optional filtered one.
|
||||
fn filterable<T>(
|
||||
r: &mut Reader,
|
||||
value: impl Fn(&mut Reader) -> Result<T>,
|
||||
) -> Result<(T, Option<T>)> {
|
||||
let raw = value(r)?;
|
||||
if r.bool()? {
|
||||
value(r)?;
|
||||
}
|
||||
Ok(raw)
|
||||
let filtered = if r.bool()? { Some(value(r)?) } else { None };
|
||||
Ok((raw, filtered))
|
||||
}
|
||||
/// The filtered variants of a list of pages, or none when no page has one.
|
||||
fn filtered_variants<T>(pages: &mut Vec<(T, Option<T>)>) -> (Vec<T>, Vec<Option<T>>) {
|
||||
let any = pages.iter().any(|(_, f)| f.is_some());
|
||||
let (raw, filtered): (Vec<_>, Vec<_>) = pages.drain(..).unzip();
|
||||
(raw, if any { filtered } else { Vec::new() })
|
||||
}
|
||||
|
||||
/// `ResolvableProfile`: an optional name and UUID and the profile's property
|
||||
@@ -252,25 +267,28 @@ impl Component {
|
||||
| OMINOUS_BOTTLE_AMPLIFIER => Self::Int(r.varint()?),
|
||||
WRITABLE_BOOK_CONTENT => {
|
||||
let n = r.count(100)?;
|
||||
Self::WritableBook(
|
||||
(0..n)
|
||||
.map(|_| filterable(r, |r| r.string(1024)))
|
||||
.collect::<Result<_>>()?,
|
||||
)
|
||||
let mut pages = (0..n)
|
||||
.map(|_| filterable(r, |r| r.string(1024)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let (pages, filtered) = filtered_variants(&mut pages);
|
||||
Self::WritableBook { pages, filtered }
|
||||
}
|
||||
WRITTEN_BOOK_CONTENT => {
|
||||
let title = filterable(r, |r| r.string(32))?;
|
||||
let (title, title_filtered) = filterable(r, |r| r.string(32))?;
|
||||
let author = r.string(32767)?;
|
||||
let generation = r.varint()?;
|
||||
let n = r.count(r.remaining())?;
|
||||
let pages = (0..n)
|
||||
let mut pages = (0..n)
|
||||
.map(|_| filterable(r, |r| r.nbt()))
|
||||
.collect::<Result<_>>()?;
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let (pages, pages_filtered) = filtered_variants(&mut pages);
|
||||
Self::WrittenBook(WrittenBook {
|
||||
title,
|
||||
title_filtered,
|
||||
author,
|
||||
generation,
|
||||
pages,
|
||||
pages_filtered,
|
||||
resolved: r.bool()?,
|
||||
})
|
||||
}
|
||||
@@ -600,22 +618,40 @@ impl Component {
|
||||
}
|
||||
w.bool(*tracked);
|
||||
}
|
||||
Self::WritableBook(pages) => {
|
||||
Self::WritableBook { pages, filtered } => {
|
||||
w.varint(pages.len() as i32);
|
||||
for page in pages {
|
||||
for (i, page) in pages.iter().enumerate() {
|
||||
w.string(page);
|
||||
w.bool(false);
|
||||
match filtered.get(i).and_then(Option::as_ref) {
|
||||
Some(f) => {
|
||||
w.bool(true);
|
||||
w.string(f);
|
||||
}
|
||||
None => w.bool(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::WrittenBook(b) => {
|
||||
w.string(&b.title);
|
||||
w.bool(false);
|
||||
match &b.title_filtered {
|
||||
Some(f) => {
|
||||
w.bool(true);
|
||||
w.string(f);
|
||||
}
|
||||
None => w.bool(false),
|
||||
}
|
||||
w.string(&b.author);
|
||||
w.varint(b.generation);
|
||||
w.varint(b.pages.len() as i32);
|
||||
for page in &b.pages {
|
||||
for (i, page) in b.pages.iter().enumerate() {
|
||||
w.nbt(page);
|
||||
w.bool(false);
|
||||
match b.pages_filtered.get(i).and_then(Option::as_ref) {
|
||||
Some(f) => {
|
||||
w.bool(true);
|
||||
w.nbt(f);
|
||||
}
|
||||
None => w.bool(false),
|
||||
}
|
||||
}
|
||||
w.bool(b.resolved);
|
||||
}
|
||||
@@ -1002,6 +1038,8 @@ mod tests {
|
||||
author: "Guest".into(),
|
||||
generation: 1,
|
||||
pages: vec![literal("one"), literal("two")],
|
||||
pages_filtered: vec![None, Some(literal("2"))],
|
||||
title_filtered: Some("N".into()),
|
||||
resolved: true,
|
||||
}),
|
||||
));
|
||||
@@ -1020,7 +1058,10 @@ mod tests {
|
||||
let mut quill = Stack::new(1099, 1);
|
||||
quill.added.push((
|
||||
component::WRITABLE_BOOK_CONTENT,
|
||||
Component::WritableBook(vec!["a".into(), String::new()]),
|
||||
Component::WritableBook {
|
||||
pages: vec!["a".into(), String::new()],
|
||||
filtered: vec![None, Some("b".into())],
|
||||
},
|
||||
));
|
||||
round::<Option<Stack>, Slot>(Some(quill));
|
||||
assert_eq!(sword.damage(), 17);
|
||||
@@ -1098,13 +1139,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_book_pages_are_read_and_dropped() {
|
||||
fn filtered_book_pages_are_read_and_kept() {
|
||||
// A writable book with one page "hi" and its filtered "**".
|
||||
let bytes = [1, 7, 1, 0, 56, 1, 2, b'h', b'i', 1, 2, b'*', b'*'];
|
||||
let stack = Slot::read(&mut Reader::new(&bytes)).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
stack.component(component::WRITABLE_BOOK_CONTENT),
|
||||
Some(&Component::WritableBook(vec!["hi".into()]))
|
||||
Some(&Component::WritableBook {
|
||||
pages: vec!["hi".into()],
|
||||
filtered: vec![Some("**".into())],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +77,13 @@ fn pick(table: &[&str], id: i32) -> Option<HashCode> {
|
||||
fn text(tag: &horizon_world::nbt::Tag) -> HashCode {
|
||||
HashOps::from_nbt(tag, &TEXT_BOOLS)
|
||||
}
|
||||
/// `Filterable.codec`: the full record, with no filtered variant.
|
||||
fn filterable(raw: HashCode) -> HashCode {
|
||||
RecordHash::new().field("raw", raw).build()
|
||||
/// `Filterable.codec`: the full record (`Codec.withAlternative` encodes with
|
||||
/// the first codec), the filtered variant absent when there is none.
|
||||
fn filterable(raw: HashCode, filtered: Option<HashCode>) -> HashCode {
|
||||
RecordHash::new()
|
||||
.field("raw", raw)
|
||||
.optional("filtered", filtered)
|
||||
.build()
|
||||
}
|
||||
/// `Codec.xxx.optionalFieldOf(name, default)`: absent when equal to the default.
|
||||
fn or_default<T: PartialEq>(
|
||||
@@ -283,18 +287,26 @@ impl Component {
|
||||
}
|
||||
.build()
|
||||
}
|
||||
Self::WritableBook(pages) => if pages.is_empty() {
|
||||
Self::WritableBook { pages, filtered } => if pages.is_empty() {
|
||||
RecordHash::new()
|
||||
} else {
|
||||
RecordHash::new().field(
|
||||
"pages",
|
||||
HashOps::create_list(pages.iter().map(|p| filterable(string(p)))),
|
||||
HashOps::create_list(pages.iter().enumerate().map(|(i, p)| {
|
||||
filterable(
|
||||
string(p),
|
||||
filtered.get(i).and_then(Option::as_ref).map(|f| string(f)),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}
|
||||
.build(),
|
||||
Self::WrittenBook(b) => {
|
||||
let rec = RecordHash::new()
|
||||
.field("title", filterable(string(&b.title)))
|
||||
.field(
|
||||
"title",
|
||||
filterable(string(&b.title), b.title_filtered.as_deref().map(string)),
|
||||
)
|
||||
.field("author", string(&b.author));
|
||||
let rec = or_default(
|
||||
rec,
|
||||
@@ -308,7 +320,12 @@ impl Component {
|
||||
} else {
|
||||
rec.field(
|
||||
"pages",
|
||||
HashOps::create_list(b.pages.iter().map(|p| filterable(text(p)))),
|
||||
HashOps::create_list(b.pages.iter().enumerate().map(|(i, p)| {
|
||||
filterable(
|
||||
text(p),
|
||||
b.pages_filtered.get(i).and_then(Option::as_ref).map(text),
|
||||
)
|
||||
})),
|
||||
)
|
||||
};
|
||||
or_default(
|
||||
@@ -727,7 +744,10 @@ mod tests {
|
||||
(
|
||||
"writable",
|
||||
k::WRITABLE_BOOK_CONTENT,
|
||||
C::WritableBook(vec!["page one".into(), "two".into()]),
|
||||
C::WritableBook {
|
||||
pages: vec!["page one".into(), "two".into()],
|
||||
filtered: vec![],
|
||||
},
|
||||
1900254632,
|
||||
),
|
||||
(
|
||||
@@ -739,6 +759,7 @@ mod tests {
|
||||
generation: 1,
|
||||
pages: vec![text("p1")],
|
||||
resolved: true,
|
||||
..WrittenBook::default()
|
||||
}),
|
||||
-112185549,
|
||||
),
|
||||
@@ -751,6 +772,7 @@ mod tests {
|
||||
generation: 0,
|
||||
pages: vec![],
|
||||
resolved: false,
|
||||
..WrittenBook::default()
|
||||
}),
|
||||
-809804424,
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! the codec form. See [`super::raw`] for the rest.
|
||||
use super::*;
|
||||
use crate::buf::Reader;
|
||||
use horizon_world::snbt_text::to_snbt;
|
||||
|
||||
const USE_ANIMATION: [&str; 12] = [
|
||||
"none",
|
||||
@@ -215,9 +216,57 @@ fn kinetic_condition(r: &mut Reader) -> Option<HashCode> {
|
||||
Some(float_or(rec, "min_relative_speed", relative, 0.0).build())
|
||||
}
|
||||
|
||||
/// `BlockPredicate.STREAM_CODEC` as `BlockPredicate.CODEC` writes it. A block
|
||||
/// entity `nbt` (a lenient SNBT string) and `predicates` (partial component
|
||||
/// matchers) have no codec form built here.
|
||||
/// The `Codec.BOOL` fields of the component predicates' codecs
|
||||
/// (`FireworkPredicate`, `WrittenBookPredicate`, `MobEffectInstancePredicate`):
|
||||
/// they travel as NBT bytes and hash as booleans. The text components inside a
|
||||
/// written book's page predicates add [`TEXT_BOOLS`].
|
||||
const PREDICATE_BOOLS: [&str; 12] = [
|
||||
"has_twinkle",
|
||||
"has_trail",
|
||||
"resolved",
|
||||
"ambient",
|
||||
"visible",
|
||||
"bold",
|
||||
"italic",
|
||||
"underlined",
|
||||
"strikethrough",
|
||||
"obfuscated",
|
||||
"interpret",
|
||||
"plain",
|
||||
];
|
||||
/// Whether a compound anywhere in `tag` has a field `name`.
|
||||
fn has_field(tag: &horizon_world::nbt::Tag, name: &str) -> bool {
|
||||
use horizon_world::nbt::Tag;
|
||||
match tag {
|
||||
Tag::Compound(map) => map
|
||||
.iter()
|
||||
.any(|(k, v)| k.0.iter().copied().eq(name.encode_utf16()) || has_field(v, name)),
|
||||
Tag::List(items) => items.iter().any(|t| has_field(t, name)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// `DataComponentPredicate.STREAM_CODEC`: each entry is a type (a predicate
|
||||
/// type, or a component type for `AnyValue`) and the predicate as the NBT its
|
||||
/// `CODEC` encodes (`fromCodecWithRegistries`), hashed as the map
|
||||
/// `DataComponentPredicate.CODEC` (`Codec.dispatchedMap`) writes.
|
||||
fn partial_predicate(r: &mut Reader, names: &dyn Names) -> Option<(HashCode, HashCode)> {
|
||||
let registry = if boolean(r)? {
|
||||
"minecraft:data_component_predicate_type"
|
||||
} else {
|
||||
"minecraft:data_component_type"
|
||||
};
|
||||
let key = entry(names, registry, int(r)?)?;
|
||||
let tag = r.nbt().ok()?;
|
||||
// The nested `components` of an item predicate (`container`,
|
||||
// `bundle_contents`) are component values as NBT, whose booleans cannot be
|
||||
// told from bytes here.
|
||||
if has_field(&tag, "components") {
|
||||
return None;
|
||||
}
|
||||
Some((key, HashOps::from_nbt(&tag, &PREDICATE_BOOLS)))
|
||||
}
|
||||
|
||||
/// `BlockPredicate.STREAM_CODEC` as `BlockPredicate.CODEC` writes it.
|
||||
fn block_predicate(r: &mut Reader, names: &dyn Names) -> Option<HashCode> {
|
||||
let blocks = optional(r, |r| holder_set("minecraft:block", names, r))?;
|
||||
// StatePropertiesPredicate: a name and an exact value or a (min, max) range.
|
||||
@@ -238,9 +287,12 @@ fn block_predicate(r: &mut Reader, names: &dyn Names) -> Option<HashCode> {
|
||||
})?;
|
||||
Some(HashOps::create_map(props))
|
||||
})?;
|
||||
if boolean(r)? {
|
||||
return None;
|
||||
}
|
||||
// NbtPredicate: `TagParser.LENIENT_CODEC` encodes with its flattened codec,
|
||||
// the tag's SNBT string.
|
||||
let nbt = optional(r, |r| match r.nbt().ok()? {
|
||||
tag @ horizon_world::nbt::Tag::Compound(_) => Some(string(&to_snbt(&tag))),
|
||||
_ => None,
|
||||
})?;
|
||||
// DataComponentExactPredicate: the expected components, by type name.
|
||||
let exact = list(r, |r| {
|
||||
let kind = int(r)?;
|
||||
@@ -248,17 +300,25 @@ fn block_predicate(r: &mut Reader, names: &dyn Names) -> Option<HashCode> {
|
||||
let value = Component::read(kind, r).ok()?.code(kind, names)?;
|
||||
Some((name, value))
|
||||
})?;
|
||||
if r.count(r.remaining()).ok()? != 0 {
|
||||
// DataComponentPredicate: the partial matchers (at most 64).
|
||||
let partial = list(r, |r| partial_predicate(r, names))?;
|
||||
if partial.len() > 64 {
|
||||
return None;
|
||||
}
|
||||
let rec = RecordHash::new()
|
||||
.optional("blocks", blocks)
|
||||
.optional("state", state);
|
||||
.optional("state", state)
|
||||
.optional("nbt", nbt);
|
||||
let rec = if exact.is_empty() {
|
||||
rec
|
||||
} else {
|
||||
rec.field("components", HashOps::create_map(exact))
|
||||
};
|
||||
Some(
|
||||
if exact.is_empty() {
|
||||
if partial.is_empty() {
|
||||
rec
|
||||
} else {
|
||||
rec.field("components", HashOps::create_map(exact))
|
||||
rec.field("predicates", HashOps::create_map(partial))
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -482,9 +482,26 @@ mod tests {
|
||||
("minecraft:entity_type", 11) => "minecraft:bee",
|
||||
("minecraft:entity_type", 30) => "minecraft:cow",
|
||||
("minecraft:block_entity_type", 1) => "minecraft:chest",
|
||||
("minecraft:data_component_type", 1) => "minecraft:max_stack_size",
|
||||
("minecraft:data_component_type", 2) => "minecraft:max_damage",
|
||||
("minecraft:data_component_type", 3) => "minecraft:damage",
|
||||
("minecraft:data_component_type", 12) => "minecraft:rarity",
|
||||
("minecraft:villager_type", 0) => "minecraft:desert",
|
||||
("minecraft:data_component_predicate_type", 0) => "minecraft:damage",
|
||||
("minecraft:data_component_predicate_type", 1) => "minecraft:enchantments",
|
||||
("minecraft:data_component_predicate_type", 2) => "minecraft:stored_enchantments",
|
||||
("minecraft:data_component_predicate_type", 3) => "minecraft:potion_contents",
|
||||
("minecraft:data_component_predicate_type", 4) => "minecraft:custom_data",
|
||||
("minecraft:data_component_predicate_type", 5) => "minecraft:container",
|
||||
("minecraft:data_component_predicate_type", 6) => "minecraft:bundle_contents",
|
||||
("minecraft:data_component_predicate_type", 7) => "minecraft:firework_explosion",
|
||||
("minecraft:data_component_predicate_type", 8) => "minecraft:fireworks",
|
||||
("minecraft:data_component_predicate_type", 9) => "minecraft:writable_book_content",
|
||||
("minecraft:data_component_predicate_type", 10) => "minecraft:written_book_content",
|
||||
("minecraft:data_component_predicate_type", 11) => "minecraft:attribute_modifiers",
|
||||
("minecraft:data_component_predicate_type", 12) => "minecraft:trim",
|
||||
("minecraft:data_component_predicate_type", 13) => "minecraft:jukebox_playable",
|
||||
("minecraft:data_component_predicate_type", 14) => "minecraft:villager/variant",
|
||||
("minecraft:attribute", 1) => "minecraft:armor",
|
||||
("minecraft:attribute", 3) => "minecraft:attack_damage",
|
||||
("minecraft:attribute", 23) => "minecraft:max_health",
|
||||
@@ -854,4 +871,142 @@ mod tests {
|
||||
// Bytes left over after the value.
|
||||
assert_eq!(Component::Raw(vec![0, 0]).hash(34, &Pinned), None);
|
||||
}
|
||||
|
||||
/// Block predicates with an `nbt` or partial component predicates, and books
|
||||
/// with filtered text: stream bytes and hashes vanilla 26.3 printed (the same
|
||||
/// driver as above, SNBT through the component's codec). The one `None` is a
|
||||
/// container predicate whose nested item predicate holds exact `components`.
|
||||
#[test]
|
||||
fn predicates_and_filtered_books_hash_as_vanilla_does() {
|
||||
let cases: [(&str, i32, &str, Option<i32>); 21] = [
|
||||
(
|
||||
"stone_nbt",
|
||||
14,
|
||||
"0101020100010a010001610108000162000178000000",
|
||||
Some(-359417396),
|
||||
),
|
||||
(
|
||||
"nbt_all_types",
|
||||
14,
|
||||
"010000010a08000361206200027122020001730007060001654202a05f20000000050001783fc000000600017940000000000000000b0001690000000200000001000000020900054974656d730a000000010800026964000e6d696e6563726166743a64697274010005436f756e7401000700017a0000000201020400016c00000000000000050c00014c0000000100000000000000030500016e00000000000000",
|
||||
Some(18640168),
|
||||
),
|
||||
(
|
||||
"damage_partial",
|
||||
14,
|
||||
"010102010000000101000a0a000664616d6167650300036d696e000000010300036d6178000000050000",
|
||||
Some(-752850259),
|
||||
),
|
||||
(
|
||||
"custom_data",
|
||||
14,
|
||||
"010000000001010408000d7b613a312c623a5b312c325d7d",
|
||||
Some(-1320400474),
|
||||
),
|
||||
(
|
||||
"twinkle",
|
||||
14,
|
||||
"01000000000101070a080005736861706500047374617201000b6861735f7477696e6b6c650100",
|
||||
Some(-1476889576),
|
||||
),
|
||||
(
|
||||
"trail_false",
|
||||
14,
|
||||
"01000000000101070a0100096861735f747261696c0000",
|
||||
Some(-1034820270),
|
||||
),
|
||||
("any_value", 14, "01000000000100020a00", Some(-1904716790)),
|
||||
(
|
||||
"villager",
|
||||
14,
|
||||
"010000000001010e0800106d696e6563726166743a646573657274",
|
||||
Some(1625640440),
|
||||
),
|
||||
(
|
||||
"potion_effects",
|
||||
14,
|
||||
"01000000000101030a0a000765666665637473090008636f6e7461696e730a000000010a000f6d696e6563726166743a73706565640a00086475726174696f6e0300036d6178000000640001000776697369626c6500030009616d706c696669657200000001010007616d6269656e740100000000",
|
||||
Some(150754403),
|
||||
),
|
||||
(
|
||||
"container",
|
||||
14,
|
||||
"01000000000101050a0a00056974656d73090008636f6e7461696e730a000000010a0005636f756e740300036d696e00000002000800056974656d73000f6d696e6563726166743a73746f6e650003000473697a65000000030000",
|
||||
Some(-820017514),
|
||||
),
|
||||
(
|
||||
"fireworks",
|
||||
14,
|
||||
"01000000000101080a0a000a6578706c6f73696f6e7303000473697a65000000010003000f666c696768745f6475726174696f6e0000000200",
|
||||
Some(-551561987),
|
||||
),
|
||||
(
|
||||
"attribute_modifiers",
|
||||
14,
|
||||
"010000000001010b0a0a00096d6f64696669657273090008636f6e7461696e730a000000010a0006616d6f756e740600036d696e3ff800000000000000080009617474726962757465000f6d696e6563726166743a61726d6f72080004736c6f740004686561640800096f7065726174696f6e00096164645f76616c7565000000",
|
||||
Some(-233125185),
|
||||
),
|
||||
(
|
||||
"bundle_stored",
|
||||
14,
|
||||
"01000000000201060a0a00056974656d73090008636f6e7461696e730a000000010800056974656d73000f6d696e6563726166743a73746f6e650000000102090a000000010300066c6576656c730000000300",
|
||||
Some(1901599730),
|
||||
),
|
||||
(
|
||||
"writable_book",
|
||||
14,
|
||||
"01000000000101090a0a00057061676573090008636f6e7461696e7308000000010001780000",
|
||||
Some(-883070685),
|
||||
),
|
||||
(
|
||||
"written_book",
|
||||
14,
|
||||
"010000000001010a0a03000a67656e65726174696f6e000000020a00057061676573090008636f6e7461696e730a000000010800047465787400026869010004626f6c64010000080006617574686f720001410100087265736f6c7665640000",
|
||||
Some(1470044104),
|
||||
),
|
||||
(
|
||||
"all_fields",
|
||||
14,
|
||||
"010000010a0300016100000001000101050301000a03000a6475726162696c697479000000030001040800027b7d00020a00",
|
||||
Some(808431166),
|
||||
),
|
||||
(
|
||||
"container_components",
|
||||
14,
|
||||
"01000000000101050a0a00056974656d73090008636f6e7461696e730a000000010a000a636f6d706f6e656e74730100246d696e6563726166743a656e6368616e746d656e745f676c696e745f6f7665727269646501000800056974656d73000f6d696e6563726166743a73746f6e65000000",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"can_break_two",
|
||||
15,
|
||||
"0201020100010a000000000000000100020a00",
|
||||
Some(2146318348),
|
||||
),
|
||||
(
|
||||
"written_filtered",
|
||||
57,
|
||||
"01540101460161010208000170010a08000474657874000171010004626f6c640100080005706c61696e0001",
|
||||
Some(1884774986),
|
||||
),
|
||||
(
|
||||
"written_plain",
|
||||
57,
|
||||
"01540001610001080001700000",
|
||||
Some(100213250),
|
||||
),
|
||||
(
|
||||
"writable_filtered",
|
||||
56,
|
||||
"03036f6e6501022a2a0374776f000133010133",
|
||||
Some(1192237218),
|
||||
),
|
||||
];
|
||||
for (name, kind, hex, expected) in cases {
|
||||
let bytes = unhex(hex);
|
||||
let mut r = Reader::new(&bytes);
|
||||
let c = Component::read(kind, &mut r).unwrap();
|
||||
assert_eq!(r.remaining(), 0, "{name}");
|
||||
assert_eq!(c.hash(kind, &Pinned), expected, "{name} {hex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ pub mod registry_fluid;
|
||||
pub mod retrogen;
|
||||
pub mod server_player_combat;
|
||||
pub mod server_player_damage;
|
||||
pub mod snbt_text;
|
||||
pub mod stored_consume;
|
||||
pub mod stored_effect_attributes;
|
||||
pub mod suffocation;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! `StringTagVisitor` (compact SNBT, `Tag.toString`) and Java's number
|
||||
//! formatting, below the text-component layer so that the protocol can hash
|
||||
//! a tag's string form.
|
||||
use crate::nbt::Tag;
|
||||
|
||||
/// `Double.toString`.
|
||||
pub fn java_double(v: f64) -> String {
|
||||
java_float_text(v, &format!("{v}"), &format!("{v:e}"))
|
||||
}
|
||||
/// `Float.toString`.
|
||||
pub fn java_float(v: f32) -> String {
|
||||
java_float_text(f64::from(v), &format!("{v}"), &format!("{v:e}"))
|
||||
}
|
||||
fn java_float_text(v: f64, plain: &str, sci: &str) -> String {
|
||||
if v.is_nan() {
|
||||
return "NaN".into();
|
||||
}
|
||||
if v.is_infinite() {
|
||||
return if v > 0.0 { "Infinity" } else { "-Infinity" }.into();
|
||||
}
|
||||
if v == 0.0 {
|
||||
return if v.is_sign_negative() { "-0.0" } else { "0.0" }.into();
|
||||
}
|
||||
let a = v.abs();
|
||||
if (1.0e-3..1.0e7).contains(&a) {
|
||||
if plain.contains('.') {
|
||||
plain.to_owned()
|
||||
} else {
|
||||
format!("{plain}.0")
|
||||
}
|
||||
} else {
|
||||
let (mantissa, exponent) = sci.split_once('e').unwrap_or((sci, "0"));
|
||||
let mantissa = if mantissa.contains('.') {
|
||||
mantissa.to_owned()
|
||||
} else {
|
||||
format!("{mantissa}.0")
|
||||
};
|
||||
format!("{mantissa}E{exponent}")
|
||||
}
|
||||
}
|
||||
|
||||
/// `SnbtGrammar.escapeControlCharacters`.
|
||||
fn escape_control(c: char) -> Option<&'static str> {
|
||||
Some(match c {
|
||||
'\u{8}' => "b",
|
||||
'\u{c}' => "f",
|
||||
'\n' => "n",
|
||||
'\r' => "r",
|
||||
'\t' => "t",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// `StringTag.quoteAndEscape`.
|
||||
pub fn quote_and_escape(input: &str) -> String {
|
||||
let mut out = String::from(" ");
|
||||
let mut quote = '\0';
|
||||
for c in input.chars() {
|
||||
if c == '\\' {
|
||||
out.push_str("\\\\");
|
||||
} else if c != '"' && c != '\'' {
|
||||
match escape_control(c) {
|
||||
Some(e) => {
|
||||
out.push('\\');
|
||||
out.push_str(e);
|
||||
}
|
||||
None => out.push(c),
|
||||
}
|
||||
} else {
|
||||
if quote == '\0' {
|
||||
quote = if c == '"' { '\'' } else { '"' };
|
||||
}
|
||||
if quote == c {
|
||||
out.push('\\');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
if quote == '\0' {
|
||||
quote = '"';
|
||||
}
|
||||
out.replace_range(0..1, "e.to_string());
|
||||
out.push(quote);
|
||||
out
|
||||
}
|
||||
fn unquoted_key(key: &str) -> bool {
|
||||
if key.eq_ignore_ascii_case("true") || key.eq_ignore_ascii_case("false") {
|
||||
return false;
|
||||
}
|
||||
let mut chars = key.chars();
|
||||
let first_ok = chars
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '.' || c == '_');
|
||||
// `[A-Za-z._]+[A-Za-z0-9._+-]*`
|
||||
first_ok && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '+' | '-'))
|
||||
}
|
||||
pub fn string_of(t: &crate::nbt::JavaString) -> String {
|
||||
t.to_string_lossy()
|
||||
}
|
||||
|
||||
/// `Tag.toString` (StringTagVisitor).
|
||||
pub fn to_snbt(tag: &Tag) -> String {
|
||||
let mut out = String::new();
|
||||
visit(tag, &mut out);
|
||||
out
|
||||
}
|
||||
fn visit(tag: &Tag, out: &mut String) {
|
||||
match tag {
|
||||
Tag::End => out.push_str("END"),
|
||||
Tag::Byte(v) => out.push_str(&format!("{v}b")),
|
||||
Tag::Short(v) => out.push_str(&format!("{v}s")),
|
||||
Tag::Int(v) => out.push_str(&v.to_string()),
|
||||
Tag::Long(v) => out.push_str(&format!("{v}L")),
|
||||
Tag::Float(v) => {
|
||||
out.push_str(&java_float(f32::from_bits(*v)));
|
||||
out.push('f');
|
||||
}
|
||||
Tag::Double(v) => {
|
||||
out.push_str(&java_double(f64::from_bits(*v)));
|
||||
out.push('d');
|
||||
}
|
||||
Tag::String(s) => out.push_str("e_and_escape(&string_of(s))),
|
||||
Tag::ByteArray(a) => {
|
||||
out.push_str("[B;");
|
||||
out.push_str(
|
||||
&a.iter()
|
||||
.map(|v| format!("{}B", *v as i8))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
out.push(']');
|
||||
}
|
||||
Tag::IntArray(a) => {
|
||||
out.push_str("[I;");
|
||||
out.push_str(&a.iter().map(i32::to_string).collect::<Vec<_>>().join(","));
|
||||
out.push(']');
|
||||
}
|
||||
Tag::LongArray(a) => {
|
||||
out.push_str("[L;");
|
||||
out.push_str(
|
||||
&a.iter()
|
||||
.map(|v| format!("{v}L"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
out.push(']');
|
||||
}
|
||||
Tag::List(l) => {
|
||||
out.push('[');
|
||||
for (i, t) in l.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
visit(t, out);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Tag::Compound(c) => {
|
||||
out.push('{');
|
||||
let mut entries: Vec<(String, &Tag)> =
|
||||
c.iter().map(|(k, v)| (string_of(k), v)).collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (i, (k, v)) in entries.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
if unquoted_key(k) {
|
||||
out.push_str(k);
|
||||
} else {
|
||||
out.push_str("e_and_escape(k));
|
||||
}
|
||||
out.push(':');
|
||||
visit(v, out);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user