mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 02:29:50 -10:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
})?)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -194,7 +194,7 @@ mod tests {
|
||||
BlockPos::new(1, 75, 1),
|
||||
BlockState {
|
||||
name: "minecraft:air".into(),
|
||||
properties: BTreeMap::new(),
|
||||
properties: Default::default(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user