Beet Minecraft Pack Development Kit
repository·main·Indexed 43 days ago
https://github.com/mcbeet/beetA development kit for Minecraft that unifies resource pack and data pack tooling into a single pipeline. It includes a Python library for programmatic pack manipulation, a CLI toolchain for building and watching projects, and integrated tools like Lectern for converting markdown or plain text into packs, and Bolt for generating complex Minecraft commands using Python logic.
What's inside Beet
- Beet is a Minecraft pack development kit designed to unify the tooling for resource packs and data packs into a single pipeline. It acts as an authoring layer that sits above the standard Minecraft distribution formats, allowing developers to manage complex file structures and integrate various pre-processors, frameworks, and generators seamlessly. This helps avoid the manual management of hundreds of files typically required in large-scale Minecraft pack development.
Supercharge Minecraft commands with Python
mainBolt allows you to define Minecraft NBT data structures as Python dictionaries and use Python logic (loops, functions, f-strings) to generate and execute complex Minecraft commands. This enables programmatic command generation, such as summoning armies of entities with specific attributes.
infinite_invisibility = { Id: 14, Duration: 999999, Amplifier: 1, ShowParticles: false, } def summon_chicken_army(n): for i in range(n): summon chicken ~i ~ ~ { Tags: [f"quack{i}"], IsChickenJockey: true, Passengers: [{ id: zombie, IsBaby: true, ActiveEffects: [infinite_invisibility] }] } say Go forth, my minions! summon_chicken_army(16)Access files using namespaced locations (Proxies)
mainInstead of navigating the full hierarchy, you can use proxies on the
DataPackorResourcePackobject to access files using their namespaced string identifiers (e.g.,namespace:path). These proxies are always in sync with the underlying namespace objects.Proxy attributes are named after the file type (e.g.,
.functions,.textures).from beet import DataPack, Function pack = DataPack() # Using namespaced strings directly on the pack pack.functions["demo:foo"] = Function() # This is equivalent to: # pack["demo"].functions["foo"]Use the yield statement for plugin exit phases
mainPlugins in the
beetpipeline operate in two phases: an entry phase and an exit phase. This is because plugins are conceptually wrapped around each other like layers.By using the
yieldstatement, a plugin can pause its execution, allow all dependent plugins (those itrequire()d) to run, and then resume to perform cleanup or finalization tasks.Execution Flow:
- Plugin enters (code before
yield). - Plugin yields (pauses).
- Dependent plugins run to completion.
- Plugin resumes (code after
yield).
This is useful for aggregating data from multiple plugins before writing a final file (e.g., merging multiple language entries into one
.jsonfile).def add_translations_plugin(ctx: Context): # [Entry Phase] Initialize shared state ctx.meta["translations"] = {} yield # <--- Wait for all other plugins to call 'set_translation' # [Exit Phase] Finalize and write files for code, data in ctx.meta["translations"].items(): ctx.assets["minecraft:lang"].merge(data)- Plugin enters (code before
Implement Service Injection with ctx.inject()
mainFor complex logic, instead of using arbitrary metadata, you can package functionality into a Service. Services are instantiated via
ctx.inject()and live for the duration of the pipeline.Pattern for creating a service:
- Define a class that accepts
ctx: Contextin its__init__. - In
__init__, usectx.require()to register a plugin method that handles the service's finalization (usingyield). - Use
ctx.inject(YourServiceClass)within plugins to access the service's API.
This provides a strongly-typed, explicit way to share state and logic across plugins.
from beet import Context class Internationalization: def __init__(self, ctx: Context): self.languages = {} # Register the cleanup plugin automatically ctx.require(self.add_translations) def add_translations(self, ctx: Context): yield # Finalize: merge all collected languages into assets ctx.assets["minecraft"].languages.merge(self.languages) def set(self, key: str, **kwargs: str): # API for other plugins to use for code, message in kwargs.items(): self.languages[code][key] = message # Usage in a plugin: def my_plugin(ctx: Context): i18n = ctx.inject(Internationalization) i18n.set("hello", en_us="hello", fr_fr="bonjour")- Define a class that accepts
Use the beet pipeline for automated builds
mainThe
beetpipeline acts as a "conveyor belt" for your data packs and resource packs. You can write or use plugins to operate on the output of the pipeline. This allows you to:- Transform Data: Apply changes to the pack contents automatically.
- Integrate Frameworks: Run external tools (like
sandstone) as part of the build process to bundle generated content into your final package. - Automate Repetitive Tasks: Replace custom scripts with formal plugins that analyze or generate pack components.
By using
beet, you can achieve a unified build system where different tools (likemechaorlectern) interoperate by using the data pack format as a common language.Understand the Lectern font data structure
mainThe Lectern snapshot format represents Minecraft font data using a JSON structure containing multiple font definitions. Each definition includes:
- Unicode Pages: A list of character strings representing the glyphs on a specific unicode page.
- Bitmaps: Objects defining a bitmap font, including:
type: Set to"bitmap".file: The resource path to the image (e.g.,"minecraft:font/ascii.png").ascent: The vertical offset for the font.chars: A 2D array of characters representing the pixel data or glyph mapping.
- Legacy Unicode: A definition for legacy unicode support, specifying:
type: Set to"legacy_unicode".sizes: The path to the binary file containing glyph sizes (e.g.,"minecraft:font/glyph_sizes.bin").template: A string template for locating unicode page images (e.g.,"minecraft:font/unicode_page_%s.png").
{ "type": "bitmap", "file": "minecraft:font/ascii.png", "ascent": 7, "chars": [ "\u0000\u0000...", "\u0020\u0021..." ] }Understand the beet development model
mainUnlike traditional Minecraft development where you edit files directly in your
.minecraftfolder,beetintroduces a formal build step. You treat your project as a collection of source files (data packs and resource packs) that are processed through a pipeline and output to a destination (like a development world or a distribution folder).This model provides several advantages:
- Version Control: Keep your data packs and resource packs side-by-side in a single repository.
- Automated Releases: Separate your project sources from the final published assets.
- Live Reloading: Use the
beet watch --reloadcommand to benefit from live data pack reloading during development. - Extensibility: Use a plugin-based pipeline to automate transformations, analysis, and generation.
Understand the Data Pack and Resource Pack object hierarchy
mainIn
beet,DataPackandResourcePackobjects are dict-like primitives that map directly to the filesystem structure. The hierarchy follows this pattern:- Pack: The top-level object (e.g.,
DataPack). It contains namespaces. - Namespace: A dict-like object within the pack (e.g.,
pack["demo"]). Namespaces are created automatically. - Namespace Container: A dict-like object within a namespace that groups files by type (e.g.,
namespace.functions). - File Instance: The leaf nodes representing actual files (e.g.,
Function,Texture).
You can access containers via attribute access (e.g.,
.functions) or type-based indexing (e.g.,[Function]). Files can also be added directly to a namespace, and the namespace will automatically dispatch them to the correct container based on their type.from beet import DataPack, Function pack = DataPack() # Accessing via namespace and type demo_namespace = pack["demo"] demo_functions = demo_namespace[Function] # Adding a file (automatic dispatch to container) demo_functions["foo"] = Function(["say hello"]) # Equivalent attribute access demo_namespace.functions["foo"]- Pack: The top-level object (e.g.,
Understand the beet toolchain and plugin system
mainThe
beettoolchain is a build system for Minecraft data packs and resource packs. Instead of writing one-off scripts, you implement logic as plugins.Each plugin is a callable Python object that accepts a
Contextobject. The toolchain initializes aContextwith an empty data pack and an empty resource pack, then executes the plugins defined in your configuration sequentially (or as a dependency graph).Workflow:
- Define plugins as Python functions taking
ctx: Context. - Configure the build in a
beet.jsonfile. - Run the
beetcommand to execute the pipeline and output the generated packs.
{ "name": "greeting", "output": "out", "pipeline": ["my_plugins.add_greeting"] }- Define plugins as Python functions taking
Understand nested @script directives
mainThe
@scriptdirective supports nesting. You can nest directives by increasing the number of@symbols prefixing the directive name.@scriptis the top-level directive.@@scriptis a second-level nested directive.@@@scriptis a third-level nested directive.
Note: While nesting is supported, deep nesting (e.g.,
@@@@function) is possible but should generally be avoided for clarity.@script @@function demo:script_nested say wow @script @@script @@@script @@@@function demo:script_please_avoid_this say noUse modifiers in Lectern directives
mainDirectives can be adjusted using modifiers specified in parentheses immediately after the directive name.
Content Modification Modifiers
append: Concatenates the fragment content to existing content (available for all text-based namespaced resource directives).prepend: Adds the fragment content before the existing content.merge: Uses thebeetmerging strategy to combine the fragment with the existing file (useful for JSON files likeminecraft:load).base64: Decodes the content of the code fragment from base64.download: Used with block fragments to download remote files from a URL.strip_final_newline: Removes the final newline at the end of code block fragments.
Syntax
@<directive_name>(<modifier>) <arg1> <arg2> ...@function(append) tutorial:greeting say This is added afterwards. @function(prepend) tutorial:greeting say This is added before. @function_tag(merge) minecraft:load { "values": ["#tutorial:something_else"] } @function_tag(base64) tutorial:something_else ewogICJ2YWx1ZXMiOiBbInR1dG9yaWFsOnN0cmlwcGVkIl0KfQ== @function_tag(download) tutorial:from_github https://raw.githubusercontent.com/mcbeet/beet/main/examples/load_basic/src/data/demo/functions/foo.mcfunction @function(strip_final_newline) tutorial:stripped say This function doesn't have a final newline.