diff --git a/core/definitions.py b/core/definitions.py
index 940499b622..9ec84f7805 100644
--- a/core/definitions.py
+++ b/core/definitions.py
@@ -29,6 +29,7 @@
BIOMES_URL = "https://raw.githubusercontent.com/misode/mcmeta/{}-summary/data/worldgen/biome/data.min.json"
BLOCKS_URL = "https://raw.githubusercontent.com/misode/mcmeta/{}-summary/blocks/data.min.json"
ITEMS_URL = "https://raw.githubusercontent.com/misode/mcmeta/{}-registries/item/data.min.json"
+SOUNDS_URL = "https://raw.githubusercontent.com/mcbookshelf/Bookshelf-McData/refs/tags/{}/blocks/sounds.min.json"
SHAPES_URL = "https://raw.githubusercontent.com/mcbookshelf/Bookshelf-McData/refs/tags/{}/blocks/shapes.min.json"
VERSION_URL = "https://raw.githubusercontent.com/misode/mcmeta/refs/tags/{}-summary/version.json"
diff --git a/docs/_templates/changelog/v3.0.0.md b/docs/_templates/changelog/v3.0.0.md
index 016e775097..2b8ba43b01 100644
--- a/docs/_templates/changelog/v3.0.0.md
+++ b/docs/_templates/changelog/v3.0.0.md
@@ -20,6 +20,11 @@
- ⚙️ **[#285](https://github.com/mcbookshelf/Bookshelf/pull/285)** - Added new workflows with automatic deployment to **Modrinth** and **Smithed**.
+### `🧱 bs.block`
+
+- ✨ **[#279](https://github.com/mcbookshelf/Bookshelf/issues/279)** - Added a `#bs.block:play_block_sound` tag for playing block sounds.
+
+
### `🎯 bs.hitbox`
- ⚠️ **[#297](https://github.com/mcbookshelf/Bookshelf/issues/297)** - Renamed block tag `is_composite` to `not_full_cube` for better clarity.
diff --git a/docs/modules/block.md b/docs/modules/block.md
index dd5a5456c9..7913f605fe 100644
--- a/docs/modules/block.md
+++ b/docs/modules/block.md
@@ -183,6 +183,12 @@ Get all data related to the block at the current location, including its state a
- {nbt}`string` **state**: Represent the state of a block (e.g., `[shape=straight]`).
- {nbt}`compound` **nbt**: Data tags used by block entities or an empty string.
- {nbt}`compound` **properties**: Block state as properties (used by entities like falling blocks).
+ - {nbt}`compound` **sounds**: The sound list of a block.
+ - {nbt}`string` **break**: The sound played when a player break the block.
+ - {nbt}`string` **hit**: The sound played when a player hit the block.
+ - {nbt}`string` **fall**: The sound played when a player fall on the block.
+ - {nbt}`string` **place**: The sound played when a player place the block.
+ - {nbt}`string` **step**: The sound played when a player step on the block.
:::
```
@@ -217,6 +223,12 @@ Get the block type at the current location. Although states, NBTs, and propertie
- {nbt}`string` **state**: Represent the state of a block **[empty string]**.
- {nbt}`compound` **nbt**: Data tags used by block entities **[empty string]**.
- {nbt}`compound` **properties**: Block state as properties **[empty compound]**.
+ - {nbt}`compound` **sounds**: The sound list of a block.
+ - {nbt}`string` **break**: The sound played when a player break the block.
+ - {nbt}`string` **hit**: The sound played when a player hit the block.
+ - {nbt}`string` **fall**: The sound played when a player fall on the block.
+ - {nbt}`string` **place**: The sound played when a player place the block.
+ - {nbt}`string` **step**: The sound played when a player step on the block.
:::
```
@@ -893,6 +905,46 @@ data modify storage bs:in block.emit_block_particle merge value { delta: "0 0 0"
function #bs.block:emit_block_particle
```
+::::
+::::{tab-item} Block Sound
+
+```{function} #bs.block:play_block_sound
+
+Play a block sound of the given block.
+
+:Inputs:
+ **Execution `at ` or `positioned `**: Position where the sound will be played.
+
+ **Storage `bs:in block.play_block_sound`**:
+ :::{treeview}
+ - {nbt}`compound` Block sound data
+ - {nbt}`string` **sound**: The sound to play. Usually took from the `sounds` property of the virtual block (cf get functions).
+ - {nbt}`string` **source**: The source of the sound. Similar to the /playsound command.
+ - {nbt}`string` **targets**: The targets of the sound. Similar to the /playsound command.
+ - {nbt}`string` **pos**: X Y Z coordinates, the position of the sound. Similar to the /playsound command.
+ - {nbt}`int` **volume**: Volume of the sound. Similar to the /playsound command.
+ - {nbt}`int` **pitch**: Pitch of the sound. Similar to the /playsound command.
+ - {nbt}`int` **min_volume**: Minimum volume of the sound. Similar to the /playsound command.
+ :::
+
+:Outputs:
+ **State**: The sound is played.
+```
+
+*Play the sound of the block at 0 0 0:*
+
+```mcfunction
+# Get block data
+execute positioned 0 0 0 run function #bs.block:get_block
+
+# Setup the input
+data modify storage bs:in block.play_block_sound set value { source: "block", targets: "@s", pos: "~ ~ ~", volume: 1, pitch: 1, min_volume: 0 }
+data modify storage bs:in block.play_block_sound.sound set from storage bs:out block.sounds.break
+
+# Play the block sound
+function #bs.block:play_block_sound
+```
+
::::
:::::
diff --git a/meta/manifest.json b/meta/manifest.json
index 3a0b2cf32d..b13bcaa04a 100644
--- a/meta/manifest.json
+++ b/meta/manifest.json
@@ -345,6 +345,21 @@
"minecraft_version": "1.20.6"
}
},
+ {
+ "id": "#bs.block:play_block_sound",
+ "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/block.html#produce",
+ "authors": [
+ "theogiraudet"
+ ],
+ "created": {
+ "date": "2025/01/25",
+ "minecraft_version": "1.21.4"
+ },
+ "updated": {
+ "date": "2025/01/25",
+ "minecraft_version": "1.21.4"
+ }
+ },
{
"id": "#bs.block:remove_properties",
"documentation": "https://docs.mcbookshelf.dev/en/latest/modules/block.html#manage-state",
diff --git a/modules/bs.block/data/bs.block/function/produce/block_sound/play_block_sound.mcfunction b/modules/bs.block/data/bs.block/function/produce/block_sound/play_block_sound.mcfunction
new file mode 100644
index 0000000000..a2f3515adb
--- /dev/null
+++ b/modules/bs.block/data/bs.block/function/produce/block_sound/play_block_sound.mcfunction
@@ -0,0 +1,16 @@
+# ------------------------------------------------------------------------------------------------------------
+# 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.
+# ------------------------------------------------------------------------------------------------------------
+
+function bs.block:produce/block_sound/run with storage bs:in block.play_block_sound
diff --git a/modules/bs.block/data/bs.block/function/produce/block_sound/run.mcfunction b/modules/bs.block/data/bs.block/function/produce/block_sound/run.mcfunction
new file mode 100644
index 0000000000..832eb2c456
--- /dev/null
+++ b/modules/bs.block/data/bs.block/function/produce/block_sound/run.mcfunction
@@ -0,0 +1,16 @@
+# ------------------------------------------------------------------------------------------------------------
+# 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.
+# ------------------------------------------------------------------------------------------------------------
+
+$playsound $(sound) $(source) $(targets) $(pos) $(volume) $(pitch) $(min_volume)
diff --git a/modules/bs.block/data/bs.block/tags/function/play_block_sound.json b/modules/bs.block/data/bs.block/tags/function/play_block_sound.json
new file mode 100644
index 0000000000..df5d64e10b
--- /dev/null
+++ b/modules/bs.block/data/bs.block/tags/function/play_block_sound.json
@@ -0,0 +1,20 @@
+{
+ "__bookshelf__": {
+ "feature": true,
+ "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/block.html#produce",
+ "authors": [
+ "theogiraudet"
+ ],
+ "created": {
+ "date": "2025/01/25",
+ "minecraft_version": "1.21.4"
+ },
+ "updated": {
+ "date": "2025/01/25",
+ "minecraft_version": "1.21.4"
+ }
+ },
+ "values": [
+ "bs.block:produce/block_sound/play_block_sound"
+ ]
+}
diff --git a/modules/bs.block/gen_blocks.py b/modules/bs.block/gen_blocks.py
index d7c068a154..af5172a3c4 100644
--- a/modules/bs.block/gen_blocks.py
+++ b/modules/bs.block/gen_blocks.py
@@ -5,6 +5,7 @@
import numpy as np
from beet import BlockTag, Context, Function, LootTable
+from core.common.logger import log_step
from pydantic import BaseModel
from core.common.helpers import (
@@ -19,6 +20,7 @@
BLOCKS_URL,
ITEMS_URL,
MINECRAFT_VERSIONS,
+ SOUNDS_URL,
SPECIAL_ITEMS,
)
@@ -26,6 +28,7 @@
type StatesDict = dict[str, list[str]]
type StatesTuple = tuple[tuple[str, tuple[str, ...]], ...]
type RawBlocks = dict[str, tuple[StatesDict, StrDict]]
+type RawSounds = dict[str, dict[str, str]]
class Block(BaseModel):
"""Represents a Minecraft block."""
@@ -33,6 +36,7 @@ class Block(BaseModel):
type: str
item: str | None
group: int
+ sounds: dict[str, str]
states: list["State"]
class State(BaseModel):
@@ -95,25 +99,32 @@ def get_optimized_blocks() -> list:
error_msg = f"Expected a list, but got {type(raw_items)}"
raise TypeError(error_msg)
+ raw_sounds = download_and_parse_json(cache, SOUNDS_URL.format(version))
+ if not isinstance(raw_sounds, dict):
+ error_msg = f"Expected a dict, but got {type(raw_sounds)}"
+ raise TypeError(error_msg)
+
items = {
with_prefix(item): with_prefix(item)
for item in raw_items
} | SPECIAL_ITEMS
- return group_and_optimize_blocks(raw_blocks, items)
+ return group_and_optimize_blocks(raw_blocks, items, raw_sounds)
return [Block.model_validate(data) for data in get_optimized_blocks()]
-def group_and_optimize_blocks(raw_blocks: RawBlocks, items: StrDict) -> list:
+def group_and_optimize_blocks(raw_blocks: RawBlocks, items: StrDict, raw_sounds: RawSounds) -> list:
"""Group blocks and optimizes block state sequences."""
blocks: list[dict] = []
groups: dict[StatesTuple, int] = {(): 0}
for block, (states, properties) in raw_blocks.items():
ordered_states = reorder_states_options(states, properties)
+ prefixed_block = with_prefix(block)
insort(blocks, {
- "type": with_prefix(block),
- "item": items.get(with_prefix(block)),
+ "type": prefixed_block,
+ "item": items.get(prefixed_block),
+ "sounds": raw_sounds.get(prefixed_block, {}),
"group": groups.setdefault(ordered_states, len(groups)),
}, key=lambda x: x["group"])