From c6828f2c63e1d95f6bbcb74eb394413a3dda004f Mon Sep 17 00:00:00 2001 From: Pookachu Date: Thu, 14 May 2026 22:58:12 -0500 Subject: [PATCH 1/2] wiping old system entirely. --- src/registry/benches/registry_lookup.rs | 69 ------------ src/registry/build.rs | 135 ------------------------ src/registry/src/lib.rs | 128 ---------------------- 3 files changed, 332 deletions(-) delete mode 100644 src/registry/benches/registry_lookup.rs diff --git a/src/registry/benches/registry_lookup.rs b/src/registry/benches/registry_lookup.rs deleted file mode 100644 index 64a6941d9..000000000 --- a/src/registry/benches/registry_lookup.rs +++ /dev/null @@ -1,69 +0,0 @@ -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; -use std::hint::black_box; - -/// Helper function to run `init()` once. -/// In our new compile-time setup, this is a no-op, -/// but it's good practice to keep it. -fn setup() { - temper_registry::init(); -} - -/// Benchmarks the `ITEM_NAME_TO_ID` map (from registries.json) -fn bench_item_lookup(c: &mut Criterion) { - setup(); - let mut group = c.benchmark_group("registry_item_lookup"); - group.throughput(Throughput::Elements(1)); - - group.bench_function("lookup_item_protocol_id (apple)", |b| { - b.iter(|| black_box(temper_registry::lookup_item_protocol_id("minecraft:apple"))) - }); - - group.bench_function("lookup_item_protocol_id (cobblestone)", |b| { - b.iter(|| { - black_box(temper_registry::lookup_item_protocol_id( - "minecraft:cobblestone", - )) - }) - }); - group.finish(); -} - -/// Benchmarks the `BLOCKSTATE_ID_TO_NAME` map (from blockstates.json) -fn bench_blockstate_lookup(c: &mut Criterion) { - setup(); - let mut group = c.benchmark_group("registry_blockstate_lookup"); - group.throughput(Throughput::Elements(1)); - - group.bench_function("lookup_blockstate_name (stone)", |b| { - b.iter(|| black_box(temper_registry::lookup_blockstate_name("1"))) - }); - - group.bench_function("lookup_blockstate_name (grass)", |b| { - b.iter(|| black_box(temper_registry::lookup_blockstate_name("9"))) - }); - group.finish(); -} - -/// Benchmarks the `ITEM_ID_STR_TO_BLOCKSTATE_ID_STR` map -fn bench_item_to_block_lookup(c: &mut Criterion) { - setup(); - let mut group = c.benchmark_group("registry_item_to_block_lookup"); - group.throughput(Throughput::Elements(1)); - - group.bench_function("lookup_item_to_block_id_str (stone)", |b| { - b.iter(|| { - // Assuming item "1" (stone) maps to a block - black_box(temper_registry::lookup_item_to_block_id_str("1")) - }) - }); - group.finish(); -} - -// Register all the new benchmark functions -criterion_group!( - benches, - bench_item_lookup, - bench_blockstate_lookup, - bench_item_to_block_lookup -); -criterion_main!(benches); diff --git a/src/registry/build.rs b/src/registry/build.rs index 58500158e..e69de29bb 100644 --- a/src/registry/build.rs +++ b/src/registry/build.rs @@ -1,135 +0,0 @@ -use phf_codegen::Map; -use serde::Deserialize; -use std::collections::HashMap; -use std::env; -use std::fs; -use std::io::Write; -use std::path::Path; - -// --- 1. Define strong types for the JSON data --- - -#[derive(Deserialize)] -struct BlockStateEntry { - name: String, -} - -#[derive(Deserialize)] -struct ItemEntry { - protocol_id: i32, -} - -#[derive(Deserialize)] -struct BlockEntry { - // We only care about hardness for now - // It's sometimes missing (like for Air), so it's an Option - hardness: Option, -} - -#[derive(Deserialize)] -struct BlockRegistry { - entries: HashMap, -} - -// Type for registries.json: "minecraft:item" -> "entries" -> "minecraft:stone" -> ItemEntry -#[derive(Deserialize)] -struct RegistryRoot { - #[serde(rename = "minecraft:item")] - item: ItemRegistry, - - #[serde(rename = "minecraft:block")] - block: BlockRegistry, -} - -#[derive(Deserialize)] -struct ItemRegistry { - entries: HashMap, -} - -// --- 2. The Main Build Function --- -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=../../assets/data/registries.json"); - println!("cargo:rerun-if-changed=../../assets/data/blockstates.json"); - println!("cargo:rerun-if-changed=../../assets/data/item_to_block_mapping.json"); - - // --- 3. Load and parse all files --- - let registry_str = fs::read_to_string("../../assets/data/registries.json").unwrap(); - let registry: RegistryRoot = serde_json::from_str(®istry_str).unwrap(); - - let bs_str = fs::read_to_string("../../assets/data/blockstates.json").unwrap(); - let blockstates: HashMap = serde_json::from_str(&bs_str).unwrap(); - - let i2b_str = fs::read_to_string("../../assets/data/item_to_block_mapping.json").unwrap(); - let item_to_block: HashMap = serde_json::from_str(&i2b_str).unwrap(); - - // --- 4. Get the output path --- - let out_dir = env::var_os("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("registry_data.rs"); - let mut file = fs::File::create(&dest_path).unwrap(); - - // --- 5. Generate `phf::Map` for ItemName -> Protocol_ID --- - write!( - file, - "static ITEM_NAME_TO_ID: phf::Map<&'static str, i32> = " - ) - .unwrap(); - let mut item_map = Map::new(); - for (name, entry) in ®istry.item.entries { - // Use & to borrow - item_map.entry(name, entry.protocol_id.to_string()); - } - writeln!(file, "{};\n", item_map.build()).unwrap(); - - // --- 5b. Generate reverse map: Protocol_ID -> ItemName --- - write!( - file, - "static ITEM_ID_TO_NAME: phf::Map = " - ) - .unwrap(); - let mut item_id_map = Map::new(); - for (name, entry) in ®istry.item.entries { - // Here, the key is the i32 protocol_id - item_id_map.entry(entry.protocol_id, format!("r#\"{}\"#", name)); - } - writeln!(file, "{};\n", item_id_map.build()).unwrap(); - - // --- 6. Generate `phf::Map` for BlockStateID -> Block Name --- - write!( - file, - "static BLOCKSTATE_ID_TO_NAME: phf::Map<&'static str, &'static str> = " - ) - .unwrap(); - let mut bs_map = Map::new(); - for (id, data) in blockstates { - // Use `entry` to build a valid raw string literal for the value - bs_map.entry(id, format!("r#\"{}\"#", data.name)); - } - writeln!(file, "{};\n", bs_map.build()).unwrap(); - - // --- 7. Generate `phf::Map` for ItemID (str) -> BlockStateID (str) --- - write!( - file, - "static ITEM_ID_STR_TO_BLOCKSTATE_ID_STR: phf::Map<&'static str, &'static str> = " - ) - .unwrap(); - let mut i2b_map = Map::new(); - for (item_id_str, block_state_id_str) in item_to_block { - i2b_map.entry(item_id_str, format!("r#\"{}\"#", block_state_id_str)); - } - writeln!(file, "{};\n", i2b_map.build()).unwrap(); - - write!( - file, - "static BLOCK_NAME_TO_HARDNESS: phf::Map<&'static str, u32> = " - ) - .unwrap(); - let mut hardness_map = Map::new(); - for (name, entry) in registry.block.entries { - // We store the f32 as a u32 to use it in the const map - // (PHF maps don't like f32). We'll convert it back at runtime. - let hardness_f32 = entry.hardness.unwrap_or(0.0); - let hardness_u32 = hardness_f32.to_bits(); - hardness_map.entry(name, hardness_u32.to_string()); - } - writeln!(file, "{};\n", hardness_map.build()).unwrap(); -} diff --git a/src/registry/src/lib.rs b/src/registry/src/lib.rs index 5822b613f..e69de29bb 100644 --- a/src/registry/src/lib.rs +++ b/src/registry/src/lib.rs @@ -1,128 +0,0 @@ -// Include the file generated by `build.rs` -// This file contains all the static `phf::Map`s -include!(concat!(env!("OUT_DIR"), "/registry_data.rs")); - -/// Initializes the game registry. -/// This is now a no-op (it does nothing) because all data is -/// loaded at compile time. It's called by main.rs and tests. -pub fn init() { - // This function is now free! -} - -/// Looks up an item's protocol ID (e.g., 840) from its name (e.g., "minecraft:apple"). -/// This is an instant, compile-time map lookup. -pub fn lookup_item_protocol_id(name: &str) -> Option { - ITEM_NAME_TO_ID.get(name).copied() -} - -/// Looks up an item's name (e.g., "minecraft:apple") from its protocol ID (e.g., 840). -/// This is an instant, compile-time map lookup. -pub fn lookup_item_name(protocol_id: i32) -> Option<&'static str> { - ITEM_ID_TO_NAME.get(&protocol_id).copied() -} - -/// Looks up a block *name* (e.g., "minecraft:stone") from a protocol ID string. -/// This is an instant, compile-time map lookup. -pub fn lookup_blockstate_name(protocol_id: &str) -> Option<&'static str> { - BLOCKSTATE_ID_TO_NAME.get(protocol_id).copied() -} - -/// Looks up a block state ID string from an item ID string. -/// This is an instant, compile-time map lookup. -pub fn lookup_item_to_block_id_str(item_id_str: &str) -> Option<&'static str> { - ITEM_ID_STR_TO_BLOCKSTATE_ID_STR.get(item_id_str).copied() -} - -/// Looks up a block's hardness (e.g., 1.5 for stone) from its name. -/// This is an instant, compile-time map lookup. -pub fn lookup_block_hardness(block_name: &str) -> Option { - BLOCK_NAME_TO_HARDNESS - .get(block_name) - .map(|hardness_u32| f32::from_bits(*hardness_u32)) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Helper to ensure the registry is loaded before each test. - /// This is now a no-op, but we'll keep it for good practice. - fn setup() { - init(); - } - - #[test] - fn test_lookup_item_protocol_id() { - setup(); // This function is now free, but we call it for consistency - - // Test "apple" - let apple_id = lookup_item_protocol_id("minecraft:apple"); - assert!( - apple_id.is_some(), - "lookup_item_protocol_id(\"minecraft:apple\") failed" - ); - assert_eq!(apple_id.unwrap(), 857); - - // Test "cobblestone" - let cobble_id = lookup_item_protocol_id("minecraft:cobblestone"); - assert!( - cobble_id.is_some(), - "lookup_item_protocol_id(\"minecraft:cobblestone\") failed" - ); - // Add assert_eq! if you know the ID - } - - #[test] - fn test_lookup_item_protocol_id_non_existent() { - setup(); - let value = lookup_item_protocol_id("minecraft:non_existent_item"); - assert!(value.is_none()); - } - - #[test] - fn test_lookup_item_name() { - setup(); - - // Test "apple" (ID 857) - let apple_name = lookup_item_name(857); - assert!(apple_name.is_some(), "lookup_item_name(857) failed"); - assert_eq!(apple_name.unwrap(), "minecraft:apple"); - } - - #[test] - fn test_lookup_blockstate_stone() { - setup(); // No-op, but good practice - - let stone_entry = lookup_blockstate_name("1"); - assert!( - stone_entry.is_some(), - "lookup_blockstate_name(\"1\") returned None" - ); - assert_eq!(stone_entry.unwrap(), "minecraft:stone"); - } - - #[test] - fn test_lookup_blockstate_grass() { - setup(); - - let grass_entry = lookup_blockstate_name("9"); - assert!( - grass_entry.is_some(), - "lookup_blockstate_name(\"9\") returned None" - ); - assert_eq!(grass_entry.unwrap(), "minecraft:grass_block"); - } - - #[test] - fn test_lookup_item_to_block_id() { - setup(); - - // Assuming item "1" (stone) maps to blockstate "1" - let block_id_str = lookup_item_to_block_id_str("1"); - assert!( - block_id_str.is_some(), - "lookup_item_to_block_id_str(\"1\") returned None" - ); - assert_eq!(block_id_str.unwrap(), "1"); - } -} From 068afc4a69151c587c71071cd06beb08bdeb3aa6 Mon Sep 17 00:00:00 2001 From: Pookachu Date: Fri, 15 May 2026 14:41:50 -0500 Subject: [PATCH 2/2] Block Registry --- scripts/item_block_mapping_id_replace.py | 88 -------- scripts/item_to_block_mapping.py | 52 ----- src/core/Cargo.toml | 1 + src/core/src/block_properties.rs | 128 ----------- src/core/src/block_state_id.rs | 156 +++---------- src/core/src/lib.rs | 1 - src/inventories/src/item.rs | 5 +- src/registry/Cargo.toml | 8 - src/registry/build.rs | 0 src/registry/src/blocks.rs | 275 +++++++++++++++++++++++ src/registry/src/items.rs | 17 ++ src/registry/src/lib.rs | 2 + 12 files changed, 334 insertions(+), 399 deletions(-) delete mode 100644 scripts/item_block_mapping_id_replace.py delete mode 100644 scripts/item_to_block_mapping.py delete mode 100644 src/core/src/block_properties.rs delete mode 100644 src/registry/build.rs create mode 100644 src/registry/src/blocks.rs create mode 100644 src/registry/src/items.rs diff --git a/scripts/item_block_mapping_id_replace.py b/scripts/item_block_mapping_id_replace.py deleted file mode 100644 index 7e1536a5d..000000000 --- a/scripts/item_block_mapping_id_replace.py +++ /dev/null @@ -1,88 +0,0 @@ -import json -import logging -from datetime import datetime -from pathlib import Path -from collections import Counter -from typing import Dict, Any - -ASSETS_DIR = Path('assets') -BLOCKS_PATH = ASSETS_DIR / 'extracted' / 'blocks.json' -MAPPING_PATH = ASSETS_DIR / 'data' / 'item_to_block_mapping.json' -LOG_PATH = Path('mapping_gen.log') - -def load_json(path: Path) -> Dict[str, Any]: - """Helper to load JSON safely.""" - try: - return json.loads(path.read_text(encoding='utf-8')) - except (FileNotFoundError, json.JSONDecodeError): - return {} - -def save_json(path: Path, data: Dict[str, Any]) -> None: - """Helper to save JSON with consistent formatting.""" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(data, indent=4, sort_keys=True), encoding='utf-8') - -def generate_mapping() -> None: - log_buffer = [f"=== Generation Log: {datetime.now()} ==="] - - if not BLOCKS_PATH.exists(): - print(f"Error: Source file not found at {BLOCKS_PATH}") - return - - print(f"Reading {BLOCKS_PATH.name}...") - source_data = load_json(BLOCKS_PATH) - - old_mapping = load_json(MAPPING_PATH) - if not old_mapping and MAPPING_PATH.exists(): - print("Warning: Existing mapping file is corrupt or empty. Treating as full rebuild.") - - new_mapping = {} - stats = Counter() - blocks = source_data.get("blocks", []) - - print(f"Processing {len(blocks)} blocks...") - - for block in blocks: - block_name = block.get("name", "unknown") - item_id = block.get("item_id") - default_state = block.get("default_state_id") - - if item_id in (None, -1): - stats["skipped_no_item"] += 1 - continue - - key_id = str(item_id) - val_id = str(default_state) - - # Detect changes vs new entries - if key_id not in old_mapping: - stats["added"] += 1 - log_buffer.append(f"[NEW] Item {key_id} ({block_name}) -> State {val_id}") - elif old_mapping[key_id] != val_id: - stats["updated"] += 1 - log_buffer.append(f"[UPDATE] Item {key_id} ({block_name}): {old_mapping[key_id]} -> {val_id}") - - new_mapping[key_id] = val_id - stats["processed"] += 1 - - print(f"Writing output to {MAPPING_PATH}...") - save_json(MAPPING_PATH, new_mapping) - - summary = ( - f"\n=== Summary ===\n" - f"Total Blocks Scanned: {len(blocks)}\n" - f"Valid Mappings: {stats['processed']}\n" - f"Newly Added: {stats['added']}\n" - f"Updated: {stats['updated']}\n" - f"Skipped (No Item ID): {stats['skipped_no_item']}\n" - ) - log_buffer.append(summary) - - print(f"Saving log to {LOG_PATH}...") - LOG_PATH.write_text('\n'.join(log_buffer), encoding='utf-8') - - print(summary) - print("Done.") - -if __name__ == "__main__": - generate_mapping() \ No newline at end of file diff --git a/scripts/item_to_block_mapping.py b/scripts/item_to_block_mapping.py deleted file mode 100644 index 37d9e56bf..000000000 --- a/scripts/item_to_block_mapping.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -import json -from pathlib import Path - -REG_PATH = Path("../assets/data/registries.json") # items with protocol_id -BS_PATH = Path("../assets/data/blockstates.json") # { "": { "name": "...", "default": bool, ... } } -OUT_PATH = Path("../assets/data/item_to_block_mapping.json") # { "": "" } - - -def main(): - root = json.loads(REG_PATH.read_text(encoding="utf-8")) - blockstates = json.loads(BS_PATH.read_text(encoding="utf-8")) - - # Build name -> chosen block id - # Rule: default:true wins and can't be overridden; otherwise use first seen. - name_to_blockid = {} - first_seen = {} - - for sid, data in blockstates.items(): - if not isinstance(data, dict): - continue - name = data.get("name") - if not isinstance(name, str): - continue - sid = str(sid) - is_def = bool(data.get("default", False)) - - if name not in first_seen: - first_seen[name] = sid - if is_def: - name_to_blockid[name] = sid - elif name not in name_to_blockid: - name_to_blockid[name] = sid - - # Build item_pid -> block_id via exact name match - items = root.get("minecraft:item", {}).get("entries", {}) - mapping = {} - for item_name, item_data in items.items(): - if isinstance(item_data, dict) and "protocol_id" in item_data: - block_id = name_to_blockid.get(item_name) - if block_id is not None: - mapping[str(item_data["protocol_id"])] = block_id - - # OPTIONAL sanity guard (remove if you don't like the assert) - if mapping.get("35") != "14": - raise RuntimeError(f"Sanity-check failed: item 35 should map to 14, got {mapping.get('35')}") - - OUT_PATH.write_text(json.dumps(mapping, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -if __name__ == "__main__": - main() diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 01c1ca6b9..670b9ce0b 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -10,6 +10,7 @@ bevy_ecs = { workspace = true } bevy_math = { workspace = true } temper-text = { workspace = true } temper-codec = { workspace = true } +temper-registry = { workspace = true } crossbeam-queue = { workspace = true } bitcode = { workspace = true } bitcode_derive = { workspace = true } diff --git a/src/core/src/block_properties.rs b/src/core/src/block_properties.rs deleted file mode 100644 index 5a8b722f2..000000000 --- a/src/core/src/block_properties.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::sync::LazyLock; - -use crate::block_data::BlockData; -use crate::block_state_id::{BlockStateId, ID2BLOCK}; - -/// Precomputed solidity for all block states. -/// A block is solid if it has a full collision box that entities cannot walk through. -/// Indexed by `BlockStateId::raw()`. -static SOLID_BLOCKS: LazyLock> = LazyLock::new(|| { - ID2BLOCK - .get_or_init(|| crate::block_state_id::create_block_mappings().0) - .iter() - .map(compute_solid) - .collect() -}); - -/// Returns whether a block is solid (has a full collision box). -/// -/// This is the single source of truth for solidity, used by both the collision -/// system and the pathfinding system. -#[inline] -pub fn is_solid(id: BlockStateId) -> bool { - SOLID_BLOCKS - .get(id.raw() as usize) - .copied() - .unwrap_or(false) -} - -/// Determine whether a block data entry represents a solid block. -fn compute_solid(data: &BlockData) -> bool { - let name = data.name.trim_start_matches("minecraft:"); - - // Air variants are never solid - if name.ends_with("air") { - return false; - } - - // Liquids - if matches!(name, "water" | "lava" | "bubble_column") { - return false; - } - - // Fire - if matches!(name, "fire" | "soul_fire") { - return false; - } - if name.ends_with("_campfire") { - // Campfires are solid blocks you can stand on, but lit ones deal damage - return true; - } - - // Doors, fence gates, trapdoors: solid only when closed - if name.ends_with("_door") || name.ends_with("_fence_gate") || name.ends_with("_trapdoor") { - let open = data - .properties - .as_ref() - .and_then(|p| p.get("open")) - .is_some_and(|v| v == "true"); - return !open; - } - - // Non-solid vegetation and decorations - if is_non_solid_decoration(name) { - return false; - } - - // Default: solid - true -} - -/// Returns true for blocks that have no collision box (decorative, vegetation, etc.) -pub fn is_non_solid_decoration(name: &str) -> bool { - if matches!( - name, - "grass" - | "short_grass" - | "tall_grass" - | "fern" - | "large_fern" - | "dead_bush" - | "snow" - | "string" - | "nether_portal" - | "spore_blossom" - | "glow_lichen" - | "dandelion" - | "poppy" - | "blue_orchid" - | "allium" - | "azure_bluet" - | "oxeye_daisy" - | "cornflower" - | "lily_of_the_valley" - | "wither_rose" - | "sunflower" - | "lilac" - | "rose_bush" - | "peony" - | "torchflower" - | "pitcher_plant" - | "pitcher_pod" - | "sweet_berry_bush" - | "cobweb" - | "powder_snow" - | "redstone_wire" - | "rail" - | "powered_rail" - | "detector_rail" - | "activator_rail" - | "tripwire" - | "tripwire_hook" - | "structure_void" - ) { - return true; - } - - name.ends_with("_button") - || name.ends_with("_pressure_plate") - || name.ends_with("_sign") - || name.ends_with("_banner") - || name.ends_with("_carpet") - || name.ends_with("_torch") - || name.ends_with("_sapling") - || name.ends_with("_mushroom") - || name.ends_with("_flower") - || name.ends_with("_vine") - || name.ends_with("_roots") -} diff --git a/src/core/src/block_state_id.rs b/src/core/src/block_state_id.rs index 621dce9ee..4298ed7f1 100644 --- a/src/core/src/block_state_id.rs +++ b/src/core/src/block_state_id.rs @@ -1,167 +1,83 @@ -use crate::block_data::BlockData; -use ahash::RandomState; +// temper/src/core/src/block_state_id.rs use deepsize::DeepSizeOf; -use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::fmt::Display; -use std::process::exit; -use std::str::FromStr; use temper_codec::net_types::var_int::VarInt; -use tracing::{error, warn}; use type_hash::TypeHash; -// The number of block entries in the mappings file -// Go to the .etc/blockstates.json file, see what the last ID is, and add 1 to it. -const BLOCK_ENTRIES: usize = 27914; - -const BLOCKSFILE: &str = include_str!("../../../assets/data/blockstates.json"); - -pub static ID2BLOCK: OnceCell> = OnceCell::new(); -pub static BLOCK2ID: OnceCell> = OnceCell::new(); - -pub fn create_block_mappings() -> (Vec, HashMap) { - let string_keys: HashMap = - serde_json::from_str(BLOCKSFILE).unwrap(); - if string_keys.len() != BLOCK_ENTRIES { - error!("Block mappings file is not the correct length"); - error!( - "Expected {} entries, found {}", - BLOCK_ENTRIES, - string_keys.len() - ); - exit(1); - } - let mut id2block = Vec::with_capacity(BLOCK_ENTRIES); - for _ in 0..BLOCK_ENTRIES { - id2block.push(BlockData::default()); - } - string_keys - .iter() - .map(|(k, v)| (k.parse::().unwrap(), v.clone())) - .for_each(|(k, v)| id2block[k as usize] = v); - let block2id: HashMap = id2block - .iter() - .enumerate() - .map(|(k, v)| (v.clone(), k as i32)) - .collect(); - (id2block, block2id) -} +use temper_registry::blocks::{self, BlockMaterial}; -/// An ID for a block, and it's state in the world. Use this over `BlockData` unless you need to -/// modify or read the block's name/properties directly. -/// -/// This should be used over `BlockData` in most cases, as it's much more efficient to store and pass around. -/// You can also generate a block's id at runtime with the [temper_macros::block!] macro. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, DeepSizeOf, TypeHash)] -pub struct BlockStateId(u32); +#[repr(transparent)] +pub struct BlockStateId(pub u32); impl BlockStateId { - /// Do NOT use this by yourself. Instead use the block macro `block!("stone")` the item to block - /// map + #[inline(always)] pub const fn new(id: u32) -> Self { Self(id) } - /// Given a BlockData, return a BlockStateId. Does not clone, should be quite fast. - pub fn from_block_data(block_data: &BlockData) -> Self { - let id = BLOCK2ID - .get_or_init(|| create_block_mappings().1) - .get(block_data) - .copied() - .unwrap_or_else(|| { - warn!("Block data '{block_data}' not found in block mappings file"); - 0 - }); - BlockStateId(id as u32) - } - - /// Given a block state ID, return a BlockData. Will clone, so don't use in hot loops. - /// If the ID is not found, returns None. - pub fn to_block_data(&self) -> Option { - ID2BLOCK - .get_or_init(|| create_block_mappings().0) - .get(self.0 as usize) - .cloned() - } - + #[inline(always)] pub fn from_varint(var_int: VarInt) -> Self { - BlockStateId(var_int.0 as u32) + Self(var_int.0 as u32) } + #[inline(always)] pub fn to_varint(&self) -> VarInt { VarInt(self.0 as i32) } - /// Do Not use this by yourself. This is only useful for apis that use this as an index or key - /// to get additionally information about this state. + #[inline(always)] pub const fn raw(&self) -> u32 { self.0 } -} -impl Display for BlockStateId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(block_data) = self.to_block_data() { - write!(f, "BlockStateId({}: {:?})", self.0, block_data) - } else { - write!(f, "BlockStateId({}: Unknown)", self.0) - } - } -} + // --- Registry Lookups --- -impl BlockData { - /// Converts a BlockData to a BlockStateId. Will panic if the ID is not found. - pub fn to_block_state_id(&self) -> BlockStateId { - BlockStateId::from_block_data(self) + #[inline(always)] + pub fn hardness(self) -> f32 { + blocks::hardness(self.0) } - /// Converts a BlockStateId to a BlockData. Will panic if the ID is not found. - pub fn from_block_state_id(block_state_id: BlockStateId) -> BlockData { - block_state_id - .to_block_data() - .expect("Block state ID not found in block mappings file") + #[inline(always)] + pub fn material(self) -> BlockMaterial { + blocks::material(self.0) } - pub fn try_to_block_state_id(&self) -> Option { - Some(BlockStateId::from_block_data(self)) - } -} -impl From for BlockStateId { - fn from(block_data: BlockData) -> Self { - BlockStateId::from_block_data(&block_data) + #[inline(always)] + pub fn is_transparent(self) -> bool { + blocks::is_transparent(self.0) } -} -impl From for BlockData { - /// Converts a BlockStateId to a BlockData. Will panic if the ID is not found. - fn from(block_state_id: BlockStateId) -> Self { - block_state_id - .to_block_data() - .expect("Block state ID not found in block mappings file") - } -} -impl From for BlockStateId { - /// Converts a VarInt to a BlockStateId. Probably a no-op, but included for completeness. - fn from(var_int: VarInt) -> Self { - Self(var_int.0 as u32) + #[inline(always)] + pub fn is_solid(self) -> bool { + temper_registry::blocks::is_solid(self.0) } } -impl From for VarInt { - /// Converts a BlockStateId to a VarInt. Probably a no-op, but included for completeness. - fn from(block_state_id: BlockStateId) -> Self { - VarInt(block_state_id.0 as i32) +impl Display for BlockStateId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "BlockStateId({})", self.0) } } impl Default for BlockStateId { - /// Returns a BlockStateId with ID 0, which is air. fn default() -> Self { Self(0) } } +// ----------------------------------------------------------------------------- +// ITEM TO BLOCK MAPPING +// Note: This still relies on runtime JSON parsing and a slow HashMap. +// You should migrate this to `temper-registry/src/items.rs` using a flat array +// in exactly the same way we just did for blocks. +// ----------------------------------------------------------------------------- + +use once_cell::sync::OnceCell; +use std::collections::HashMap; +use std::str::FromStr; + const ITEM_TO_BLOCK_MAPPING_FILE: &str = include_str!("../../../assets/data/item_to_block_mapping.json"); pub static ITEM_TO_BLOCK_MAPPING: OnceCell> = OnceCell::new(); diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs index 073b93128..af38f11f1 100644 --- a/src/core/src/lib.rs +++ b/src/core/src/lib.rs @@ -2,7 +2,6 @@ pub mod errors; // Core structs/types. Usually used in ECS Components. pub mod block_data; -pub mod block_properties; pub mod block_state_id; pub mod color; pub mod dimension; diff --git a/src/inventories/src/item.rs b/src/inventories/src/item.rs index 8d6c64da1..2f52680d5 100644 --- a/src/inventories/src/item.rs +++ b/src/inventories/src/item.rs @@ -33,9 +33,10 @@ impl ItemID { let id_key = protocol_id.to_string(); - // 1. Call the new compile-time lookup - let block_name = temper_registry::lookup_blockstate_name(&id_key)?; + // call the registry to get the block name as a string + let block_name = temper_registry::blocks::name(block_state_id.raw()); + // feed that shit to the item system ItemID::from_name(block_name) } diff --git a/src/registry/Cargo.toml b/src/registry/Cargo.toml index ed16d0fa5..82c10b611 100644 --- a/src/registry/Cargo.toml +++ b/src/registry/Cargo.toml @@ -2,18 +2,10 @@ name = "temper-registry" version = "0.1.0" edition = "2024" -build = "build.rs" [dependencies] -phf = { workspace = true } - -[dev-dependencies] -criterion = { workspace = true } - -[build-dependencies] serde = { workspace = true } serde_json = { workspace = true } -phf_codegen = { workspace = true } [lints] workspace = true diff --git a/src/registry/build.rs b/src/registry/build.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/registry/src/blocks.rs b/src/registry/src/blocks.rs new file mode 100644 index 000000000..ecf7155cc --- /dev/null +++ b/src/registry/src/blocks.rs @@ -0,0 +1,275 @@ +use serde::Deserialize; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::OnceLock; + +static REGISTRY: OnceLock = OnceLock::new(); + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawBlockData<'a> { + id: u32, + name: &'a str, + hardness: Option, + resistance: f32, + material: Option<&'a str>, + transparent: bool, + min_state_id: u32, + max_state_id: u32, + states: Vec, +} + +#[derive(Deserialize)] +struct RawState { + id: u32, + properties: Option>, +} + +struct BlockRegistry { + hardness: Vec, + resistance: Vec, + is_transparent: Vec, + material: Vec, + state_to_block: Vec, + is_solid: Vec, + names: Vec, +} + +// --- Enums --- + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum BlockMaterial { + Default = 0, + Cobweb = 1, + GourdAxe = 2, + IncorrectForWoodenTool = 3, + LeavesHoe = 4, + Axe = 5, + Hoe = 6, + Pickaxe = 7, + Shovel = 8, + PlantAxe = 9, + SwordInstantlyMines = 10, + VinePlantAxe = 11, + Wool = 12, + Unknown = 255, +} + +impl BlockMaterial { + pub fn from_str(s: &str) -> Self { + match s { + "default" => Self::Default, + "coweb" => Self::Cobweb, + "gourd;mineable/axe" => Self::GourdAxe, + "incorrect_for_wooden_tool" => Self::IncorrectForWoodenTool, + "leaves;mineable/hoe" => Self::LeavesHoe, + "mineable/axe" => Self::Axe, + "mineable/hoe" => Self::Hoe, + "mineable/pickaxe" => Self::Pickaxe, + "mineable/shovel" => Self::Shovel, + "plant;mineable/axe" => Self::PlantAxe, + "sword_instantly_mines" => Self::SwordInstantlyMines, + "vine_or_glow_lichen;plant;mineable/axe" => Self::VinePlantAxe, + "wool" => Self::Wool, + _ => Self::Unknown, + } + } +} + +// --- Getters (Using State IDs) --- + +#[inline(always)] +fn base_id(state_id: u32) -> u32 { + let reg = REGISTRY.get().expect("Registry not initialized"); + reg.state_to_block + .get(state_id as usize) + .copied() + .unwrap_or(0) +} + +#[inline(always)] +pub fn hardness(state_id: u32) -> f32 { + let reg = REGISTRY.get().expect("Registry not initialized"); + reg.hardness + .get(base_id(state_id) as usize) + .copied() + .unwrap_or(0.0) +} + +#[inline(always)] +pub fn material(state_id: u32) -> BlockMaterial { + let reg = REGISTRY.get().expect("Registry not initialized"); + reg.material + .get(base_id(state_id) as usize) + .copied() + .unwrap_or(BlockMaterial::Default) +} + +#[inline(always)] +pub fn is_transparent(state_id: u32) -> bool { + let reg = REGISTRY.get().expect("Registry not initialized"); + reg.is_transparent + .get(base_id(state_id) as usize) + .copied() + .unwrap_or(false) +} + +#[inline(always)] +pub fn is_solid(state_id: u32) -> bool { + let reg = REGISTRY.get().expect("Registry not initialized"); + reg.is_solid + .get(state_id as usize) + .copied() + .unwrap_or(false) +} + +#[inline(always)] +pub fn name(state_id: u32) -> &'static str { + let reg = REGISTRY.get().expect("Registry not initialized"); + reg.names + .get(base_id(state_id) as usize) + .map(|s| s.as_str()) + .unwrap_or("air") // Fallback to air if out of bounds +} + +fn is_non_solid_decoration(name: &str) -> bool { + if matches!( + name, + "grass" + | "short_grass" + | "tall_grass" + | "fern" + | "large_fern" + | "dead_bush" + | "snow" + | "string" + | "nether_portal" + | "spore_blossom" + | "glow_lichen" + | "dandelion" + | "poppy" + | "blue_orchid" + | "allium" + | "azure_bluet" + | "oxeye_daisy" + | "cornflower" + | "lily_of_the_valley" + | "wither_rose" + | "sunflower" + | "lilac" + | "rose_bush" + | "peony" + | "torchflower" + | "pitcher_plant" + | "pitcher_pod" + | "sweet_berry_bush" + | "cobweb" + | "powder_snow" + | "redstone_wire" + | "rail" + | "powered_rail" + | "detector_rail" + | "activator_rail" + | "tripwire" + | "tripwire_hook" + | "structure_void" + ) { + return true; + } + + name.ends_with("_button") + || name.ends_with("_pressure_plate") + || name.ends_with("_sign") + || name.ends_with("_banner") + || name.ends_with("_carpet") + || name.ends_with("_torch") + || name.ends_with("_sapling") + || name.ends_with("_mushroom") + || name.ends_with("_flower") + || name.ends_with("_vine") + || name.ends_with("_roots") +} + +pub fn init(json_string: &str) { + let raw_blocks: Vec = + serde_json::from_str(json_string).expect("Failed to parse blocks.json"); + + let max_id = raw_blocks.iter().map(|b| b.id).max().unwrap_or(0) as usize; + let highest_state = raw_blocks.iter().map(|b| b.max_state_id).max().unwrap_or(0) as usize; + + let mut hardness = vec![0.0; max_id + 1]; + let mut resistance = vec![0.0; max_id + 1]; + let mut is_transparent = vec![false; max_id + 1]; + let mut material = vec![BlockMaterial::Default; max_id + 1]; + let mut state_to_block = vec![0; highest_state + 1]; + let mut is_solid = vec![false; highest_state + 1]; + let mut names = vec![String::new(); max_id + 1]; + + for block in raw_blocks { + let idx = block.id as usize; + names[idx] = block.name.to_string(); + hardness[idx] = block.hardness.unwrap_or(0.0); + resistance[idx] = block.resistance; + material[idx] = block + .material + .map(BlockMaterial::from_str) + .unwrap_or(BlockMaterial::Default); + is_transparent[idx] = block.transparent; + + // Fallback mapping for all states + for state_id in block.min_state_id..=block.max_state_id { + state_to_block[state_id as usize] = block.id; + } + + let name = block.name; + + // Process specific properties and overwrite the solidity map + for state in block.states { + let state_id = state.id as usize; + state_to_block[state_id] = block.id; + + let mut solid = true; + + if name.ends_with("air") + || matches!( + name, + "water" | "lava" | "bubble_column" | "fire" | "soul_fire" + ) + { + solid = false; + } else if name.ends_with("_campfire") { + solid = true; + } else if name.ends_with("_door") + || name.ends_with("_fence_gate") + || name.ends_with("_trapdoor") + { + // Prismarine handles booleans inconsistently. Check for native bool OR string "true". + let is_open = state + .properties + .as_ref() + .and_then(|p| p.get("open")) + .map(|v| v.as_bool().unwrap_or_else(|| v.as_str() == Some("true"))) + .unwrap_or(false); + solid = !is_open; + } else if is_non_solid_decoration(name) { + solid = false; + } + + is_solid[state_id] = solid; + } + } + + REGISTRY + .set(BlockRegistry { + hardness, + resistance, + is_transparent, + material, + state_to_block, + is_solid, + names, + }) + .map_err(|_| "Registry already initialized") + .unwrap(); +} diff --git a/src/registry/src/items.rs b/src/registry/src/items.rs new file mode 100644 index 000000000..396ce42ab --- /dev/null +++ b/src/registry/src/items.rs @@ -0,0 +1,17 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawItemData<'a> { + id: u32, + name: &'a str, + stack_size: u8, + max_durability: Option, +} + +struct ItemRegistry { + names: Vec, + stack_size: Vec, + // For max_durability, 0 will mean "unbreakable" or "does not use durability" + max_durability: Vec, +} diff --git a/src/registry/src/lib.rs b/src/registry/src/lib.rs index e69de29bb..253d0ebc4 100644 --- a/src/registry/src/lib.rs +++ b/src/registry/src/lib.rs @@ -0,0 +1,2 @@ +pub mod blocks; +pub mod items;