Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
Ludovic Marie committed 2026-11-01 13:34:42 +01:00
commit d635bc57f9
20 files changed
+623 -62

No files matched your search

@@ -133,7 +133,7 @@ fn main() -> Result<()> {
.collect();
let descriptor = BlockState {
name: name.clone(),
properties,
properties: properties.into(),
};
let Some(id) = registry.id(&descriptor) else {
states.insert(key.clone(), Value::Null);
@@ -179,7 +179,7 @@ fn state(registry: &BlockRegistry, text: &str) -> BlockStateId {
registry
.resolve(&BlockState {
name: name.to_owned(),
properties,
properties: properties.into(),
})
.unwrap_or_else(|e| panic!("{text}: {e}"))
}
@@ -119,7 +119,7 @@ fn parse_state(spec: &str) -> BlockState {
.collect();
BlockState {
name: name.to_owned(),
properties,
properties: properties.into(),
}
}
+6 -1
View File
@@ -7734,7 +7734,12 @@ impl Game<'_> {
// GuiGraphicsExtractor.itemDecorations, laid out at the origin and
// moved to (x, y): the damage bar, the cooldown shade and the count.
let info = self.items().get(&stack.item);
let max_damage = info.map_or(0, |info| info.max_damage as i32);
// ItemStack.isBarVisible: isDamaged, which an unbreakable stack never is.
let max_damage = if horizon_client::stations::is_damageable(self.items(), stack) {
info.map_or(0, |info| info.max_damage as i32)
} else {
0
};
let cooldown =
horizon_client::cooldowns::group(stack, info.and_then(|i| i.use_cooldown.as_ref()));
let partial = self.clock.fraction() as f32;
+4 -1
View File
@@ -28,7 +28,10 @@ fn parse_state(text: &str) -> Result<BlockState> {
let (k, v) = pair.split_once('=').ok_or("properties are key=value")?;
properties.insert(k.to_owned(), v.to_owned());
}
Ok(BlockState { name, properties })
Ok(BlockState {
name,
properties: properties.into(),
})
}
impl Game<'_> {
@@ -183,7 +183,7 @@ impl Game<'_> {
}
let state = horizon_world::chunk::BlockState {
name: name.to_owned(),
properties,
properties: properties.into(),
};
match self.pack.assets.registry.resolve(&state) {
Ok(id) => self.level_set(pos, id).is_ok(),
@@ -195,7 +195,7 @@ impl Game<'_> {
}
Ok(registry.resolve(&BlockState {
name: name.to_owned(),
properties,
properties: properties.into(),
})?)
}
+52
View File
@@ -574,6 +574,58 @@ mod tests {
);
}
/// An unbreakable damaged stack must keep both components through inventory
/// sync, including the integrated server's window (#5721).
#[test]
fn unbreakable_survives_inventory_sync() {
use horizon_protocol::buf::{Codec, Reader, Writer};
use horizon_protocol::item::Slot;
let mut components = vec![String::new(); component::UNBREAKABLE as usize + 1];
components[component::DAMAGE as usize] = "minecraft:damage".into();
components[component::UNBREAKABLE as usize] = "minecraft:unbreakable".into();
let builtin = Builtin {
registries: [
(
"minecraft:item".into(),
Ids::from_names(vec!["minecraft:air".into(), "minecraft:diamond_hoe".into()]),
),
(
"minecraft:data_component_type".into(),
Ids::from_names(components),
),
]
.into(),
};
for unbreakable in [false, true] {
let mut hoe = ItemStack::new("minecraft:diamond_hoe", 1);
hoe.damage = 58;
let marker = Tag::Compound(Default::default());
if unbreakable {
hoe.data_mut()
.set_other("minecraft:unbreakable", Some(marker.clone()));
}
let sent = crate::lan::guest::stack_to_network(|_| 1, |_, _| None, &Some(hoe));
let mut w = Writer::new();
Slot::write(&sent, &mut w);
let mut r = Reader::new(&w.data);
let received = Slot::read(&mut r).unwrap().unwrap();
assert_eq!(r.remaining(), 0);
assert_eq!(
received.component(component::UNBREAKABLE),
unbreakable.then_some(&Component::Unit)
);
let back = from_network(&received, &builtin, &Dynamic::default());
assert_eq!(
(&*back.item, back.count, back.damage),
("minecraft:diamond_hoe", 1, 58)
);
assert_eq!(
back.data().and_then(|d| d.other("minecraft:unbreakable")),
unbreakable.then_some(&marker)
);
}
}
/// An ominous bottle's `ominous_bottle_amplifier` reaches the client
/// through the packet's bytes, so its tooltip names the Bad Omen level.
#[test]
+8
View File
@@ -753,6 +753,14 @@ fn network_stack(
));
}
if let Some(d) = s.data() {
// Unbreakable is a unit component; clients need its presence for the
// durability bar, tooltip and item model, including in single player.
if d.other("minecraft:unbreakable").is_some() {
stack.added.push((
horizon_protocol::item::component::UNBREAKABLE,
horizon_protocol::item::Component::Unit,
));
}
for (name, value) in &d.other {
let (removed, kind) = match name.strip_prefix('!') {
Some(kind) => (true, kind),
+1 -1
View File
@@ -194,7 +194,7 @@ mod tests {
BlockPos::new(1, 75, 1),
BlockState {
name: "minecraft:air".into(),
properties: BTreeMap::new(),
properties: Default::default(),
},
)
.unwrap();
+11 -11
View File
@@ -7,7 +7,7 @@ use horizon_world::loot::PackOverlay;
use horizon_world::registry::{BlockRegistry, BlockStateId};
use serde_json::Value;
use std::{
collections::{BTreeMap, HashMap, HashSet},
collections::{HashMap, HashSet},
fs,
path::Path,
sync::Arc,
@@ -1043,7 +1043,7 @@ impl Items {
table: &Value,
random: &mut R,
) -> Vec<ItemStack> {
let empty = BTreeMap::new();
let empty = horizon_world::chunk::BlockProperties::new();
let ctx = LootContext {
items: self,
block: "",
@@ -1236,7 +1236,7 @@ impl Items {
let Some(table) = self.loot.get(id) else {
return vec![];
};
let empty = BTreeMap::new();
let empty = horizon_world::chunk::BlockProperties::new();
let ctx = LootContext {
items: self,
block: "",
@@ -1295,7 +1295,7 @@ impl Items {
let Some(table) = self.loot.get(id) else {
return vec![];
};
let empty = BTreeMap::new();
let empty = horizon_world::chunk::BlockProperties::new();
let ctx = LootContext {
items: self,
block: "",
@@ -1318,7 +1318,7 @@ impl Items {
let Some(table) = self.loot.get(&format!("minecraft:entities/{name}")) else {
return vec![];
};
let empty = BTreeMap::new();
let empty = horizon_world::chunk::BlockProperties::new();
let ctx = LootContext {
items: self,
block: "",
@@ -1365,7 +1365,7 @@ impl LootContext<'_> {
struct LootContext<'a> {
items: &'a Items,
block: &'a str,
properties: &'a BTreeMap<String, String>,
properties: &'a horizon_world::chunk::BlockProperties,
held: Option<&'a ItemStack>,
killed_by_player: bool,
/// `LootContextParams.EXPLOSION_RADIUS`: an explosion that decays drops.
@@ -2097,7 +2097,7 @@ mod tests {
fn ctx<'a>(
items: &'a Items,
held: Option<&'a ItemStack>,
props: &'a BTreeMap<String, String>,
props: &'a horizon_world::chunk::BlockProperties,
block: &'a str,
) -> LootContext<'a> {
LootContext {
@@ -2114,7 +2114,7 @@ mod tests {
#[test]
fn fortune_silk_touch_and_looting() {
let items = Items::for_tests(HashMap::new());
let props = BTreeMap::new();
let props = horizon_world::chunk::BlockProperties::new();
let mut pick = ItemStack::new("minecraft:diamond_pickaxe", 1);
pick.enchant("minecraft:fortune", 3);
let ore = json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:alternatives","children":[
@@ -2186,7 +2186,7 @@ mod tests {
#[test]
fn set_potion_gives_the_potion_its_contents() {
let items = Items::for_tests(HashMap::new());
let props = BTreeMap::new();
let props = horizon_world::chunk::BlockProperties::new();
let table = json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:item","name":"minecraft:splash_potion",
"modifier":{"type":"minecraft:set_potion","id":"minecraft:fire_resistance"}}]}]});
let drops = ctx(&items, None, &props, "").evaluate(&table, &mut LegacyRandom::new(1));
@@ -2203,7 +2203,7 @@ mod tests {
let Some(items) = super::prepared_items() else {
return;
};
let props = BTreeMap::new();
let props = horizon_world::chunk::BlockProperties::new();
let mut random = LegacyRandom::new(11);
// A sword at levels 20..39 comes out enchanted, as in an End city.
let levels = json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:item",
@@ -2264,7 +2264,7 @@ mod tests {
let Some(items) = super::prepared_items() else {
return;
};
let props = BTreeMap::new();
let props = horizon_world::chunk::BlockProperties::new();
let table = json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:item",
"name":"minecraft:book","modifier":[
{"type":"minecraft:enchant_randomly","options":"minecraft:mending"},
@@ -0,0 +1,418 @@
//! A block state's properties: a few name/value pairs, sorted by name, read
//! like the `BTreeMap<String, String>` they replace. A registry's states share
//! their strings (35,723 vanilla states hold 158,409 properties made of 237
//! distinct strings): one string for the whole process instead of a B-tree
//! leaf (544 bytes) and two allocations per property. Shared strings are
//! never freed, nor counted: cloning a state copies pointers, so world
//! generation threads cloning the same states don't contend on counters.
use std::{
borrow::Borrow,
cmp::Ordering,
collections::BTreeMap,
fmt,
hash::{Hash, Hasher},
ops::Deref,
sync::Mutex,
};
/// A property name or value: owned, or one string a registry shares.
#[derive(Clone)]
enum Text {
Owned(String),
Shared(&'static String),
}
impl Deref for Text {
type Target = String;
fn deref(&self) -> &String {
match self {
Self::Owned(s) => s,
Self::Shared(s) => s,
}
}
}
impl Text {
fn into_string(self) -> String {
match self {
Self::Owned(s) => s,
Self::Shared(s) => s.clone(),
}
}
fn to_mut(&mut self) -> &mut String {
if let Self::Shared(s) = self {
*self = Self::Owned((**s).clone());
}
match self {
Self::Owned(s) => s,
Self::Shared(_) => unreachable!(),
}
}
}
type Interned = horizon_core::fast_hash::FxHashMap<&'static str, &'static String>;
/// Every string a registry's states share, for the process: registries'
/// names and values only (a few hundred), never a player's.
static INTERNED: Mutex<Option<Interned>> = Mutex::new(None);
/// The strings one registry's states share ([`BlockProperties::share`]):
/// the process's, with the ones already met at hand.
#[derive(Default)]
pub struct SharedStrings(Interned);
impl SharedStrings {
pub fn get(&mut self, text: &str) -> &'static String {
if let Some(shared) = self.0.get(text) {
return shared;
}
let mut interned = INTERNED.lock().unwrap_or_else(|e| e.into_inner());
let interned = interned.get_or_insert_with(Interned::default);
let shared = match interned.get(text) {
Some(shared) => *shared,
None => {
let shared: &'static String = Box::leak(Box::new(text.to_owned()));
interned.insert(shared.as_str(), shared);
shared
}
};
self.0.insert(shared.as_str(), shared);
shared
}
}
/// A block state's properties by name, in name order.
#[derive(Clone, Default)]
pub struct BlockProperties(Box<[(Text, Text)]>);
impl BlockProperties {
pub fn new() -> Self {
Self::default()
}
fn position<Q: Ord + ?Sized>(&self, key: &Q) -> Result<usize, usize>
where
String: Borrow<Q>,
{
self.0.binary_search_by(|(k, _)| (**k).borrow().cmp(key))
}
pub fn get<Q: Ord + ?Sized>(&self, key: &Q) -> Option<&String>
where
String: Borrow<Q>,
{
self.position(key).ok().map(|i| &*self.0[i].1)
}
pub fn get_mut<Q: Ord + ?Sized>(&mut self, key: &Q) -> Option<&mut String>
where
String: Borrow<Q>,
{
let i = self.position(key).ok()?;
Some(self.0[i].1.to_mut())
}
pub fn contains_key<Q: Ord + ?Sized>(&self, key: &Q) -> bool
where
String: Borrow<Q>,
{
self.position(key).is_ok()
}
/// Sets `key` to `value`; the value it had, if any.
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
match self.position(key.as_str()) {
Ok(i) => Some(std::mem::replace(&mut self.0[i].1, Text::Owned(value)).into_string()),
Err(i) => {
let mut pairs = std::mem::take(&mut self.0).into_vec();
pairs.insert(i, (Text::Owned(key), Text::Owned(value)));
self.0 = pairs.into_boxed_slice();
None
}
}
}
pub fn remove<Q: Ord + ?Sized>(&mut self, key: &Q) -> Option<String>
where
String: Borrow<Q>,
{
let i = self.position(key).ok()?;
let mut pairs = std::mem::take(&mut self.0).into_vec();
let (_, value) = pairs.remove(i);
self.0 = pairs.into_boxed_slice();
Some(value.into_string())
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> Iter<'_> {
Iter(self.0.iter())
}
pub fn keys(&self) -> impl DoubleEndedIterator<Item = &String> + ExactSizeIterator {
self.0.iter().map(|(k, _)| &**k)
}
pub fn values(&self) -> impl DoubleEndedIterator<Item = &String> + ExactSizeIterator {
self.0.iter().map(|(_, v)| &**v)
}
/// These properties over the strings `strings` holds, which other states
/// share (a registry's states, built once). Strings already shared stay.
pub fn share(&mut self, strings: &mut SharedStrings) {
for text in self.0.iter_mut().flat_map(|(k, v)| [k, v]) {
if let Text::Owned(owned) = text {
*text = Text::Shared(strings.get(owned));
}
}
}
/// Shared names and values, collected as a `BTreeMap` collects them.
pub fn from_shared(mut pairs: Vec<(&'static String, &'static String)>) -> Self {
pairs.sort_by(|a, b| a.0.cmp(&b.0));
let mut kept: Vec<(Text, Text)> = Vec::with_capacity(pairs.len());
for (k, v) in pairs {
match kept.last_mut() {
Some(last) if *last.0 == *k => last.1 = Text::Shared(v),
_ => kept.push((Text::Shared(k), Text::Shared(v))),
}
}
Self(kept.into_boxed_slice())
}
}
pub struct Iter<'a>(std::slice::Iter<'a, (Text, Text)>);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a String, &'a String);
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(|(k, v)| (&**k, &**v))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for Iter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(|(k, v)| (&**k, &**v))
}
}
impl ExactSizeIterator for Iter<'_> {}
impl<'a> IntoIterator for &'a BlockProperties {
type Item = (&'a String, &'a String);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Iter<'a> {
self.iter()
}
}
impl IntoIterator for BlockProperties {
type Item = (String, String);
type IntoIter = IntoIter;
fn into_iter(self) -> IntoIter {
IntoIter(self.0.into_vec().into_iter())
}
}
pub struct IntoIter(std::vec::IntoIter<(Text, Text)>);
impl Iterator for IntoIter {
type Item = (String, String);
fn next(&mut self) -> Option<(String, String)> {
self.0
.next()
.map(|(k, v)| (k.into_string(), v.into_string()))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
/// As a `BTreeMap` collects: sorted by name, the last value of a name wins.
impl FromIterator<(String, String)> for BlockProperties {
fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
let mut pairs: Vec<(String, String)> = iter.into_iter().collect();
// Stable: equal names keep their order, and the last one is kept.
pairs.sort_by(|a, b| a.0.cmp(&b.0));
let mut kept: Vec<(Text, Text)> = Vec::with_capacity(pairs.len());
for (k, v) in pairs {
match kept.last_mut() {
Some(last) if *last.0 == k => last.1 = Text::Owned(v),
_ => kept.push((Text::Owned(k), Text::Owned(v))),
}
}
Self(kept.into_boxed_slice())
}
}
impl Extend<(String, String)> for BlockProperties {
fn extend<I: IntoIterator<Item = (String, String)>>(&mut self, iter: I) {
for (k, v) in iter {
self.insert(k, v);
}
}
}
impl<const N: usize> From<[(String, String); N]> for BlockProperties {
fn from(pairs: [(String, String); N]) -> Self {
pairs.into_iter().collect()
}
}
impl From<BTreeMap<String, String>> for BlockProperties {
fn from(map: BTreeMap<String, String>) -> Self {
Self(
map.into_iter()
.map(|(k, v)| (Text::Owned(k), Text::Owned(v)))
.collect(),
)
}
}
impl From<BlockProperties> for BTreeMap<String, String> {
fn from(properties: BlockProperties) -> Self {
properties.into_iter().collect()
}
}
impl<Q: Ord + ?Sized> std::ops::Index<&Q> for BlockProperties
where
String: Borrow<Q>,
{
type Output = String;
/// Panics when the key is missing, as a `BTreeMap` does.
fn index(&self, key: &Q) -> &String {
self.get(key).expect("no entry found for key")
}
}
impl serde::Serialize for BlockProperties {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_map(self.iter())
}
}
impl<'de> serde::Deserialize<'de> for BlockProperties {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
BTreeMap::<String, String>::deserialize(deserializer).map(Self::from)
}
}
// Compared, ordered and hashed as the `BTreeMap` they replace: pair by pair.
impl PartialEq for BlockProperties {
fn eq(&self, other: &Self) -> bool {
self.iter().eq(other.iter())
}
}
impl Eq for BlockProperties {}
impl PartialEq<BTreeMap<String, String>> for BlockProperties {
fn eq(&self, other: &BTreeMap<String, String>) -> bool {
self.iter().eq(other.iter())
}
}
impl PartialOrd for BlockProperties {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BlockProperties {
fn cmp(&self, other: &Self) -> Ordering {
self.iter().cmp(other.iter())
}
}
impl Hash for BlockProperties {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_usize(self.len());
for pair in self {
pair.hash(state);
}
}
}
impl fmt::Debug for BlockProperties {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.iter()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn reads_and_writes_as_the_map_it_replaces() {
let pairs = [
("waterlogged", "false"),
("facing", "north"),
("half", "top"),
];
let mut map = map(&pairs);
let mut properties: BlockProperties = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let same = |p: &BlockProperties, m: &BTreeMap<String, String>| {
assert!(p.iter().eq(m.iter()));
assert_eq!(p.len(), m.len());
assert_eq!(format!("{p:?}"), format!("{m:?}"));
};
same(&properties, &map);
assert_eq!(properties.get("facing"), map.get("facing"));
assert_eq!(properties.get("shape"), None);
assert_eq!(
properties.insert("facing".into(), "east".into()),
map.insert("facing".into(), "east".into())
);
assert_eq!(
properties.insert("axis".into(), "y".into()),
map.insert("axis".into(), "y".into())
);
*properties.get_mut("half").unwrap() = "bottom".into();
*map.get_mut("half").unwrap() = "bottom".into();
assert_eq!(properties.remove("waterlogged"), map.remove("waterlogged"));
assert_eq!(properties.remove("waterlogged"), None);
same(&properties, &map);
assert_eq!(BTreeMap::from(properties), map);
}
#[test]
fn collects_as_a_map_does() {
let pairs = [("b", "1"), ("a", "2"), ("b", "3")];
let properties: BlockProperties = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
assert!(properties.iter().eq(map(&pairs).iter()));
}
#[test]
fn orders_and_hashes_as_the_map_does() {
let sets = [
vec![],
vec![("a", "1")],
vec![("a", "2")],
vec![("a", "1"), ("b", "1")],
vec![("b", "0")],
];
let hash = |value: &dyn Fn(&mut std::collections::hash_map::DefaultHasher)| {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
value(&mut hasher);
hasher.finish()
};
for a in &sets {
for b in &sets {
let (pa, pb) = (BlockProperties::from(map(a)), BlockProperties::from(map(b)));
assert_eq!(pa.cmp(&pb), map(a).cmp(&map(b)), "{a:?} {b:?}");
assert_eq!(pa == pb, map(a) == map(b));
}
let p = BlockProperties::from(map(a));
assert_eq!(hash(&|h| p.hash(h)), hash(&|h| map(a).hash(h)));
}
}
#[test]
fn shared_strings_read_and_change_as_owned_ones() {
let mut strings = SharedStrings::default();
let pairs = map(&[("facing", "north"), ("lit", "true")]);
let mut a = BlockProperties::from(pairs.clone());
let mut b = BlockProperties::from(pairs.clone());
a.share(&mut strings);
b.share(&mut strings);
assert_eq!(a, BlockProperties::from(pairs.clone()));
// One string for both states' value.
let (Text::Shared(x), Text::Shared(y)) = (&a.0[0].1, &b.0[0].1) else {
panic!("not shared");
};
assert!(std::ptr::eq(*x, *y));
// Changing one state's value leaves the other's.
a.get_mut("facing").unwrap().push_str("_east");
assert_eq!(a.get("facing").map(String::as_str), Some("north_east"));
assert_eq!(b.get("facing").map(String::as_str), Some("north"));
assert_eq!(BTreeMap::from(b), pairs);
}
}
+9 -8
View File
@@ -1,5 +1,6 @@
//! Saved section palettes and storage-level chunk access. Changes here do not
//! run block behavior, lighting, heightmaps, neighbor updates or scheduled ticks.
pub use crate::block_state_properties::BlockProperties;
use crate::{
DATA_VERSION,
compression::invalid,
@@ -12,7 +13,7 @@ use horizon_core::{
storage::PackedStorage,
ticks::{ChunkTicks, SavedTick, TickTypeId},
};
use std::{collections::BTreeMap, io};
use std::io;
fn key(value: &str) -> JavaString {
JavaString::from(value)
@@ -161,7 +162,7 @@ impl PalettedContainer {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct BlockState {
pub name: String,
pub properties: BTreeMap<String, String>,
pub properties: BlockProperties,
}
impl BlockState {
pub fn to_nbt(&self) -> Tag {
@@ -184,13 +185,13 @@ impl BlockState {
if let Tag::String(name) = tag {
return Ok(Self {
name: name.to_string().map_err(|e| invalid(e.to_string()))?,
properties: BTreeMap::new(),
properties: BlockProperties::new(),
});
}
let Some(Tag::String(name)) = tag.get("id") else {
return Err(invalid("block state is missing id"));
};
let mut properties = BTreeMap::new();
let mut properties = Vec::new();
if let Some(tag) = tag.get("properties") {
let Tag::Compound(map) = tag else {
return Err(invalid("block properties are not a compound"));
@@ -199,15 +200,15 @@ impl BlockState {
let Tag::String(v) = v else {
return Err(invalid("block property is not a string"));
};
properties.insert(
properties.push((
k.to_string().map_err(|e| invalid(e.to_string()))?,
v.to_string().map_err(|e| invalid(e.to_string()))?,
);
));
}
}
Ok(Self {
name: name.to_string().map_err(|e| invalid(e.to_string()))?,
properties,
properties: properties.into_iter().collect(),
})
}
}
@@ -758,7 +759,7 @@ mod tests {
Tag::compound([("id", Tag::String(format!("minecraft:b{i}").as_str().into()))])
})
.collect();
let defaults: BTreeMap<_, _> = (0..states)
let defaults: std::collections::BTreeMap<_, _> = (0..states)
.map(|i| {
(
JavaString::from(format!("minecraft:b{i}").as_str()),
@@ -3,11 +3,7 @@
//! absent).
use super::*;
use crate::{chunk::BlockState, worldgen::generator::GeneratorData, worldgen::live::LiveFeatures};
use std::{
collections::{BTreeMap, HashMap},
path::PathBuf,
sync::OnceLock,
};
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
struct Fixture {
registry: &'static BlockRegistry,
@@ -121,7 +117,7 @@ impl World {
properties: props
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<BTreeMap<_, _>>(),
.collect(),
};
self.f
.registry
+2 -2
View File
@@ -5,7 +5,7 @@ use super::*;
use crate::chunk::BlockState;
use horizon_core::random::RandomSource;
use std::{
collections::{BTreeMap, HashMap, HashSet},
collections::{HashMap, HashSet},
path::PathBuf,
sync::OnceLock,
};
@@ -184,7 +184,7 @@ impl<'f> World<'f> {
properties: props
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<BTreeMap<_, _>>(),
.collect(),
};
self.f
.registry
+1
View File
@@ -15,6 +15,7 @@ pub mod block_landing;
pub mod block_placement;
pub mod block_properties;
pub mod block_render;
pub mod block_state_properties;
pub mod block_support;
pub mod block_survival;
pub mod blocking_item_damage;
+1 -1
View File
@@ -167,7 +167,7 @@ fn run(args: &[String]) -> Result<(), Box<dyn Error>> {
block,
BlockState {
name: args[6].clone(),
properties,
properties: properties.into(),
},
)?;
let timestamp = std::time::SystemTime::now()
+7 -3
View File
@@ -75,8 +75,9 @@ impl BlockRegistry {
pub fn from_facts_json_with_flags(
reader: impl io::Read,
) -> io::Result<(Self, Vec<(bool, bool)>)> {
let facts: facts::Facts = serde_json::from_reader(io::BufReader::new(reader))
.map_err(|e| invalid(e.to_string()))?;
let facts: facts::Facts =
facts::with_shared_strings(|| serde_json::from_reader(io::BufReader::new(reader)))
.map_err(|e| invalid(e.to_string()))?;
let flags = facts.states.iter().map(|r| (r.air, r.fluid)).collect();
let states = facts.states.into_iter().map(|r| r.state).collect();
Ok((Self::from_states(states, facts.defaults)?, flags))
@@ -92,7 +93,10 @@ impl BlockRegistry {
collided: BTreeMap::new(),
defaults: BTreeMap::new(),
};
for (i, state) in states.into_iter().enumerate() {
// The states' property names and values: a few hundred strings, shared.
let mut strings = crate::block_state_properties::SharedStrings::default();
for (i, mut state) in states.into_iter().enumerate() {
state.properties.share(&mut strings);
if result.id(&state).is_some() {
return Err(invalid("duplicate block registry state"));
}
+95 -22
View File
@@ -1,11 +1,14 @@
//! The part of `facts.json` the block registry reads, deserialized as it
//! streams: each row's state (its fifth column) and the defaults.
use crate::chunk::BlockState;
use crate::{
block_state_properties::SharedStrings,
chunk::{BlockProperties, BlockState},
};
use serde::{
Deserialize, Deserializer,
de::{self, IgnoredAny, SeqAccess, Visitor},
de::{self, IgnoredAny, MapAccess, SeqAccess, Visitor},
};
use std::{collections::BTreeMap, fmt};
use std::{cell::RefCell, collections::BTreeMap, fmt};
#[derive(Deserialize)]
pub(super) struct Facts {
@@ -48,31 +51,101 @@ impl<'de> Deserialize<'de> for Row {
}
}
thread_local! {
/// The strings the states being read share (`with_shared_strings`).
static STRINGS: RefCell<Option<SharedStrings>> = const { RefCell::new(None) };
}
/// Runs `read` with the states it reads sharing their property strings.
pub(super) fn with_shared_strings<T>(read: impl FnOnce() -> T) -> T {
STRINGS.with(|s| *s.borrow_mut() = Some(SharedStrings::default()));
let out = read();
STRINGS.with(|s| s.borrow_mut().take());
out
}
/// A property name or value, shared with the other states' equal ones.
struct Shared(&'static String);
impl<'de> Deserialize<'de> for Shared {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct SharedVisitor;
impl Visitor<'_> for SharedVisitor {
type Value = Shared;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a string")
}
fn visit_str<E: de::Error>(self, text: &str) -> Result<Shared, E> {
Ok(Shared(STRINGS.with(|s| match s.borrow_mut().as_mut() {
Some(strings) => strings.get(text),
None => SharedStrings::default().get(text),
})))
}
}
deserializer.deserialize_str(SharedVisitor)
}
}
/// A state's `properties` object, read straight into shared strings.
struct Properties(BlockProperties);
impl<'de> Deserialize<'de> for Properties {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct PropertiesVisitor;
impl<'de> Visitor<'de> for PropertiesVisitor {
type Value = Properties;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("block state properties")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Properties, A::Error> {
let mut pairs = Vec::new();
while let Some((Shared(k), Shared(v))) = map.next_entry()? {
pairs.push((k, v));
}
Ok(Properties(BlockProperties::from_shared(pairs)))
}
}
deserializer.deserialize_map(PropertiesVisitor)
}
}
/// A state as BlockState::from_nbt reads it: a name, or `{"id", "properties"}`.
struct State(BlockState);
impl<'de> Deserialize<'de> for State {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Shape {
Name(String),
Full {
id: String,
#[serde(default)]
properties: BTreeMap<String, String>,
},
struct StateVisitor;
impl<'de> Visitor<'de> for StateVisitor {
type Value = State;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a block state name or {\"id\", \"properties\"}")
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<State, E> {
Ok(State(BlockState {
name: name.to_owned(),
properties: BlockProperties::new(),
}))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<State, A::Error> {
let (mut id, mut properties) = (None, None);
while let Some(key) = map.next_key::<std::borrow::Cow<'de, str>>()? {
match &*key {
"id" if id.is_some() => return Err(de::Error::duplicate_field("id")),
"id" => id = Some(map.next_value::<String>()?),
"properties" if properties.is_some() => {
return Err(de::Error::duplicate_field("properties"));
}
"properties" => properties = Some(map.next_value::<Properties>()?.0),
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
Ok(State(BlockState {
name: id.ok_or_else(|| de::Error::missing_field("id"))?,
properties: properties.unwrap_or_default(),
}))
}
}
Ok(State(match Shape::deserialize(deserializer)? {
Shape::Name(name) => BlockState {
name,
properties: BTreeMap::new(),
},
Shape::Full { id, properties } => BlockState {
name: id,
properties,
},
}))
deserializer.deserialize_any(StateVisitor)
}
}
@@ -788,7 +788,7 @@ impl StairPillar {
fn stair(
resources: &StructureResources,
block: &str,
from: &std::collections::BTreeMap<String, String>,
from: &crate::chunk::BlockProperties,
facing: Option<&str>,
) -> Option<BlockStateId> {
let registry = resources.registry;