diff --git a/docs/changelog/v3.2.0.md b/docs/changelog/v3.2.0.md
index 609cbd14e..5343115f6 100644
--- a/docs/changelog/v3.2.0.md
+++ b/docs/changelog/v3.2.0.md
@@ -4,7 +4,7 @@
>
> *Introduces new block data and deprecates several older features in preparation for the future.*
-Bookshelf is now based on **Minecraft 1.21.9/10**.
+Bookshelf is now based on **Minecraft 1.21.9-1.21.11**.
- ✨ **[#403](https://github.com/mcbookshelf/bookshelf/issues/403)** - Added the `#bs.load:status` function to view loaded modules.
- ⚡ **[#501](https://github.com/mcbookshelf/bookshelf/pull/501)** - Optimized tp commands used in macros.
@@ -31,6 +31,7 @@ Bookshelf is now based on **Minecraft 1.21.9/10**.
- 🗑️ **[#484](https://github.com/mcbookshelf/bookshelf/pull/484)** - Block tag `has_offset` has been **deprecated** and will be removed in a v4.0.0. Please use `has_shape_offset` instead.
- 🗑️ **[#484](https://github.com/mcbookshelf/bookshelf/pull/484)** - Function `#bs.hitbox:get_block` has been **deprecated** and will be removed in a v4.0.0. Please use `#bs.hitbox:get_block_shape` or `#bs.hitbox:get_block_collision` instead.
- 🗑️ **[#484](https://github.com/mcbookshelf/bookshelf/pull/484)** - Functions `#bs.hitbox:is_entity_in_block_interaction`, `#bs.hitbox:is_entity_in_blocks_interaction`, and `#bs.hitbox:is_in_block_interaction` have been **deprecated** and will be removed in a v4.0.0. Please use `#bs.hitbox:is_entity_in_block_shape`, `#bs.hitbox:is_entity_in_blocks_shape`, or `#bs.hitbox:is_in_block_shape` instead.
+- ✨ **[#502](https://github.com/mcbookshelf/bookshelf/pull/502)** - Added new 1.21.11 entities.
### `🏃 bs.move`
diff --git a/modules/bs.block/data/bs.block/test/get/block.mcfunction b/modules/bs.block/data/bs.block/test/get/block.mcfunction
index 52b7a6a11..f1a1c1438 100644
--- a/modules/bs.block/data/bs.block/test/get/block.mcfunction
+++ b/modules/bs.block/data/bs.block/test/get/block.mcfunction
@@ -16,7 +16,7 @@
setblock ~ ~ ~ minecraft:stone_stairs[facing=west,half=top,shape=straight,waterlogged=false]
function #bs.block:get_block
-assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,waterlogged=false,facing=west,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,waterlogged=false,facing=west,]", properties: { facing: "west", half: "top", shape: "straight", waterlogged: "false" } }
+assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,facing=west,waterlogged=false,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,facing=west,waterlogged=false,]", properties: { facing: "west", half: "top", shape: "straight", waterlogged: "false" } }
setblock ~ ~ ~ minecraft:campfire
function #bs.block:get_block
diff --git a/modules/bs.block/data/bs.block/test/transform/map_type.mcfunction b/modules/bs.block/data/bs.block/test/transform/map_type.mcfunction
index 561708da1..3f3bdb82d 100644
--- a/modules/bs.block/data/bs.block/test/transform/map_type.mcfunction
+++ b/modules/bs.block/data/bs.block/test/transform/map_type.mcfunction
@@ -17,7 +17,7 @@ setblock ~ ~ ~ minecraft:spruce_stairs[facing=west,half=top,shape=straight,water
function #bs.block:get_block
function #bs.block:map_type {type:"minecraft:oak_planks",mapping_registry:"bs.shapes"}
-assert data storage bs:out block{ block: "minecraft:oak_stairs[shape=straight,half=top,waterlogged=false,facing=west,]" }
+assert data storage bs:out block{ block: "minecraft:oak_stairs[shape=straight,half=top,facing=west,waterlogged=false,]" }
function #bs.block:map_type {type:"minecraft:spruce_planks",mapping_registry:"bs.shapes"}
-assert data storage bs:out block{ block: "minecraft:spruce_stairs[shape=straight,half=top,waterlogged=false,facing=west,]" }
+assert data storage bs:out block{ block: "minecraft:spruce_stairs[shape=straight,half=top,facing=west,waterlogged=false,]" }
diff --git a/modules/bs.block/data/bs.block/test/transform/merge_properties.mcfunction b/modules/bs.block/data/bs.block/test/transform/merge_properties.mcfunction
index fea79bc82..dce54aae5 100644
--- a/modules/bs.block/data/bs.block/test/transform/merge_properties.mcfunction
+++ b/modules/bs.block/data/bs.block/test/transform/merge_properties.mcfunction
@@ -18,4 +18,4 @@ function #bs.block:get_block
setblock ~ ~ ~ minecraft:furnace[facing=north]
function #bs.block:merge_properties {properties:[{name:"facing"}]}
-assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,waterlogged=false,facing=north,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,waterlogged=false,facing=north,]", properties: { facing: "north", half: "top", shape: "straight", waterlogged: "false" } }
+assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,facing=north,waterlogged=false,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,facing=north,waterlogged=false,]", properties: { facing: "north", half: "top", shape: "straight", waterlogged: "false" } }
diff --git a/modules/bs.block/data/bs.block/test/transform/replace_properties.mcfunction b/modules/bs.block/data/bs.block/test/transform/replace_properties.mcfunction
index d3617c75f..9c34465a0 100644
--- a/modules/bs.block/data/bs.block/test/transform/replace_properties.mcfunction
+++ b/modules/bs.block/data/bs.block/test/transform/replace_properties.mcfunction
@@ -17,4 +17,4 @@ setblock ~ ~ ~ minecraft:stone_stairs[facing=west,half=top,shape=straight,waterl
function #bs.block:get_block
function #bs.block:replace_properties {properties:[{name:"facing",value:"north"}]}
-assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,waterlogged=false,facing=north,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,waterlogged=false,facing=north,]", properties: { facing: "north", half: "top", shape: "straight", waterlogged: "false" } }
+assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,facing=north,waterlogged=false,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,facing=north,waterlogged=false,]", properties: { facing: "north", half: "top", shape: "straight", waterlogged: "false" } }
diff --git a/modules/bs.block/data/bs.block/test/transform/replace_type.mcfunction b/modules/bs.block/data/bs.block/test/transform/replace_type.mcfunction
index 6e2f2d663..d0c85c3fb 100644
--- a/modules/bs.block/data/bs.block/test/transform/replace_type.mcfunction
+++ b/modules/bs.block/data/bs.block/test/transform/replace_type.mcfunction
@@ -17,7 +17,7 @@ setblock ~ ~ ~ minecraft:stone_stairs[facing=west,half=top,shape=straight,waterl
function #bs.block:get_block
function #bs.block:replace_type {type:"minecraft:spruce_stairs"}
-assert data storage bs:out block{ block: "minecraft:spruce_stairs[shape=straight,half=top,waterlogged=false,facing=west,]", type: "minecraft:spruce_stairs", state: "[shape=straight,half=top,waterlogged=false,facing=west,]", properties: { facing: "west", half: "top", shape: "straight", waterlogged: "false" } }
+assert data storage bs:out block{ block: "minecraft:spruce_stairs[shape=straight,half=top,facing=west,waterlogged=false,]", type: "minecraft:spruce_stairs", state: "[shape=straight,half=top,facing=west,waterlogged=false,]", properties: { facing: "west", half: "top", shape: "straight", waterlogged: "false" } }
function #bs.block:replace_type {type:"minecraft:stone"}
assert data storage bs:out block{ block: "minecraft:stone", type: "minecraft:stone" }
diff --git a/modules/bs.block/data/bs.block/test/transform/shift_properties.mcfunction b/modules/bs.block/data/bs.block/test/transform/shift_properties.mcfunction
index 8c5e866cd..e6d0b271e 100644
--- a/modules/bs.block/data/bs.block/test/transform/shift_properties.mcfunction
+++ b/modules/bs.block/data/bs.block/test/transform/shift_properties.mcfunction
@@ -17,4 +17,4 @@ setblock ~ ~ ~ minecraft:stone_stairs[facing=west,half=top,shape=straight,waterl
function #bs.block:get_block
function #bs.block:shift_properties {properties:[{name:"facing",by:2}]}
-assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,waterlogged=false,facing=north,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,waterlogged=false,facing=north,]", properties: { facing: "north", half: "top", shape: "straight", waterlogged: "false" } }
+assert data storage bs:out block{ block: "minecraft:stone_stairs[shape=straight,half=top,facing=north,waterlogged=false,]", type: "minecraft:stone_stairs", state: "[shape=straight,half=top,facing=north,waterlogged=false,]", properties: { facing: "north", half: "top", shape: "straight", waterlogged: "false" } }
diff --git a/modules/bs.block/gen_block.py b/modules/bs.block/gen_block.py
index 201f2a848..4f8fd41ba 100644
--- a/modules/bs.block/gen_block.py
+++ b/modules/bs.block/gen_block.py
@@ -1,12 +1,16 @@
+from __future__ import annotations
+
from collections import defaultdict
-from collections.abc import Sequence
+from typing import TYPE_CHECKING
-from beet import Context, Function, LootTable, Predicate
+from beet import Context, Function, LootTable, PackFile, Predicate
-from bookshelf.definitions import MC_VERSIONS
from bookshelf.models import Block, StateNode, StatePredicate, StateProperty, StateValue
from bookshelf.services import minecraft
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Sequence
+
ATTR_TAGS = [
("has_state", lambda b: b.group > 0),
("can_occlude", lambda b: b.can_occlude),
@@ -44,41 +48,47 @@
"sounds",
}
+LOOT_TABLE_INCLUDE = {
+ "type",
+ "item",
+ "group",
+ "sounds", # @deprecated sounds (removed in in 4.0.0)
+}
+
-def beet_default(ctx: Context) -> None:
- """Generate files used by the bs.block module."""
- namespace = ctx.directory.name
- blocks = minecraft.get_blocks(ctx, MC_VERSIONS[-1])
+@minecraft.generator
+def beet_default(ctx: Context, version: str) -> Iterable[tuple[str, PackFile]]:
+ """Generate files used by the module for a given version."""
+ ns = ctx.directory.name
+ blocks = minecraft.get_blocks(ctx.cache, version)
- loot_table = make_block_loot_table(blocks)
- ctx.generate(f"{namespace}:internal/get_type", render=loot_table)
- loot_table = make_block_loot_table(blocks, f"{namespace}:internal")
- ctx.generate(f"{namespace}:internal/get_block", render=loot_table)
+ yield f"{ns}:internal/get_type", make_block_loot_table(blocks)
+ yield f"{ns}:internal/get_block", make_block_loot_table(blocks, f"{ns}:internal")
for state in {s.group: s for b in blocks for s in b.properties}.values():
- loot_table = make_block_state_loot_table(state, f"{namespace}:internal")
- ctx.generate(f"{namespace}:internal/group_{state.group}", render=loot_table)
+ location = f"{ns}:internal/group_{state.group}"
+ yield location, make_block_state_loot_table(state, f"{ns}:internal")
for name, formatter in [
("types", format_types_table),
("items", format_items_table),
("groups", format_groups_table),
]:
- ctx.generate(
- f"{namespace}:import/{name}_table",
- render=Function(source_path=f"{name}_table.jinja"),
- data=formatter(blocks),
- )
+ location = f"{ns}:import/{name}_table"
+ content = ctx.template.render(f"{name}_table.jinja", data=formatter(blocks))
+ yield location, Function(content)
for name, predicate in ATTR_TAGS:
- tag = ctx.data.block_tags.get(f"{namespace}:{name}")
- tag.merge(minecraft.make_block_tag(blocks, predicate))
+ base = ctx.data.block_tags[location := f"{ns}:{name}"]
+ yield location, minecraft.make_block_tag(base, blocks, predicate)
for name, attribute in ATTR_PREDICATES:
groups = defaultdict(list)
for block in blocks:
groups[attribute(block)].append(block)
- merge_attr_predicate(ctx.data.predicates.get(f"{namespace}:{name}"), groups)
+
+ base = ctx.data.predicates[location := f"{ns}:{name}"]
+ yield location, make_attr_predicate(base, groups)
for name, attribute in ATTR_LOOT_TABLES:
seen = set()
@@ -87,11 +97,11 @@ def beet_default(ctx: Context) -> None:
groups[value := attribute(block)].append(block)
if isinstance(value, StatePredicate) and value not in seen:
seen.add(value)
- file = make_attr_state_loot_table(name, value)
- ctx.generate(f"{namespace}:internal/{name}_{value.group}", render=file)
+ location = f"{ns}:internal/{name}_{value.group}"
+ yield location, make_attr_state_loot_table(name, value)
- file = make_attr_loot_table(name, groups, f"{namespace}:internal")
- ctx.generate(f"{namespace}:internal/get_{name}", render=file)
+ location = f"{ns}:internal/get_{name}"
+ yield location, make_attr_loot_table(name, groups, f"{ns}:internal")
def format_types_table(blocks: Sequence[Block]) -> dict:
@@ -120,11 +130,10 @@ def format_groups_table(blocks: Sequence[Block]) -> dict:
def format_block_loot_entry(entry: dict, block: Block) -> dict:
"""Attach block data to a loot entry."""
- # @deprecated sounds (removed in in 4.0.0)
return {**entry, "functions": [{
"function": "set_custom_data",
"tag": minecraft.render_snbt(block.model_dump(
- include={"type", "item", "group", "sounds"},
+ include=LOOT_TABLE_INCLUDE,
)),
}]}
@@ -219,10 +228,10 @@ def make_attr_state_loot_table(attr: str, entry: StatePredicate) -> LootTable:
})
-def merge_attr_predicate(
- predicate: Predicate,
+def make_attr_predicate(
+ base: Predicate,
groups: dict[StateValue[bool], list[Block]],
-) -> None:
+) -> Predicate:
"""Create a predicate for a collection of blocks grouped by attribute."""
def optimize_entry(entry: dict) -> dict | None:
terms = list(filter(None, entry.get("terms", [])))
@@ -251,8 +260,8 @@ def build_node[T: bool](node: StateNode[T] | T) -> dict | None:
return optimize_entry({"condition":"any_of","terms":terms})
- predicate.set_content(optimize_entry({
- **predicate.deserialize(predicate.get_content()),
+ return Predicate({
+ **base.data,
"condition": "any_of",
"terms": [optimize_entry({
"condition": "all_of", "terms":[{
@@ -263,4 +272,4 @@ def build_node[T: bool](node: StateNode[T] | T) -> dict | None:
"condition": "location_check",
"predicate": {"block": {"blocks": [b.type[10:] for b in blocks]}},
} for group, blocks in groups.items() if group],
- }))
+ })
diff --git a/modules/bs.environment/data/bs.environment/function/biome/get_biome.mcfunction b/modules/bs.environment/data/bs.environment/function/biome/get_biome.mcfunction
index 41f321ef5..1ba0ef4f9 100644
--- a/modules/bs.environment/data/bs.environment/function/biome/get_biome.mcfunction
+++ b/modules/bs.environment/data/bs.environment/function/biome/get_biome.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
data remove storage bs:out environment.get_biome
-loot replace entity B5-0-0-0-3 contents loot bs.environment:biome/get
+loot replace entity B5-0-0-0-3 contents loot bs.environment:internal/get_biome
data modify storage bs:out environment.get_biome set from entity B5-0-0-0-3 item.components."minecraft:custom_data"
diff --git a/modules/bs.environment/data/bs.environment/function/temperature/get_temperature.mcfunction b/modules/bs.environment/data/bs.environment/function/temperature/get_temperature.mcfunction
index afd371089..4f771a866 100644
--- a/modules/bs.environment/data/bs.environment/function/temperature/get_temperature.mcfunction
+++ b/modules/bs.environment/data/bs.environment/function/temperature/get_temperature.mcfunction
@@ -13,7 +13,7 @@
# For more details, refer to the MPL v2.0.
# ------------------------------------------------------------------------------------------------------------
-loot replace entity B5-0-0-0-3 contents loot bs.environment:biome/get
+loot replace entity B5-0-0-0-3 contents loot bs.environment:internal/get_biome
execute store result score $environment.celestial_angle.daytime bs.in run data get entity B5-0-0-0-3 item.components."minecraft:custom_data".temperature
execute store result storage bs:ctx y double .00000001 summon minecraft:marker run function bs.environment:temperature/variation
$execute store result score $environment.get_temperature bs.out run return run data get storage bs:ctx y $(scale)
diff --git a/modules/bs.environment/gen_environment.py b/modules/bs.environment/gen_environment.py
index 72db557f9..f1601a03d 100644
--- a/modules/bs.environment/gen_environment.py
+++ b/modules/bs.environment/gen_environment.py
@@ -1,38 +1,49 @@
+from __future__ import annotations
+
from collections import defaultdict
-from collections.abc import Sequence
+from typing import TYPE_CHECKING
-from beet import Context, LootTable, Predicate
+from beet import Context, LootTable, PackFile, Predicate
-from bookshelf.definitions import MC_VERSIONS
-from bookshelf.models import Biome
from bookshelf.services import minecraft
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Sequence
+
+ from bookshelf.models import Biome
+
SNOW_THRESHOLD = 0.4
-def beet_default(ctx: Context) -> None:
+@minecraft.generator
+def beet_default(ctx: Context, version: str) -> Iterable[tuple[str, PackFile]]:
"""Generate files used by the environment module."""
- namespace = ctx.directory.name
- biomes = minecraft.get_biomes(ctx, MC_VERSIONS[-1])
-
- loot_table = make_biome_loot_table(biomes)
- ctx.generate(f"{namespace}:biome/get", render=loot_table)
+ ns = ctx.directory.name
+ biomes = minecraft.get_biomes(ctx.cache, version)
- if predicate := ctx.data.predicates.get(f"{namespace}:can_snow"):
- predicate.data.update(make_can_snow_predicate(biomes).data)
- if predicate := ctx.data.predicates.get(f"{namespace}:has_precipitation"):
- predicate.data.update(make_has_precipitation_predicate(biomes).data)
+ yield f"{ns}:internal/get_biome", make_biome_loot_table(biomes)
+ base = ctx.data.predicates[location := f"{ns}:can_snow"]
+ yield location, make_can_snow_predicate(base, biomes)
+ base = ctx.data.predicates[location := f"{ns}:has_precipitation"]
+ yield location, make_has_precipitation_predicate(base, biomes)
-def make_has_precipitation_predicate(biomes: Sequence[Biome]) -> Predicate:
+def make_has_precipitation_predicate(
+ base: Predicate,
+ biomes: Sequence[Biome],
+) -> Predicate:
"""Create a predicate to determine biomes with precipitation."""
return Predicate({
+ **base.data,
"condition": "minecraft:location_check",
"predicate": {"biomes": [b.type for b in biomes if b.has_precipitation]},
})
-def make_can_snow_predicate(biomes: Sequence[Biome]) -> Predicate:
+def make_can_snow_predicate(
+ base: Predicate,
+ biomes: Sequence[Biome],
+) -> Predicate:
"""Create a predicate to determine where snow can occur."""
groups = defaultdict(list[str])
for biome in filter(
@@ -44,6 +55,7 @@ def make_can_snow_predicate(biomes: Sequence[Biome]) -> Predicate:
groups[str(y if y > 0 else -2147483648)].append(biome.type)
return Predicate({
+ **base.data,
"condition":"minecraft:any_of",
"terms": [{
"condition": "minecraft:location_check",
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/is_gliding.json b/modules/bs.hitbox/1.21.10/data/bs.hitbox/predicate/internal/is_gliding.json
similarity index 100%
rename from modules/bs.hitbox/data/bs.hitbox/predicate/is_gliding.json
rename to modules/bs.hitbox/1.21.10/data/bs.hitbox/predicate/internal/is_gliding.json
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/10.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/10.mcfunction
index bbd70531b..dbf58b1d0 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/10.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/10.mcfunction
@@ -15,6 +15,6 @@
# camel group
execute at @s positioned ~ ~1 ~ if entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:1.7,height:2.375}
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.7,height:0.945}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.7,height:0.945}
execute at @s positioned ~ ~1 ~ if entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:0.765,height:1.06875}
data modify storage bs:out hitbox set value {width:0.765,height:0.42525}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/11.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/11.mcfunction
index db0b41585..5775d4837 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/11.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/11.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# cat_like group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:0.7}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:0.7}
data modify storage bs:out hitbox set value {width:0.3,height:0.35}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/13.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/13.mcfunction
index ba1dedd5c..dbe1fd3d2 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/13.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/13.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# chicken group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.4,height:0.7}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.4,height:0.7}
data modify storage bs:out hitbox set value {width:0.2,height:0.35}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/15.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/15.mcfunction
index 2a0c7788c..edeccfe17 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/15.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/15.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# cow_like group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.4}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.4}
data modify storage bs:out hitbox set value {width:0.45,height:0.7}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/17.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/17.mcfunction
index 53f52bd54..5f1f4662b 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/17.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/17.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# dolphin group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.585,height:0.39}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.585,height:0.39}
data modify storage bs:out hitbox set value {width:0.9,height:0.6}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/18.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/18.mcfunction
index 62312a7ae..605589364 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/18.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/18.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# donkey group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.3964844,height:1.5}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.3964844,height:1.5}
data modify storage bs:out hitbox set value {width:0.6982422,height:0.75}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/25.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/25.mcfunction
index 6fa0b277e..709f68f04 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/25.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/25.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# ghast group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:4.0,height:4.0}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:4.0,height:4.0}
data modify storage bs:out hitbox set value {width:0.95,height:0.95}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/29.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/29.mcfunction
index adc7080d4..8971589da 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/29.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/29.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# hoglin group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.3964844,height:1.4}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.3964844,height:1.4}
data modify storage bs:out hitbox set value {width:0.6982422,height:0.7}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/30.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/30.mcfunction
index 9838da94b..2281e9360 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/30.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/30.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# horse_like group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.3964844,height:1.6}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.3964844,height:1.6}
data modify storage bs:out hitbox set value {width:0.6982422,height:0.8}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/36.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/36.mcfunction
index 4250adda2..26dbe1f2a 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/36.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/36.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# llama group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.87}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.87}
data modify storage bs:out hitbox set value {width:0.45,height:0.935}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/39.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/39.mcfunction
index cb4e7c454..e8ea31297 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/39.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/39.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# panda group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.3,height:1.25}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.3,height:1.25}
data modify storage bs:out hitbox set value {width:0.65,height:0.625}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/41.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/41.mcfunction
index 41c00ac80..320fe15fa 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/41.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/41.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# pig group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:0.9}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:0.9}
data modify storage bs:out hitbox set value {width:0.45,height:0.45}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/42.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/42.mcfunction
index efe685345..aeb43fd2b 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/42.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/42.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# polar_bear group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.4,height:1.4}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.4,height:1.4}
data modify storage bs:out hitbox set value {width:0.7,height:0.7}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/44.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/44.mcfunction
index 83847311c..2b2de7a0d 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/44.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/44.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# rabbit group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.4,height:0.5}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.4,height:0.5}
data modify storage bs:out hitbox set value {width:0.2,height:0.25}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/46.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/46.mcfunction
index 96ee2172c..cefe51ae5 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/46.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/46.mcfunction
@@ -14,6 +14,6 @@
# ------------------------------------------------------------------------------------------------------------
# salmon group
-execute unless predicate bs.hitbox:salmon/large run return run data modify storage bs:out hitbox set value {width:1.05,height:0.6}
-execute unless predicate bs.hitbox:salmon/small run return run data modify storage bs:out hitbox set value {width:0.35,height:0.2}
+execute unless predicate bs.hitbox:internal/salmon/large run return run data modify storage bs:out hitbox set value {width:1.05,height:0.6}
+execute unless predicate bs.hitbox:internal/salmon/small run return run data modify storage bs:out hitbox set value {width:0.35,height:0.2}
data modify storage bs:out hitbox set value {width:0.7,height:0.4}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/47.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/47.mcfunction
index 000ad2708..f8e4d40f0 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/47.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/47.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# sheep group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.3}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.3}
data modify storage bs:out hitbox set value {width:0.45,height:0.65}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/5.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/5.mcfunction
index 666a27eb5..310473320 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/5.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/5.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# axolotl group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.75,height:0.42}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.75,height:0.42}
data modify storage bs:out hitbox set value {width:0.375,height:0.21}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/53.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/53.mcfunction
index 36971aa15..b1aa0a2d0 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/53.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/53.mcfunction
@@ -15,6 +15,6 @@
# sniffer group
execute at @s positioned ~ ~1.5 ~ if entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:1.9,height:1.75}
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.9,height:0.4}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.9,height:0.4}
execute at @s positioned ~ ~.5 ~ if entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:0.95,height:0.875}
data modify storage bs:out hitbox set value {width:0.95,height:0.4}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/56.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/56.mcfunction
index e2ced0b56..19b0e6e1f 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/56.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/56.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# squid group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.4,height:0.4}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.4,height:0.4}
data modify storage bs:out hitbox set value {width:0.8,height:0.8}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/57.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/57.mcfunction
index 6f8fd1c44..e115076dc 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/57.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/57.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# strider group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.7}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.9,height:1.7}
data modify storage bs:out hitbox set value {width:0.45,height:0.85}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/60.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/60.mcfunction
index 7fde717e7..dcbc43487 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/60.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/60.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# turtle group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:1.2,height:0.4}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:1.2,height:0.4}
data modify storage bs:out hitbox set value {width:0.36,height:0.12}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/66.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/66.mcfunction
index dec364e56..96e789c99 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/66.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/66.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# wolf group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:0.85}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:0.85}
data modify storage bs:out hitbox set value {width:0.3,height:0.425}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/67.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/67.mcfunction
index 539c0b187..6048b4443 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/67.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/67.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# zombie_like group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:1.95}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:1.95}
data modify storage bs:out hitbox set value {width:0.3,height:0.975}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/68.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/68.mcfunction
index bed0e5049..bd6d33cc9 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/68.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/68.mcfunction
@@ -15,7 +15,7 @@
# player group
execute if entity @s[gamemode=spectator] run return run data modify storage bs:out hitbox set value {width:0.0,height:0.0}
-execute if predicate bs.hitbox:is_sneaking run return run data modify storage bs:out hitbox set value {width:0.6,height:1.5}
-execute if predicate bs.hitbox:is_swimming run return run data modify storage bs:out hitbox set value {width:0.6,height:0.6}
-execute if predicate bs.hitbox:is_gliding run return run data modify storage bs:out hitbox set value {width:0.6,height:0.6}
+execute if predicate bs.hitbox:internal/is_sneaking run return run data modify storage bs:out hitbox set value {width:0.6,height:1.5}
+execute if predicate bs.hitbox:internal/is_swimming run return run data modify storage bs:out hitbox set value {width:0.6,height:0.6}
+execute if predicate bs.hitbox:internal/is_gliding run return run data modify storage bs:out hitbox set value {width:0.6,height:0.6}
data modify storage bs:out hitbox set value {width:0.6,height:1.8}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/69.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/69.mcfunction
index 99088d884..34c85d1fe 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/69.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/69.mcfunction
@@ -15,5 +15,5 @@
# villager group
execute at @s positioned ~ ~.5 ~ unless entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:0.2,height:0.2}
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:1.95}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.6,height:1.95}
data modify storage bs:out hitbox set value {width:0.3,height:0.975}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/7.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/7.mcfunction
index 991e249c9..d9d147286 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/7.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/7.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# bee group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.7,height:0.6}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.7,height:0.6}
data modify storage bs:out hitbox set value {width:0.35,height:0.3}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/70.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/70.mcfunction
index e7b8ca76d..48dd1f548 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/70.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/70.mcfunction
@@ -15,6 +15,6 @@
# goat group
execute at @s positioned ~ ~1 ~ if entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:0.9,height:1.3}
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.63,height:0.90999997}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.63,height:0.90999997}
execute at @s positioned ~ ~.5 ~ if entity @s[dx=0] run return run data modify storage bs:out hitbox set value {width:0.45,height:0.65}
data modify storage bs:out hitbox set value {width:0.315,height:0.45499998}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/71.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/71.mcfunction
index ca14ff701..b6dd09d15 100644
--- a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/71.mcfunction
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/71.mcfunction
@@ -14,5 +14,5 @@
# ------------------------------------------------------------------------------------------------------------
# armadilo group
-execute unless predicate bs.hitbox:is_baby run return run data modify storage bs:out hitbox set value {width:0.7,height:0.65}
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.7,height:0.65}
data modify storage bs:out hitbox set value {width:0.42,height:0.39}
diff --git a/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/75.mcfunction b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/75.mcfunction
new file mode 100644
index 000000000..b59871629
--- /dev/null
+++ b/modules/bs.hitbox/data/bs.hitbox/function/get_entity/registry/75.mcfunction
@@ -0,0 +1,18 @@
+# ------------------------------------------------------------------------------------------------------------
+# Copyright (c) 2025 Gunivers
+#
+# This file is part of the Bookshelf project (https://github.com/mcbookshelf/bookshelf).
+#
+# This source code is subject to the terms of the Mozilla Public License, v. 2.0.
+# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# Conditions:
+# - You may use this file in compliance with the MPL v2.0
+# - Any modifications must be documented and disclosed under the same license
+#
+# For more details, refer to the MPL v2.0.
+# ------------------------------------------------------------------------------------------------------------
+
+# nautilus group
+execute unless predicate bs.hitbox:internal/is_baby run return run data modify storage bs:out hitbox set value {width:0.875,height:0.95}
+data modify storage bs:out hitbox set value {width:0.4375,height:0.475}
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/is_baby.json b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_baby.json
similarity index 100%
rename from modules/bs.hitbox/data/bs.hitbox/predicate/is_baby.json
rename to modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_baby.json
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_gliding.json b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_gliding.json
new file mode 100644
index 000000000..f4b1b0c66
--- /dev/null
+++ b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_gliding.json
@@ -0,0 +1,9 @@
+{
+ "condition": "minecraft:entity_properties",
+ "entity": "this",
+ "predicate": {
+ "flags": {
+ "is_fall_flying": true
+ }
+ }
+}
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/is_sneaking.json b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_sneaking.json
similarity index 100%
rename from modules/bs.hitbox/data/bs.hitbox/predicate/is_sneaking.json
rename to modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_sneaking.json
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/is_swimming.json b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_swimming.json
similarity index 100%
rename from modules/bs.hitbox/data/bs.hitbox/predicate/is_swimming.json
rename to modules/bs.hitbox/data/bs.hitbox/predicate/internal/is_swimming.json
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/salmon/large.json b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/salmon/large.json
similarity index 100%
rename from modules/bs.hitbox/data/bs.hitbox/predicate/salmon/large.json
rename to modules/bs.hitbox/data/bs.hitbox/predicate/internal/salmon/large.json
diff --git a/modules/bs.hitbox/data/bs.hitbox/predicate/salmon/small.json b/modules/bs.hitbox/data/bs.hitbox/predicate/internal/salmon/small.json
similarity index 100%
rename from modules/bs.hitbox/data/bs.hitbox/predicate/salmon/small.json
rename to modules/bs.hitbox/data/bs.hitbox/predicate/internal/salmon/small.json
diff --git a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/camel.json b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/camel.json
new file mode 100644
index 000000000..d6728ac8e
--- /dev/null
+++ b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/camel.json
@@ -0,0 +1,9 @@
+{
+ "values": [
+ "minecraft:camel",
+ {
+ "id": "minecraft:camel_husk",
+ "required": false
+ }
+ ]
+}
diff --git a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_2.json b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_2.json
index 19ef28f8e..92a352017 100644
--- a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_2.json
+++ b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_2.json
@@ -4,7 +4,7 @@
"minecraft:armor_stand",
"#bs.hitbox:internal/bat_like",
"minecraft:bee",
- "minecraft:camel",
+ "#bs.hitbox:internal/camel",
"#bs.hitbox:internal/cat_like",
"minecraft:cod",
"#bs.hitbox:internal/cow_like",
diff --git a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_8.json b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_8.json
index 06cd55314..181135774 100644
--- a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_8.json
+++ b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/group_8.json
@@ -2,7 +2,7 @@
"values": [
"minecraft:blaze",
"#bs.hitbox:internal/boat",
- "minecraft:camel",
+ "#bs.hitbox:internal/camel",
"#bs.hitbox:internal/cat_like",
"minecraft:cave_spider",
"minecraft:chicken",
diff --git a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/nautilus.json b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/nautilus.json
new file mode 100644
index 000000000..0513f8b5f
--- /dev/null
+++ b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/nautilus.json
@@ -0,0 +1,12 @@
+{
+ "values": [
+ {
+ "id": "minecraft:nautilus",
+ "required": false
+ },
+ {
+ "id": "minecraft:zombie_nautilus",
+ "required": false
+ }
+ ]
+}
diff --git a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/skeleton.json b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/skeleton.json
index 19e61b9d6..3a62cab54 100644
--- a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/skeleton.json
+++ b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/internal/skeleton.json
@@ -2,6 +2,10 @@
"values": [
"minecraft:bogged",
"minecraft:skeleton",
- "minecraft:stray"
+ "minecraft:stray",
+ {
+ "id": "minecraft:parched",
+ "required": false
+ }
]
}
diff --git a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/is_sized.json b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/is_sized.json
index 8d8042df0..9738b3262 100644
--- a/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/is_sized.json
+++ b/modules/bs.hitbox/data/bs.hitbox/tags/entity_type/is_sized.json
@@ -18,6 +18,7 @@
"#bs.hitbox:internal/arrow_like",
"#bs.hitbox:internal/bat_like",
"#bs.hitbox:internal/boat",
+ "#bs.hitbox:internal/camel",
"#bs.hitbox:internal/cat_like",
"#bs.hitbox:internal/cow_like",
"#bs.hitbox:internal/falling_block_like",
@@ -41,7 +42,6 @@
"minecraft:bee",
"minecraft:blaze",
"minecraft:breeze",
- "minecraft:camel",
"minecraft:cave_spider",
"minecraft:chicken",
"minecraft:cod",
diff --git a/modules/bs.hitbox/gen_hitbox.py b/modules/bs.hitbox/gen_hitbox.py
index 3fd6b4e65..4d68b73b1 100644
--- a/modules/bs.hitbox/gen_hitbox.py
+++ b/modules/bs.hitbox/gen_hitbox.py
@@ -1,11 +1,17 @@
-from collections import defaultdict
+from __future__ import annotations
-from beet import Context, LootTable
+from collections import defaultdict
+from typing import TYPE_CHECKING
-from bookshelf.definitions import MC_VERSIONS
from bookshelf.models import Block, StatePredicate, StateValue, VoxelShape
from bookshelf.services import minecraft
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+
+ from beet import Context, LootTable, PackFile
+
+
CUBE = ((0.0, 0.0, 0.0, 16.0, 16.0, 16.0),)
INTANGIBLE = [
"minecraft:light",
@@ -13,10 +19,11 @@
]
-def beet_default(ctx: Context) -> None:
+@minecraft.generator
+def beet_default(ctx: Context, version: str) -> Iterable[tuple[str, PackFile]]:
"""Generate files used by the bs.hitbox module."""
- namespace = ctx.directory.name
- blocks = minecraft.get_blocks(ctx, MC_VERSIONS[-1])
+ ns = ctx.directory.name
+ blocks = minecraft.get_blocks(ctx.cache, version)
groups = {"shape": defaultdict(list), "collision": defaultdict(list)}
seen = set()
@@ -28,12 +35,10 @@ def beet_default(ctx: Context) -> None:
for shape in (block.shape, block.collision_shape):
if isinstance(shape, StatePredicate) and shape.group not in seen:
seen.add(shape.group)
- loot_table = make_loot_table_state(shape)
- ctx.generate(f"{namespace}:block/{shape.group}", render=loot_table)
+ yield f"{ns}:block/{shape.group}", make_loot_table_state(shape)
for name, mapping in groups.items():
- loot_table = make_shape_loot_table(mapping, f"{namespace}:block")
- ctx.generate(f"{namespace}:block/get_{name}", render=loot_table)
+ yield f"{ns}:block/get_{name}", make_shape_loot_table(mapping, f"{ns}:block")
for name, predicate in [
("has_shape_offset", lambda b: b.has_shape_offset),
@@ -44,8 +49,8 @@ def beet_default(ctx: Context) -> None:
("is_waterloggable", lambda b:
any(p.name == "waterlogged" for p in b.properties)),
]:
- if tag := ctx.data.block_tags.get(f"{namespace}:{name}"):
- tag.merge(minecraft.make_block_tag(blocks, predicate))
+ base = ctx.data.block_tags[location := f"{ns}:{name}"]
+ yield location, minecraft.make_block_tag(base, blocks, predicate)
def make_shape_loot_table(
diff --git a/modules/bs.hitbox/module.json b/modules/bs.hitbox/module.json
index 0fe68d5c5..ec2c63e3c 100644
--- a/modules/bs.hitbox/module.json
+++ b/modules/bs.hitbox/module.json
@@ -2,14 +2,23 @@
"extend": "../config.json",
"data_pack": {
"name": "bs.hitbox",
- "load": "."
+ "load": ".",
+ "overlays": [
+ {
+ "directory": "1.21.10",
+ "min_format": 88,
+ "max_format": 88
+ }
+ ]
},
"meta": {
"name": "Hitbox",
"slug": "bookshelf-hitbox",
"description": "Bookshelf module for retrieving and checking the hitboxes of blocks and entities.",
"documentation": "https://docs.mcbookshelf.dev/en/latest/modules/hitbox.html",
- "tags": ["runtime"],
+ "tags": [
+ "runtime"
+ ],
"weak_dependencies": [
"bs.log"
]
diff --git a/modules/bs.position/data/bs.position/function/get/rotation/all.mcfunction b/modules/bs.position/data/bs.position/function/get/rotation/all.mcfunction
index ec630aed5..ee839d076 100644
--- a/modules/bs.position/data/bs.position/function/get/rotation/all.mcfunction
+++ b/modules/bs.position/data/bs.position/function/get/rotation/all.mcfunction
@@ -13,6 +13,7 @@
# For more details, refer to the MPL v2.0.
# ------------------------------------------------------------------------------------------------------------
+execute in minecraft:overworld run tp B5-0-0-0-1 -30000000 0 1600 0 0
execute in minecraft:overworld run tp B5-0-0-0-1 -30000000 0 1600 ~ ~
$execute store result score @s bs.rot.h run data get entity B5-0-0-0-1 Rotation[0] $(scale)
$execute store result score @s bs.rot.v run data get entity B5-0-0-0-1 Rotation[1] $(scale)
diff --git a/modules/bs.position/data/bs.position/function/get/rotation/h.mcfunction b/modules/bs.position/data/bs.position/function/get/rotation/h.mcfunction
index 1c3b859f7..d419e7b43 100644
--- a/modules/bs.position/data/bs.position/function/get/rotation/h.mcfunction
+++ b/modules/bs.position/data/bs.position/function/get/rotation/h.mcfunction
@@ -13,5 +13,6 @@
# For more details, refer to the MPL v2.0.
# ------------------------------------------------------------------------------------------------------------
+execute in minecraft:overworld run tp B5-0-0-0-1 -30000000 0 1600 0 0
execute in minecraft:overworld run tp B5-0-0-0-1 -30000000 0 1600 ~ ~
$execute store result score @s bs.rot.h run data get entity B5-0-0-0-1 Rotation[0] $(scale)
diff --git a/modules/bs.position/data/bs.position/function/get/rotation/v.mcfunction b/modules/bs.position/data/bs.position/function/get/rotation/v.mcfunction
index 2e38c52cc..fe6d6cea2 100644
--- a/modules/bs.position/data/bs.position/function/get/rotation/v.mcfunction
+++ b/modules/bs.position/data/bs.position/function/get/rotation/v.mcfunction
@@ -13,5 +13,6 @@
# For more details, refer to the MPL v2.0.
# ------------------------------------------------------------------------------------------------------------
+execute in minecraft:overworld run tp B5-0-0-0-1 -30000000 0 1600 0 0
execute in minecraft:overworld run tp B5-0-0-0-1 -30000000 0 1600 ~ ~
$execute store result score @s bs.rot.v run data get entity B5-0-0-0-1 Rotation[1] $(scale)
diff --git a/modules/bs.string/gen_string.py b/modules/bs.string/gen_string.py
index 2da9ae9b7..1cf98db09 100644
--- a/modules/bs.string/gen_string.py
+++ b/modules/bs.string/gen_string.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from beet import Context, Function
LOWER_TO_UPPER = {chr(c): chr(c).upper() for c in range(0x110000) if chr(c).islower()}
diff --git a/modules/config.json b/modules/config.json
index ab6395bef..cadfdb3c0 100644
--- a/modules/config.json
+++ b/modules/config.json
@@ -1,5 +1,7 @@
{
"pipeline": [
+ "bookshelf.plugins.setup_templates",
+ "bookshelf.plugins.minify_json",
"bookshelf.plugins.include_deps",
"bookshelf.plugins.update_tags",
"bookshelf.plugins.inject_import",
diff --git a/pyproject.toml b/pyproject.toml
index 531306682..38f758128 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,10 +10,10 @@ authors = [{ name = "Bookshelf Contributors", email = "contact@gunivers.net" }]
license = { text = "MPL-2.0" }
requires-python = ">=3.12"
dependencies = [
- "beet>=0.112.0",
+ "beet>=0.112.2",
"frozendict>=2.4.7",
"orjson>=3.11.4",
- "pydantic>=2.12.4",
+ "pydantic>=2.12.5",
"semver>=3.0.4",
]
@@ -29,7 +29,7 @@ examples = "bookshelf.commands.examples:examples"
[dependency-groups]
dev = [
- "click>=8.3.0",
+ "click>=8.3.1",
"httpx>=0.28.1",
"lectern>=0.34.0",
"platformdirs>=4.5.0",
diff --git a/src/bookshelf/commands/examples.py b/src/bookshelf/commands/examples.py
index 7c250e735..833e06bb5 100644
--- a/src/bookshelf/commands/examples.py
+++ b/src/bookshelf/commands/examples.py
@@ -1,19 +1,15 @@
from __future__ import annotations
from pathlib import Path
-from typing import TYPE_CHECKING
import click
from bookshelf.common.logging import summarize_logs
from bookshelf.common.termui import track
from bookshelf.common.utils import watch_and_run
-from bookshelf.definitions import BUILD_DIR, EXAMPLES, EXAMPLES_DIR, MODULES_DIR
+from bookshelf.definitions import BUILD_DIR, EXAMPLES, EXAMPLES_DIR
from bookshelf.services import builder
-if TYPE_CHECKING:
- from collections.abc import Sequence
-
original_read_text = Path.read_text
def utf8_safe_read_text(
@@ -33,14 +29,15 @@ def examples() -> None:
@examples.command()
-@click.argument("examples", nargs=-1)
+@click.argument("examples", default=EXAMPLES, nargs=-1)
def build(examples: tuple[str, ...]) -> None:
"""Build the specified examples."""
with summarize_logs("🔨 BUILDING EXAMPLES…"):
- build_examples(
- examples or EXAMPLES,
- builder.BuildOptions(output=BUILD_DIR),
- )
+ entries = track((f"Build example [green]{e}", e) for e in examples)
+ builder.ExampleBuilder(
+ require=["bookshelf.plugins.build_pack"],
+ meta={"build": {"output": BUILD_DIR}},
+ ).build(entries)
@examples.command()
@@ -48,19 +45,10 @@ def build(examples: tuple[str, ...]) -> None:
def watch(examples: tuple[str, ...]) -> None:
"""Watch for changes in specified examples and rebuild them."""
with summarize_logs("👀 WATCHING EXAMPLES…"):
- watch_and_run(
- build_examples,
- [MODULES_DIR, EXAMPLES_DIR],
- examples or EXAMPLES,
- builder.BuildOptions(output=BUILD_DIR),
- )
-
-
-def build_examples(examples: Sequence[str], options: builder.BuildOptions) -> None:
- """Run the build for each example, reporting progress to the user."""
- builder.clean_links()
- for build, example in track((
- f"Build example [green]{example}",
- (builder.build_example, example),
- ) for example in examples):
- build(example, options)
+ def run() -> None:
+ entries = track((f"Build example [green]{e}", e) for e in examples)
+ builder.ExampleBuilder(
+ require=["bookshelf.plugins.build_pack"],
+ meta={"build": {"output": BUILD_DIR}},
+ ).build(entries)
+ watch_and_run(run, EXAMPLES_DIR)
diff --git a/src/bookshelf/commands/modules.py b/src/bookshelf/commands/modules.py
index ddd800952..9f288595a 100644
--- a/src/bookshelf/commands/modules.py
+++ b/src/bookshelf/commands/modules.py
@@ -3,19 +3,22 @@
import asyncio
from pathlib import Path
from tempfile import TemporaryDirectory
-from typing import TYPE_CHECKING
import click
from bookshelf.common.logging import summarize_logs
from bookshelf.common.termui import track
from bookshelf.common.utils import watch_and_run
-from bookshelf.definitions import BUILD_DIR, BUNDLES, MC_VERSIONS, MODULES, MODULES_DIR
+from bookshelf.definitions import (
+ BUILD_DIR,
+ BUNDLES,
+ MC_VERSIONS,
+ MODULES,
+ MODULES_DIR,
+ RELEASE_DIR,
+)
from bookshelf.services import builder, packtest, publishers
-if TYPE_CHECKING:
- from collections.abc import Sequence
-
@click.group()
def modules() -> None:
@@ -23,48 +26,44 @@ def modules() -> None:
@modules.command()
-@click.argument("modules", nargs=-1)
+@click.argument("modules", default=MODULES, nargs=-1)
def build(modules: tuple[str, ...]) -> None:
"""Build the specified modules."""
with summarize_logs("🔨 BUILDING MODULES…"):
- build_modules(
- modules or MODULES,
- builder.BuildOptions(
- require=["bookshelf.plugins.include_tests"],
- output=BUILD_DIR,
- ),
- )
+ entries = track((f"Build module [green]{m}", m) for m in modules)
+ builder.ModuleBuilder(
+ require=["bookshelf.plugins.build_pack"],
+ meta={"build": {"output": BUILD_DIR}},
+ ).build(entries)
@modules.command()
-@click.argument("modules", nargs=-1)
+@click.argument("modules", default=MODULES, nargs=-1)
def watch(modules: tuple[str, ...]) -> None:
"""Watch for changes in specified modules and rebuild them."""
with summarize_logs("👀 WATCHING MODULES…"):
- watch_and_run(
- build_modules,
- [MODULES_DIR],
- modules or MODULES,
- builder.BuildOptions(
- require=["bookshelf.plugins.include_tests"],
- output=BUILD_DIR,
- ),
- )
+ def run() -> None:
+ entries = track((f"Build module [green]{m}", m) for m in modules)
+ builder.ModuleBuilder(
+ require=["bookshelf.plugins.build_pack"],
+ meta={"build": {"output": BUILD_DIR}},
+ ).build(entries)
+ watch_and_run(run, MODULES_DIR)
@modules.command()
def release() -> None:
"""Build and release zipped modules."""
- packs = []
with summarize_logs("🔨 BUILDING MODULES…", exit_on_errors=True):
- build_modules([*BUNDLES, *MODULES], builder.BuildOptions(
- zipped=True,
+ packs = []
+ entries = track((f"Build module [green]{m}", m) for m in [*BUNDLES, *MODULES])
+ builder.ModuleBuilder(
require=["bookshelf.plugins.release_pack"],
- meta={
- "autosave": {"link": False},
- "publish": lambda spec: packs.append(spec),
- },
- ))
+ meta={"release": {
+ "output": RELEASE_DIR,
+ "enqueue": lambda spec: packs.append(spec),
+ }, "versions": MC_VERSIONS},
+ ).build(entries)
with summarize_logs("🚀 PUBLISHING MODULES…", exit_on_errors=True):
for publish in track([
@@ -75,46 +74,36 @@ def release() -> None:
@modules.command()
-@click.argument("modules", nargs=-1)
+@click.argument("modules", default=MODULES, nargs=-1)
@click.option("--versions", is_flag=True)
def test(modules: tuple[str, ...], *, versions: bool) -> None:
"""Build and test modules."""
with TemporaryDirectory() as directory:
+ output = Path(directory)
+
with summarize_logs("🔨 BUILDING MODULES…", exit_on_errors=True):
- build_modules(
- modules or MODULES,
- builder.BuildOptions(
- require=["bookshelf.plugins.include_tests"],
- meta={"autosave": {"link": False}},
- output=(output := Path(directory)),
- zipped=True,
- ),
- )
+ entries = track((f"Build module [green]{m}", m) for m in modules)
+ builder.ModuleBuilder(
+ require=["bookshelf.plugins.build_pack"],
+ meta={"build": {
+ "output": output,
+ "link": False,
+ }, "versions": MC_VERSIONS},
+ zipped=True,
+ ).build(entries)
with summarize_logs("🔬 TESTING MODULES…", exit_on_errors=True):
- mc_versions = reversed(MC_VERSIONS) if versions else MC_VERSIONS[-1:]
- asyncio.run(test_modules(output, list(mc_versions)))
-
-
-def build_modules(modules: Sequence[str], options: builder.BuildOptions) -> None:
- """Run the build for each module, reporting progress to the user."""
- builder.clean_links()
- for build, module in track((
- f"Build module [green]{module}",
- (builder.build_module, module),
- ) for module in modules):
- build(module, options)
-
-
-async def test_modules(datapacks: Path, mc_versions: list[str]) -> None:
- """Run tests for the given datapacks on specified Minecraft versions."""
- coros = [packtest.run(datapacks, v) for v in mc_versions]
- tasks = [asyncio.create_task(coro) for coro in coros]
-
- for task in track(
- (f"Test version [green]{v}[/green]", process)
- for v, process in zip(mc_versions, tasks, strict=True)
- ):
- logs = await task
- for event in logs:
- event.log()
+ async def test_modules() -> None:
+ vers = list(reversed(MC_VERSIONS)) if versions else MC_VERSIONS[-1:]
+ coros = [packtest.run(output, v) for v in vers]
+ tasks = [asyncio.create_task(coro) for coro in coros]
+
+ for task in track(
+ (f"Test version [green]{v}[/green]", t)
+ for v, t in zip(vers, tasks, strict=True)
+ ):
+ logs = await task
+ for event in logs:
+ event.log()
+
+ asyncio.run(test_modules())
diff --git a/src/bookshelf/common/json.py b/src/bookshelf/common/json.py
index 1d95b5881..863b13e57 100644
--- a/src/bookshelf/common/json.py
+++ b/src/bookshelf/common/json.py
@@ -1,16 +1,13 @@
from __future__ import annotations
from functools import cache
-from typing import TYPE_CHECKING
+from pathlib import Path
import orjson
from pydantic import BaseModel
from bookshelf.common import errors, utils
-if TYPE_CHECKING:
- from pathlib import Path
-
def dump(file: Path, data: BaseModel | dict | list, indent: int | None = 2) -> int:
"""Write data to a JSON file using orjson."""
@@ -19,9 +16,14 @@ def dump(file: Path, data: BaseModel | dict | list, indent: int | None = 2) -> i
return file.write_bytes(contents)
-def load[T: BaseModel | dict | list](file: Path, expected_type: type[T]) -> T:
- """Load a JSON file and return its content."""
- data = _parse_json(file)
+def load[T: BaseModel | dict | list](obj: bytes | Path, expected_type: type[T]) -> T:
+ """Load JSON from bytes or a file path and return its content."""
+ if isinstance(obj, Path):
+ data = _parse_file(obj)
+ file = obj
+ else:
+ data = _parse_bytes(obj)
+ file = None
if isinstance(data, expected_type):
return data
if isinstance(expected_type, type) and issubclass(expected_type, BaseModel):
@@ -35,11 +37,16 @@ def _orjson_default(obj: object) -> object:
raise TypeError
+def _parse_bytes(data: bytes) -> dict | list:
+ try:
+ return orjson.loads(data)
+ except orjson.JSONDecodeError as e:
+ raise errors.JSONDecodeError(message=str(e), line=e.lineno) from e
+
@cache
-def _parse_json(file: Path) -> dict | list:
+def _parse_file(file: Path) -> dict | list:
try:
- raw = file.read_bytes()
- return orjson.loads(raw)
+ return orjson.loads(file.read_bytes())
except FileNotFoundError as e:
raise errors.JSONFileNotFoundError(file=file) from e
except orjson.JSONDecodeError as e:
diff --git a/src/bookshelf/common/utils.py b/src/bookshelf/common/utils.py
index e122b5edd..9a0e81deb 100644
--- a/src/bookshelf/common/utils.py
+++ b/src/bookshelf/common/utils.py
@@ -14,7 +14,7 @@
from bookshelf.definitions import GITHUB_REPO, ROOT_DIR, VERSION
if TYPE_CHECKING:
- from collections.abc import Callable, Sequence
+ from collections.abc import Callable
def locate_command(command: str) -> str:
@@ -32,11 +32,7 @@ def raw_github_url(file: Path, version: str = VERSION) -> str:
return f"https://raw.githubusercontent.com/{GITHUB_REPO}/refs/tags/v{version}/{slug}"
-def validate_model[T: BaseModel](
- file: Path,
- model: type[T],
- obj: object,
-) -> T:
+def validate_model[T: BaseModel](file: Path | None, model: type[T], obj: object) -> T:
"""Validate a Model with custom errors."""
try:
return model.model_validate(obj)
@@ -54,15 +50,10 @@ def validate_model[T: BaseModel](
raise errors.BookshelfCompositeError(message, errs) from e
-def watch_and_run[**P, R](
- run: Callable[P, R],
- paths: Sequence[Path],
- *args: P.args,
- **kwargs: P.kwargs,
-) -> None:
+def watch_and_run(run: Callable[[], None], *paths: Path) -> None:
"""Run a callable once, then re-run it on file changes in the given paths."""
console = Console()
- run(*args, **kwargs)
+ run()
for changes in watch(*paths):
with error_handler(format_padding=1):
count = len(changes)
@@ -72,4 +63,4 @@ def watch_and_run[**P, R](
f"{count} change{'s' if count != 1 else ''} detected "
f"[bright_black]({', '.join(filenames)})[/bright_black]",
)
- run(*args, **kwargs)
+ run()
diff --git a/src/bookshelf/models/collection.py b/src/bookshelf/models/collection.py
index 98f49546f..bec6ee840 100644
--- a/src/bookshelf/models/collection.py
+++ b/src/bookshelf/models/collection.py
@@ -21,3 +21,7 @@ def __iter__(self) -> Iterator[T]:
def __len__(self) -> int:
"""Return the number of models in the collection."""
return len(self.root)
+
+ def __hash__(self) -> int:
+ """Hash based on the collection items."""
+ return hash(tuple(self.root))
diff --git a/src/bookshelf/models/minecraft.py b/src/bookshelf/models/minecraft.py
index 743ec2a46..520bfafc6 100644
--- a/src/bookshelf/models/minecraft.py
+++ b/src/bookshelf/models/minecraft.py
@@ -27,6 +27,10 @@ class Biome(BaseModel, frozen=True):
temperature: float
has_precipitation: bool = True
+ def __hash__(self) -> int:
+ """Hash based on the biome ID."""
+ return hash(self.type)
+
class Block(BaseModel, frozen=True):
"""Represents a Minecraft block."""
@@ -52,6 +56,10 @@ class Block(BaseModel, frozen=True):
is_conductive: StateValue[bool] = False
is_spawnable: StateValue[bool] = False
+ def __hash__(self) -> int:
+ """Hash based on the block ID."""
+ return hash(self.type)
+
class StatePredicate[T](BaseModel, frozen=True):
"""Predicate over a block's state."""
diff --git a/src/bookshelf/plugins/build_pack.py b/src/bookshelf/plugins/build_pack.py
new file mode 100644
index 000000000..7d1273b22
--- /dev/null
+++ b/src/bookshelf/plugins/build_pack.py
@@ -0,0 +1,38 @@
+
+from __future__ import annotations
+
+from pathlib import Path # noqa: TC003
+from typing import TYPE_CHECKING
+
+from beet import Context, PluginOptions, configurable
+from beet.contrib.link import LinkManager
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+
+class BuildOptions(PluginOptions):
+ """Options for building packs."""
+
+ output: Path
+ tests: bool = True
+ link: bool = True
+
+
+@configurable("build", validator=BuildOptions)
+def beet_default(ctx: Context, opts: BuildOptions) -> Generator:
+ """Plugin that outputs the data pack and the resource pack in a local directory."""
+ if opts.tests:
+ ctx.require("bookshelf.plugins.include_tests")
+ yield
+
+ link = ctx.inject(LinkManager)
+ for pack, link_directory in zip(
+ ctx.packs,
+ [link.resource_pack, link.data_pack],
+ strict=True,
+ ):
+ if pack:
+ pack.save(opts.output, overwrite=True)
+ if opts.link and link_directory:
+ link.dirty.append(str(pack.save(link_directory, overwrite=True)))
diff --git a/src/bookshelf/plugins/gen_help.py b/src/bookshelf/plugins/gen_help.py
index 8b1527697..36ae6e017 100644
--- a/src/bookshelf/plugins/gen_help.py
+++ b/src/bookshelf/plugins/gen_help.py
@@ -5,7 +5,6 @@
def beet_default(ctx: Context) -> None:
"""Generate a __help__ function with its tag for the current module."""
- ctx.require("bookshelf.plugins.setup_templates")
with ctx.override(generate_namespace=ctx.directory.name):
ctx.generate(
"__help__",
diff --git a/src/bookshelf/plugins/gen_load.py b/src/bookshelf/plugins/gen_load.py
index 0f3ab5b1d..1a665a527 100644
--- a/src/bookshelf/plugins/gen_load.py
+++ b/src/bookshelf/plugins/gen_load.py
@@ -1,14 +1,18 @@
from __future__ import annotations
-from beet import Context, Function, FunctionTag
+import re
+
+from beet import Context, Function, FunctionTag, TestEnvironment
from semver import Version
from bookshelf.definitions import MODULES, VERSION
+from bookshelf.plugins.include_tests import TestFunction
+
+PERSISTENT_ENTITY_UUID = re.compile(r"B5-0-0-0-\d+")
def beet_default(ctx: Context) -> None:
"""Generate load-related functions and tags for the current module."""
- ctx.require("bookshelf.plugins.setup_templates")
version = Version.parse(VERSION)
module = ctx.directory.name[3:]
@@ -51,6 +55,36 @@ def beet_default(ctx: Context) -> None:
ctx.meta.get("weak_dependencies", []) or [],
)
+ environment = f"# @environment bs.load:{module}\n"
+ # Render header once and compute offset for inserting environment tag
+ header = ctx.template.render("bookshelf/header.jinja")
+ offset = len(header)
+ count = 0
+
+ # Insert environment tag after header for all test files
+ for _, file in ctx.data.all(extend=TestFunction):
+ count += 1
+ file.set_content(f"{file.text[:offset]}{environment}{file.text[offset:]}")
+
+ if count > 0:
+ load = ctx.data.functions.get(f"{ctx.data.name}:__load__")
+ load_content = load.get_content() if load is not None else []
+ entities = {m for i in load_content for m in PERSISTENT_ENTITY_UUID.findall(i)}
+ # Create a function that loads the test module with some setup commands
+ ctx.data.functions[f"bs.load:v{VERSION}/test/{module}"] = Function([
+ header,
+ "function #bs.load:unload",
+ f"function #bs.load:module/{module}",
+ "",
+ "forceload add 0 0",
+ *(f"await entity {entity}" for entity in entities),
+ ])
+ # Register the test environment to run the setup function
+ ctx.data.test_environments[f"bs.load:{module}"] = TestEnvironment({
+ "type": "minecraft:function",
+ "setup": f"bs.load:v{VERSION}/test/{module}",
+ })
+
def gen_load_tag(modules: list[str]) -> FunctionTag:
"""Generate a tag to load all modules."""
diff --git a/src/bookshelf/plugins/include_tests.py b/src/bookshelf/plugins/include_tests.py
index 3f19be587..0a9755c71 100644
--- a/src/bookshelf/plugins/include_tests.py
+++ b/src/bookshelf/plugins/include_tests.py
@@ -1,66 +1,17 @@
from __future__ import annotations
-import re
-from typing import TYPE_CHECKING, ClassVar
+from typing import ClassVar
-from beet import (
- Context,
- Function,
- JsonFile,
- NamespaceFile,
- NamespaceFileScope,
- TestEnvironment,
-)
+from beet import Context, NamespaceFile, NamespaceFileScope, TextFileBase
-from bookshelf.definitions import VERSION
-if TYPE_CHECKING:
- from collections.abc import Generator
-
-
-PERSISTENT_ENTITY_UUID = re.compile(r"B5-0-0-0-\d+")
-
-
-class TestFunction(JsonFile, NamespaceFile):
+class TestFunction(TextFileBase, NamespaceFile):
"""Represents a PackTest Minecraft function file."""
scope: ClassVar[NamespaceFileScope] = ("test",)
extension: ClassVar[str] = ".mcfunction"
-def beet_default(ctx: Context) -> Generator:
+def beet_default(ctx: Context) -> None:
"""Include test functions from the test folder."""
- ctx.require("bookshelf.plugins.setup_templates")
ctx.data.extend_namespace.append(TestFunction)
- yield
-
- module = ctx.directory.name[3:]
- environment = f"# @environment bs.load:{module}\n"
- # Render header once and compute offset for inserting environment tag
- header = ctx.template.render("bookshelf/header.jinja")
- offset = len(header)
- count = 0
-
- # Insert environment tag after header for all test files
- for _, file in ctx.data.all(extend=TestFunction):
- count += 1
- file.set_content(f"{file.text[:offset]}{environment}{file.text[offset:]}")
-
- if count > 0:
- load = ctx.data.functions.get(f"{ctx.data.name}:__load__")
- load_content = load.get_content() if load is not None else []
- entities = {m for i in load_content for m in PERSISTENT_ENTITY_UUID.findall(i)}
- # Create a function that loads the test module with some setup commands
- ctx.data.functions[f"bs.load:v{VERSION}/test/{module}"] = Function([
- header,
- "function #bs.load:unload",
- f"function #bs.load:module/{module}",
- "",
- "forceload add 0 0",
- *(f"await entity {entity}" for entity in entities),
- ])
- # Register the test environment to run the setup function
- ctx.data.test_environments[f"bs.load:{module}"] = TestEnvironment({
- "type": "minecraft:function",
- "setup": f"bs.load:v{VERSION}/test/{module}",
- })
diff --git a/src/bookshelf/plugins/inject_logger.py b/src/bookshelf/plugins/inject_logger.py
index a364fe803..8e553460c 100644
--- a/src/bookshelf/plugins/inject_logger.py
+++ b/src/bookshelf/plugins/inject_logger.py
@@ -10,7 +10,6 @@
def beet_default(ctx: Context) -> Generator:
"""Inject a command to register the module in the bookshelf log namespace."""
- ctx.require("bookshelf.plugins.setup_templates")
yield
# Only add if the module depends on bs.log to avoid extra noise
if "bs.log" in [
diff --git a/src/bookshelf/plugins/make_bundle.py b/src/bookshelf/plugins/make_bundle.py
index a12549cec..53a613a71 100644
--- a/src/bookshelf/plugins/make_bundle.py
+++ b/src/bookshelf/plugins/make_bundle.py
@@ -4,7 +4,7 @@
from functools import cache
from typing import TYPE_CHECKING
-from beet import Context, subproject
+from beet import Context, PngFile, subproject
from bookshelf.definitions import MODULES, MODULES_DIR
@@ -17,8 +17,12 @@
def beet_default(ctx: Context) -> None:
"""Include all modules."""
for mod in MODULES:
- config = {"directory": f"{MODULES_DIR}/{mod}", "extend": "module.json"}
- ctx.require(subproject(config))
+ ctx.require(subproject({
+ "directory": f"{MODULES_DIR}/{mod}",
+ "extend": "module.json",
+ "meta": ctx.meta,
+ }))
+ ctx.data.icon = PngFile(source_path=ctx.directory / "pack.png")
@cache
@@ -28,6 +32,10 @@ def plugin(ctx: Context) -> None:
for mod in MODULES:
meta = json.loads((MODULES_DIR / mod / "module.json").read_text("utf-8"))
if tag in meta.get("meta", {}).get("tags", []):
- config = {"directory": f"{MODULES_DIR}/{mod}", "extend": "module.json"}
- ctx.require(subproject(config))
+ ctx.require(subproject({
+ "directory": f"{MODULES_DIR}/{mod}",
+ "extend": "module.json",
+ "meta": ctx.meta,
+ }))
+ ctx.data.icon = PngFile(source_path=ctx.directory / "pack.png")
return plugin
diff --git a/src/bookshelf/plugins/minify_json.py b/src/bookshelf/plugins/minify_json.py
new file mode 100644
index 000000000..017489190
--- /dev/null
+++ b/src/bookshelf/plugins/minify_json.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import orjson
+from beet import JsonFileBase
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+ from beet import Context
+
+
+def beet_default(ctx: Context) -> Generator:
+ """Configure beet for building Bookshelf modules."""
+ yield
+ for pack in ctx.packs:
+ for _, file in pack.list_files(extend=JsonFileBase):
+ file.encoder = lambda obj: orjson.dumps(obj).decode()
+ file.decoder = lambda obj: orjson.loads(obj.encode())
+ file.ensure_serialized()
diff --git a/src/bookshelf/plugins/release_pack.py b/src/bookshelf/plugins/release_pack.py
index 3e0c62b38..d03a85a02 100644
--- a/src/bookshelf/plugins/release_pack.py
+++ b/src/bookshelf/plugins/release_pack.py
@@ -1,42 +1,53 @@
from __future__ import annotations
import re
+from collections.abc import Callable # noqa: TC003
+from pathlib import Path # noqa: TC003
from typing import TYPE_CHECKING
-from bookshelf.definitions import DOC_DIR, MC_VERSIONS, RELEASE_DIR, VERSION
+from beet import Context, PluginOptions, configurable
+
+from bookshelf.definitions import DOC_DIR, MC_VERSIONS, VERSION
from bookshelf.services.publishers import PublishSpec
if TYPE_CHECKING:
from collections.abc import Generator
- from beet import Context
+class ReleaseOptions(PluginOptions):
+ """Options for the release pack plugin."""
+
+ output: Path
+ enqueue: Callable[[PublishSpec], None]
-def beet_default(ctx: Context) -> Generator:
+
+@configurable("release", validator=ReleaseOptions)
+def beet_default(ctx: Context, opts: ReleaseOptions) -> Generator:
"""Build pack to output then attempt to publish them to platforms."""
yield
- if pack := ctx.data or ctx.assets:
+ for pack in list(filter(None, ctx.packs)):
pack.name = f"{pack.name}-{MC_VERSIONS[-1]}-v{VERSION}"
- file = pack.save(RELEASE_DIR, overwrite=True)
+ file = pack.save(opts.output, overwrite=True, zipped=True)
- publish = ctx.meta["publish"]
- publish(PublishSpec(
+ opts.enqueue(PublishSpec(
file=file,
name=f"Bookshelf {ctx.meta.get('name', '')}".strip(),
kind="datapack" if ctx.data else "resourcepack",
slug=ctx.meta["slug"],
icon=ctx.directory / "pack.png",
readme=ctx.directory / "README.md",
- changelog=create_specialized_changelog(ctx.directory.name),
+ changelog=make_changelog(ctx.directory.name),
description=ctx.meta.get("description", ""),
documentation=ctx.meta.get("documentation", ""),
))
-def create_specialized_changelog(module: str) -> str:
+def make_changelog(module: str) -> str:
"""Create a changelog specific to the given module."""
- changelog = (DOC_DIR / f"changelog/v{VERSION}.md").read_text("utf-8")
+ file = DOC_DIR / f"changelog/v{VERSION}.md"
+ changelog = file.read_text("utf-8")
sections = re.split(r"(###.*?)$", changelog, flags=re.MULTILINE)
+
return sections.pop(0) + "".join(
"".join(sections[i:i+2])
for i in range(0, len(sections) - 1, 2)
diff --git a/src/bookshelf/plugins/update_mcmeta.py b/src/bookshelf/plugins/update_mcmeta.py
index de577a242..a8faf6e72 100644
--- a/src/bookshelf/plugins/update_mcmeta.py
+++ b/src/bookshelf/plugins/update_mcmeta.py
@@ -14,35 +14,29 @@
VERSION_META = "https://raw.githubusercontent.com/misode/mcmeta/refs/tags/{}-summary/version.json"
-class VersionRange(TypedDict):
- """Range of supported pack format versions."""
-
- min_inclusive: int
- max_inclusive: int
-
-
class SupportedFormats(TypedDict):
"""Supported pack formats for data and assets."""
- data: VersionRange
- assets: VersionRange
+ data: int
+ assets: int
def beet_default(ctx: Context) -> Generator:
"""Set the pack format for the module based on supported Minecraft versions."""
yield
# Retrieve supported data and asset pack formats based on MC versions
- formats = get_supported_formats(ctx, MC_VERSIONS)
+ min_formats = get_supported_formats(ctx, MC_VERSIONS[0])
+ max_formats = get_supported_formats(ctx, MC_VERSIONS[-1])
# Set metadata for resource pack
- ctx.assets.description = ctx.meta["description"]
- ctx.assets.min_format = formats["assets"]["min_inclusive"]
- ctx.assets.max_format = formats["assets"]["max_inclusive"]
+ ctx.assets.description = ctx.meta.get("description")
+ ctx.assets.min_format = min_formats["assets"]
+ ctx.assets.max_format = max_formats["assets"]
# Set metadata for data pack
- ctx.data.description = ctx.meta["description"]
- ctx.data.min_format = formats["data"]["min_inclusive"]
- ctx.data.max_format = formats["data"]["max_inclusive"]
+ ctx.data.description = ctx.meta.get("description")
+ ctx.data.min_format = min_formats["data"]
+ ctx.data.max_format = max_formats["data"]
# Ensure the mcmeta file includes the data pack id (Smithed convention)
mcmeta = ctx.data.mcmeta
@@ -52,25 +46,13 @@ def beet_default(ctx: Context) -> Generator:
})
-def get_supported_formats(ctx: Context, versions: list[str]) -> SupportedFormats:
- """Retrieve the supported formats for the given Minecraft versions."""
- # Download min version data
- cache = ctx.cache[f"version/{versions[0]}"]
- file = cache.download(VERSION_META.format(versions[0]))
- min_version = json.load(file, dict)
-
- # Download max version data
- cache = ctx.cache[f"version/{versions[-1]}"]
- file = cache.download(VERSION_META.format(versions[-1]))
- max_version = json.load(file, dict)
+def get_supported_formats(ctx: Context, version: str) -> SupportedFormats:
+ """Retrieve the supported formats for the given Minecraft version."""
+ cache = ctx.cache[f"version/{version}"]
+ file = cache.download(VERSION_META.format(version))
+ versions = json.load(file, dict)
return SupportedFormats(
- data=VersionRange(
- min_inclusive=min_version["data_pack_version"],
- max_inclusive=max_version["data_pack_version"],
- ),
- assets=VersionRange(
- min_inclusive=min_version["resource_pack_version"],
- max_inclusive=max_version["resource_pack_version"],
- ),
+ data=versions["data_pack_version"],
+ assets=versions["resource_pack_version"],
)
diff --git a/src/bookshelf/services/builder.py b/src/bookshelf/services/builder.py
index a327cccda..828ccd555 100644
--- a/src/bookshelf/services/builder.py
+++ b/src/bookshelf/services/builder.py
@@ -1,65 +1,75 @@
-# ruff: noqa: TC003
from __future__ import annotations
-from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING, Any
-from beet import PackConfig, Project, ProjectBuilder, ProjectConfig
-from beet.contrib.link import LinkManager
+from beet import Context, PackConfig, Project, ProjectBuilder, ProjectConfig
from pydantic import BaseModel, Field
from bookshelf.definitions import EXAMPLES_DIR, MODULES_DIR, ROOT_DIR
+if TYPE_CHECKING:
+ from collections.abc import Iterable
-class BuildOptions(BaseModel):
- """Configuration controlling how the build process operates."""
- pipeline: list[str] = Field(default_factory=list)
+class BaseBuilder(BaseModel):
+ """Base class for build process implementations."""
+
require: list[str] = Field(default_factory=list)
+ pipeline: list[str] = Field(default_factory=list)
meta: dict[str, Any] = Field(default_factory=dict)
- output: Path | None = None
zipped: bool = False
+ def run(self, ctx: Context, entries: Iterable[str]) -> None:
+ """Run the Beet build pipeline for multiple entries."""
+ raise NotImplementedError
+
+ def build(self, entries: Iterable[str]) -> None:
+ """Run the builder for multiple entries."""
+ project = Project(ProjectConfig().resolve(ROOT_DIR))
+ with ProjectBuilder(project=project, root=True).build() as ctx:
+ self.run(ctx, entries)
+
+ def make_pack_config(self, *, zipped: bool) -> PackConfig:
+ """Generate a Beet pack configuration with optional compression."""
+ return PackConfig(
+ compression="deflate",
+ compression_level=9,
+ zipped=True,
+ ) if zipped else PackConfig()
+
+
+class ModuleBuilder(BaseBuilder):
+ """Builder for modules."""
+
+ def run(self, _: Context, modules: Iterable[str]) -> None:
+ """Run the Beet build pipeline the given modules."""
+ for module in modules:
+ with ProjectBuilder(Project(ProjectConfig(
+ extend="module.json",
+ data_pack=self.make_pack_config(zipped=self.zipped),
+ resource_pack=self.make_pack_config(zipped=self.zipped),
+ require=[*self.require, "bookshelf.plugins.update_mcmeta"],
+ pipeline=self.pipeline,
+ meta={**self.meta, "autosave": {"link": False}},
+ ).resolve(MODULES_DIR / module))).build():
+ pass
+
+
+class ExampleBuilder(BaseBuilder):
+ """Builder for examples."""
-def clean_links() -> None:
- """Remove the previously linked files and folders."""
- project = Project(ProjectConfig().resolve(ROOT_DIR))
- LinkManager(project.cache).clean()
-
-
-def make_pack_config(*, zipped: bool) -> PackConfig:
- """Generate a Beet pack configuration with optional compression."""
- return PackConfig(
- compression="deflate",
- compression_level=9,
- zipped=True,
- ) if zipped else PackConfig()
-
-
-def build_module(module: str, options: BuildOptions) -> None:
- """Run the Beet build pipeline for a single module."""
- with ProjectBuilder(Project(ProjectConfig(
- extend="module.json", # type: ignore[arg-type]
- broadcast=[MODULES_DIR / module], # type: ignore[arg-type]
- data_pack=make_pack_config(zipped=options.zipped),
- resource_pack=make_pack_config(zipped=options.zipped),
- pipeline=[*options.pipeline],
- require=[*options.require, "bookshelf.plugins.update_mcmeta"],
- meta=options.meta,
- output=options.output,
- ).resolve(ROOT_DIR)), root=False).build():
- pass
-
-
-def build_example(example: str, options: BuildOptions) -> None:
- """Run the Beet build pipeline for a single example."""
- with ProjectBuilder(Project(ProjectConfig(
- broadcast=[EXAMPLES_DIR / f"{example}.md"], # type: ignore[arg-type]
- data_pack=make_pack_config(zipped=options.zipped),
- resource_pack=make_pack_config(zipped=options.zipped),
- pipeline=[*options.pipeline, "lectern"],
- require=[*options.require, "lectern.contrib.require"],
- meta={**options.meta, "lectern": {"load": "."}},
- output=options.output,
- ).resolve(ROOT_DIR)), root=False).build():
- pass
+ def run(self, _: Context, examples: Iterable[str]) -> None:
+ """Run the Beet build pipeline for a single example."""
+ for example in examples:
+ with ProjectBuilder(Project(ProjectConfig(
+ data_pack=self.make_pack_config(zipped=self.zipped),
+ resource_pack=self.make_pack_config(zipped=self.zipped),
+ require=["lectern.contrib.require", *self.require],
+ pipeline=["lectern", *self.pipeline],
+ meta={
+ **self.meta,
+ "autosave": {"link": False},
+ "lectern": {"load": "."},
+ },
+ ).resolve(EXAMPLES_DIR / f"{example}.md"))).build():
+ pass
diff --git a/src/bookshelf/services/minecraft/__init__.py b/src/bookshelf/services/minecraft/__init__.py
index b1ecbb11c..a693ac186 100644
--- a/src/bookshelf/services/minecraft/__init__.py
+++ b/src/bookshelf/services/minecraft/__init__.py
@@ -5,18 +5,18 @@
from .biome import get_biomes
from .block import get_blocks
from .utils import (
+ generator,
make_block_tag,
- make_loot_table,
make_loot_table_binary,
make_loot_table_state,
render_snbt,
)
__all__ = [
+ "generator",
"get_biomes",
"get_blocks",
"make_block_tag",
- "make_loot_table",
"make_loot_table_binary",
"make_loot_table_state",
"render_snbt",
diff --git a/src/bookshelf/services/minecraft/biome.py b/src/bookshelf/services/minecraft/biome.py
index f252f999e..2e98850e0 100644
--- a/src/bookshelf/services/minecraft/biome.py
+++ b/src/bookshelf/services/minecraft/biome.py
@@ -1,23 +1,21 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+import httpx
from bookshelf.common import json
from bookshelf.models import Biome, Collection
from .utils import cache_version
-if TYPE_CHECKING:
- from beet import Context
-
BIOMES_URL = "https://raw.githubusercontent.com/misode/mcmeta/{}-summary/data/worldgen/biome/data.min.json"
@cache_version("biomes", Collection[Biome])
-def get_biomes(ctx: Context, version: str) -> Collection[Biome]:
+def get_biomes(version: str) -> Collection[Biome]:
"""Retrieve biomes for the provided version."""
- cache = ctx.cache[f"version/{version}"]
- raw = json.load(cache.download(BIOMES_URL.format(version)), dict)
+ response = httpx.get(BIOMES_URL.format(version))
+ response.raise_for_status()
+ raw = json.load(response.content, dict)
return Collection(root=[Biome(
type=f"minecraft:{name}",
diff --git a/src/bookshelf/services/minecraft/block.py b/src/bookshelf/services/minecraft/block.py
index f82d6a045..8e502f316 100644
--- a/src/bookshelf/services/minecraft/block.py
+++ b/src/bookshelf/services/minecraft/block.py
@@ -6,6 +6,7 @@
from logging import getLogger
from typing import TYPE_CHECKING, Any
+import httpx
from frozendict import frozendict
from bookshelf.common import json
@@ -24,18 +25,17 @@
if TYPE_CHECKING:
from collections.abc import Callable
- from beet import Context
-
logger = getLogger(__name__)
BLOCKS_URL = "https://raw.githubusercontent.com/mcbookshelf/mcdata/refs/tags/v1/{}/blocks/data.min.json"
@cache_version("blocks", Collection[Block])
-def get_blocks(ctx: Context, version: str) -> Collection[Block]:
+def get_blocks(version: str) -> Collection[Block]:
"""Retrieve blocks for the provided version."""
- cache = ctx.cache[f"version/{version}"]
- raw = json.load(cache.download(BLOCKS_URL.format(version)), dict)
+ response = httpx.get(BLOCKS_URL.format(version))
+ response.raise_for_status()
+ raw = json.load(response.content, dict)
blocks: list[dict[str, Any]] = []
groups: dict[tuple[tuple[str, tuple[str, ...]], ...], int] = {(): 0}
@@ -61,6 +61,7 @@ def get_blocks(ctx: Context, version: str) -> Collection[Block]:
"is_spawnable": _make_state_value("is_spawnable", states),
})
+ blocks.sort(key=lambda b: b["type"])
return Collection(root=_make_blocks(blocks))
diff --git a/src/bookshelf/services/minecraft/utils.py b/src/bookshelf/services/minecraft/utils.py
index 4c15cdca4..d9a9a908b 100644
--- a/src/bookshelf/services/minecraft/utils.py
+++ b/src/bookshelf/services/minecraft/utils.py
@@ -3,18 +3,21 @@
import re
from collections.abc import Callable, Mapping, Sequence
from functools import singledispatch, wraps
-from itertools import chain
from typing import TYPE_CHECKING
import orjson
-from beet import BlockTag, LootTable
+from beet import BlockTag, Context, JsonFileBase, LootTable, PackFile
from pydantic import BaseModel
from bookshelf.common import json
+from bookshelf.definitions import MC_VERSIONS
from bookshelf.models import Block, StateNode, StatePredicate
+from bookshelf.plugins.update_mcmeta import get_supported_formats
if TYPE_CHECKING:
- from beet import Context
+ from collections.abc import Iterable
+
+ from beet import ProjectCache
def quote_snbt_key(k: str) -> str:
@@ -77,35 +80,53 @@ def render_snbt_mapping(obj: Mapping) -> str:
def cache_version[T: BaseModel | dict | list](
key: str,
expected_type: type[T],
-) -> Callable[[Callable[[Context, str], T]], Callable[[Context, str], T]]:
- """Cache functions that accept a ctx and a version to a JSON file."""
- def decorator(func: Callable[[Context, str], T]) -> Callable[[Context, str], T]:
+) -> Callable[[Callable[[str], T]], Callable[[ProjectCache, str], T]]:
+ """Cache functions that accept a version to a JSON file."""
+ def decorator(func: Callable[[str], T]) -> Callable[[ProjectCache, str], T]:
@wraps(func)
- def wrapper(ctx: Context, version: str) -> T:
- file = ctx.cache[f"version/{version}"].get_path(key).with_suffix(".json")
+ def wrapper(cache: ProjectCache, version: str) -> T:
+ file = cache[f"version/{version}"].get_path(key).with_suffix(".json")
if file.is_file():
return json.load(file, expected_type)
- json.dump(file, result := func(ctx, version), None)
+ json.dump(file, result := func(version), None)
return result
return wrapper
return decorator
+def generator(
+ func: Callable[[Context, str], Iterable[tuple[str, PackFile]]],
+) -> Callable[[Context], None]:
+ """Wrap a beet generator with overlay/compatibility logic."""
+ def decorator(ctx: Context) -> None:
+ seen = {}
+ for version in reversed(ctx.meta.get("versions", MC_VERSIONS[-1:])):
+ for location, file in func(ctx, version):
+ if isinstance(file, JsonFileBase):
+ file.encoder = lambda obj: orjson.dumps(obj).decode()
+ file.decoder = lambda obj: orjson.loads(obj.encode())
+ if location not in seen:
+ seen[location] = file.ensure_serialized()
+ ctx.data[location] = file
+ elif seen[location] != file.ensure_serialized():
+ seen[location] = file.ensure_serialized()
+ overlay = ctx.data.overlays[version]
+ min_formats = get_supported_formats(ctx, MC_VERSIONS[0])
+ max_formats = get_supported_formats(ctx, version)
+ overlay.max_format = max_formats["data"]
+ overlay.min_format = min_formats["data"]
+ overlay[location] = file
+ return decorator
+
+
def make_block_tag(
+ base: BlockTag,
blocks: Sequence[Block],
predicate: Callable[[Block], bool],
- extras: list | None = None,
) -> BlockTag:
- """Create a block tag for blocks that match the predicate."""
- values = chain(extras or [], (block.type for block in blocks if predicate(block)))
- return BlockTag({"replace":True,"values":sorted(values)})
-
-
-def make_loot_table(content: dict) -> LootTable:
- """Build an optimized loot table from a dict using orjson."""
- loot_table = LootTable(content)
- loot_table.text = orjson.dumps(loot_table.data).decode("utf-8")
- return loot_table
+ """Create or update a block tag for blocks that match the predicate."""
+ values = sorted(block.type for block in blocks if predicate(block))
+ return BlockTag({**base.data, "values": values})
def make_loot_table_binary[T](
@@ -133,7 +154,7 @@ def build_node(entries: Sequence[T]) -> dict:
right.pop("conditions")
return {"type":"alternatives","children":[left, right]}
- return make_loot_table({"pools":[{"rolls":1,"entries":[build_node(entries)]}]})
+ return LootTable({"pools":[{"rolls":1,"entries":[build_node(entries)]}]})
def make_loot_table_state[T](
@@ -159,4 +180,4 @@ def build_node(node: StateNode[T] | T) -> dict:
return {"type":"alternatives","children":children}
- return make_loot_table({"pools":[{"rolls":1,"entries":[build_node(entry.tree)]}]})
+ return LootTable({"pools":[{"rolls":1,"entries":[build_node(entry.tree)]}]})
diff --git a/src/bookshelf/services/publishers/utils.py b/src/bookshelf/services/publishers/utils.py
index e7ba22a87..b7534658c 100644
--- a/src/bookshelf/services/publishers/utils.py
+++ b/src/bookshelf/services/publishers/utils.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from logging import getLogger
-from pathlib import Path
+from pathlib import Path # noqa: TC003
from typing import TYPE_CHECKING, Literal
from pydantic import BaseModel
diff --git a/uv.lock b/uv.lock
index 501f4a934..1575c2be2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -70,7 +70,7 @@ wheels = [
[[package]]
name = "beet"
-version = "0.112.0"
+version = "0.112.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -84,9 +84,9 @@ dependencies = [
{ name = "toml" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1f/e9/2c890be2f96a4a05fe57b77dcfbf7de05a594ba6b9bd08493b4f31291800/beet-0.112.0.tar.gz", hash = "sha256:9732467adf7f33bc99077f58209dd41bd6e6fa18e13f3ae900f0be5994fcc86f", size = 93119, upload-time = "2025-10-15T22:16:49.766Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/bf/a7de57393f9d962dd93d5397c1025bdfe0d840e2df12dbf387da61468862/beet-0.112.2.tar.gz", hash = "sha256:fb007adb29df53a2300678cc9b79fc2922b36c685e2077f5832d740ec7fcb4c2", size = 93524, upload-time = "2025-12-01T22:39:24.142Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/24/cd/a08fdd765d848943cd2608aaa86cd5fad6300c4256aac654e144d3f9ea31/beet-0.112.0-py3-none-any.whl", hash = "sha256:3768c0f619ae8df1444067a0b869ea93b38d667dfb6b895e237e081971e94e36", size = 117871, upload-time = "2025-10-15T22:16:48.195Z" },
+ { url = "https://files.pythonhosted.org/packages/86/83/c5d5b0574f82e2ce3af7616d3385862b98b6f94d8dcd894cef062f9a9b01/beet-0.112.2-py3-none-any.whl", hash = "sha256:270e98fb16ee7a28900d6544ae96d35b1a957ff9bd9077f7478aee58edcbfc85", size = 118341, upload-time = "2025-12-01T22:39:22.971Z" },
]
[[package]]
@@ -157,14 +157,14 @@ wheels = [
[[package]]
name = "click"
-version = "8.3.0"
+version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
@@ -417,16 +417,16 @@ docs = [
[package.metadata]
requires-dist = [
- { name = "beet", specifier = ">=0.112.0" },
+ { name = "beet", specifier = ">=0.112.2" },
{ name = "frozendict", specifier = ">=2.4.7" },
{ name = "orjson", specifier = ">=3.11.4" },
- { name = "pydantic", specifier = ">=2.12.4" },
+ { name = "pydantic", specifier = ">=2.12.5" },
{ name = "semver", specifier = ">=3.0.4" },
]
[package.metadata.requires-dev]
dev = [
- { name = "click", specifier = ">=8.3.0" },
+ { name = "click", specifier = ">=8.3.1" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "lectern", specifier = ">=0.34.0" },
{ name = "platformdirs", specifier = ">=4.5.0" },
@@ -640,7 +640,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.12.4"
+version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -648,9 +648,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]