Skip to content

Prior art: how other servers and engines turn Minecraft into data plus components ​

Researched 2026-10-24 against the source of 24 projects at pinned commits (shallow clones under /tmp/bds-research-priorart/, read only, nothing built) and against public documentation for Mojang's Java server and the Bedrock Script API. This note continues Getting vanilla Bedrock data from Mojang's own files; section 6 there profiles Allay, Dragonfly, PowerNukkitX, Endstone and LeviLamina at a high level and is not repeated here. Every claim names a file and line or a URL. Nothing here is legal advice.

Summary ​

  • Every project separates identity data from behavior code. The vanilla palette (names, states, hashes) is always data, loaded from a dump or a generator; what a block does is always code. The projects differ only in how they attach that code: a subclass per block (Nukkit, PowerNukkitX, cuberite, this server), a behavior type per family bound to many definitions (Mojang Java, Pumpkin, Dragonfly, Minestom), or a component list per definition (Allay, CloudburstMC/Server, Mojang Bedrock itself).
  • Pumpkin is the only reimplementation that evaluates Mojang's loot, recipe and worldgen JSON directly. Its density functions are compiled from the datapack at codegen time, its loot evaluator handles about 30 condition and function types, and it checks generated chunks against 29 vanilla chunk dumps block by block. That works because Java Edition publishes all of it; Bedrock publishes none of the terrain.
  • Allay's component system is the closest Bedrock precedent: a block type is a list of component singletons wired by reflection, with one base component supplying the lifecycle hooks through direct calls and extra components reacting to ten block events on a per-object event bus. 1477 block types are covered by 366 behavior classes and 173 base component implementations; 1035 are assigned by hand and the rest by a generated null-guarded initializer.
  • CloudburstMC/Server has quietly moved to the same shape with less machinery: 65 ComponentType keys such as USE, GET_LOOT and ON_NEIGHBOUR_CHANGED bound to stateless lambdas per block type, with hardness, friction, light and flammability read per state from data/blocks.json.
  • Dragonfly shows the cost side of interface-per-behavior in a language without reflection: 44 block interfaces, roughly 50 type assertions in player.go alone, and a registry Finalize that flattens the hot properties into a uint16 per runtime id so the tick loop never asserts.
  • Mojang's own extension points define the hook vocabulary a Bedrock server should expose: 15 BlockCustomComponent hooks, 7 ItemCustomComponent hooks, dispatch "in the order they are listed", and "a component on a permutation overrides the component on the base block". Entities extend through triggerEvent and entity properties, not script hooks.
  • No Bedrock server loads behavior packs server-side. Allay, PowerNukkitX and Dragonfly forward packs to clients. Pumpkin loads Java datapacks from <world>/datapacks with embedded content first and disk overriding by id, and reads pack_format without validating it.
  • The PrismarineJS stack is a working TypeScript precedent for data-first objects (a Block is a record copied from minecraft-data plus functions) and for booting BDS in CI (bedrock-protocol boots 19 BDS versions per CI run through minecraft-bedrock-server). Its Bedrock game data stops at 1.26.30, and that file's harvestTools and drops ids resolve to nothing in its own 1.26.30 item table.
  • The recommendation is pattern (c), definitions plus component lists keyed by Mojang's component names, with Allay's split between one base behavior per archetype and additional components, Dragonfly's load-time flattening for hot paths, and explicit hook interfaces instead of reflection. Reusable under MIT: deepslate's registry and holder shape, Dragonfly's hashing and registration checks, minecraft-bedrock-server for CI, Blockception and bridge. schemas for pack validation. Allay, Pumpkin, PowerNukkitX and Cloudburst are copyleft and serve as references only.

1. Project profiles ​

Each profile: language and license, commit read, how vanilla data enters, representation, behavior attachment, loot and recipes and worldgen, pack loading, parity checks, plugin API.

1.1 Pumpkin (Pumpkin-MC/Pumpkin) ​

Rust, GPL-3.0, cd9e98c966919541fe951d78dfdf8d3401e10112, targets Java 26.3. Crates live under crates/.

  • Data enters two ways. Extractor JSON is committed in assets/ (68 MB: blocks.json 7.7 MB, items.json, entities.json, properties.json, ...), produced by a separate Fabric mod (AGENTS.md:54). assets/NOTICE.md:7-11 marks it "(c) Mojang Studios / Microsoft Corporation ... provided solely for compatibility, server emulation, and interoperability under the terms of the Minecraft EULA" and adds "Vanilla Minecraft data packs and structures are not distributed in this repository; they are downloaded on demand at build and codegen time". crates/pumpkin-data/build.rs:5-6 pins MC_VERSION = "26.3" and MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json", downloads the server jar and copies its data/ folder into the gitignored assets/datapack/.
  • Code generation is a separate tool, tools/pumpkin-codegen/ (about 70 generator modules, main.rs:27-105), writing committed Rust into crates/pumpkin-data/src/generated/: 88 files, 1,545,523 lines, 87 MB (structure_template.rs 25 MB, block.rs 24 MB, biome.rs 14 MB, loot_table.rs 2.5 MB, noise_router.rs 1.8 MB).
  • Blocks are an immutable generated definition plus an optional behavior object. crates/pumpkin-data/src/blocks.rs:19-45 defines Block { id, name, hardness, blast_resistance, map_color, slipperiness, velocity_multiplier, jump_velocity_multiplier, item_id, default_state, states, flammable, experience, loot_table }; generated/block.rs has 1,286 constants and 116 typed property structs behind a BlockProperties trait (to_index, from_state_id, to_props, from_props, lines 2257-2277). Behavior is the BlockBehaviour trait in crates/pumpkin-core/src/block/mod.rs:73-210 with about 35 default-implemented hooks (normal_use, use_with_item, on_place, placed, broken, random_tick, on_scheduled_tick, on_neighbor_update, get_state_for_neighbor_update, emits_redstone_power, get_comparator_output, on_entity_collision, explode, ...). A struct is bound to ids by #[pumpkin_block("minecraft:...")] or to a whole tag by #[pumpkin_block_from_tag("minecraft:doors")] (crates/pumpkin-macros/src/lib.rs:352-416). BlockRegistry (block/registry.rs:513-517) is a dense [u16; BlockId::COUNT] index into Vec<Arc<dyn BlockBehaviour>>; default_registry() makes 217 register calls. block/blocks/ is 217 files and 33,425 lines: 124 single-block attributes plus 32 tag-wide ones cover 1,286 blocks; the rest get None and default handling.
  • Items: 1,658 generated Item constants, each with a static components: &[(DataComponent, &dyn Impl)] list (generated/item.rs:73225-73260), plus an ItemBehaviour trait and 56 behavior files. Entities: 161 generated EntityType constants with attributes, dimensions and spawn rules; entity/ is 326 files and 93,855 lines, entity/ai/ 126 files and 22,513 lines with about 57 goals and a brain port. CONTRIBUTING.md:94-104 requires "Mob AI uses goals, selectors, navigation and movement controls, or the Brain's sensors, memories and behaviors".
  • Loot: one parser, crates/pumpkin-util/src/loot_table/parser.rs, covers all_of, any_of, alternatives, apply_bonus, furnace_smelt, killed_by_player, looting_enchant, match_tool, random_chance, set_count, survives_explosion, table_bonus and others; unknown types return None. Codegen emits 1,450 LootTable literals and the evaluator crates/pumpkin-core/src/world/loot.rs (501 lines; check_condition 199-277, generate_loot_with_context 321, fill_chest_inventory 443) accepts either a generated table or one loaded from disk (LootTableHandle, 350-358). Recipes: 523 generated literals plus a runtime data/datapack/recipe_loader.rs.
  • Worldgen: crates/pumpkin-world/src/generation/ is 232 files and 62,799 lines. Density functions are not hand-ported: tools/pumpkin-codegen/src/noise_router.rs:2951-2974 reads worldgen/noise_settings and worldgen/density_function from the datapack and parse_vanilla_df (2244-2810) panics on an unknown type. crates/pumpkin-world/AGENTS.md:11-13: "Don't copy numbers from decompiled code when the datapack defines them."
  • Pack loading: crates/pumpkin-core/src/data/datapack/ (5,876 lines). load_all (mod.rs:119-175) scans <world>/datapacks; scan_datapacks_dir (1255-1292) loads only directories named in level.dat data_packs.enabled. Embedded content loads first "so on-disk packs can override them by id" (mod.rs:138). read_pack_mcmeta (1566-1582) reads pack_format with unwrap_or(61) and never rejects.
  • Parity: section 4.
  • Plugins: Wasm (pumpkin-plugin-api, MIT OR Apache-2.0) and native. Neither can register a block or item behavior; BlockRegistry::register needs &mut self and the server holds an Arc<BlockRegistry> built once (server/mod.rs:97,181). Plugins subscribe to 44 block events instead.
  • Rationale: AGENTS.md "Vanilla parity" ("Vanilla defines observable behavior and outcomes, not a required internal algorithm") and CONTRIBUTING.md:79 "No Hardcoded Vanilla Values".
  • Bedrock side note: assets/bedrock/README.md lists block_states.nbt from CloudburstMC/Data and blocks.nbt and biomes.json from GeyserMC mappings, for its Bedrock translation layer.

1.2 Minestom and MinestomDataGenerator ​

Java, Apache-2.0 (generator MIT), 704be783b63cfb0666f38f527f90f5d118cffae3 and 1d9d47235737f5eb406600776cbbe24c80063491.

  • Data is a Maven artifact, net.minestom:data version 26.2-rv3 (gradle/libs.versions.toml:6,50). The generator runs Mojang's own data generator inside a Loom server-only project: DataGenerator.java:24-37 calls Bootstrap.bootStrap() then Main.main(new String[]{"--all", "--output=" + tempDir}) and converts the output into about 70 JSON files (DataGenType.java): static registries by reflection over BuiltInRegistries, dynamic registries copied verbatim from data/minecraft/<name>/, and loot tables with the comment "only included for legacy reasons, Minestom doesn't use them".
  • Runtime loading: RegistryData.loadRegistryFile (src/main/java/net/minestom/server/registry/RegistryData.java:121-133) tries the classpath resource, then a same-named file in the working directory. That is the whole override mechanism.
  • Blocks: Block is a sealed interface whose Javadoc says "Implementations are expected to be immutable" (instance/block/Block.java:35-42); BlockImpl is a record of the registry entry, a long of properties packed at 5 bits per value, optional NBT and a nullable BlockHandler (BlockImpl.java:27-36). BlockHandler (BlockHandler.java:22-87) has onPlace, onDestroy, onInteract, onTouch, tick, isTickable. BlockManager maps handler keys to suppliers and substitutes BlockHandler.Dummy with a warning when a saved chunk names an unknown handler (BlockManager.java:41-52). No vanilla placement rules, redstone or ticking exist.
  • Items: Material.prototype() returns a DataComponentMap; ItemStack is immutable with with(DataComponent, value); DataComponents.java declares 111 components. Entities: sealed EntityType plus a class hierarchy (300 files); entity/ai/ is 17 files and 1,317 lines with seven generic goals. No loot evaluator, no recipe JSON, Generator is a callback (instance/generator/Generator.java:6-12).
  • Dynamic registries: 64 accessors on Registries.java; DynamicRegistry.register(Key, T, DataPack) and remove (DynamicRegistry.java:122,140), frozen after startup unless REGISTRY_UNSAFE_OPS.
  • Plugins are the host program: MinecraftServer.getBlockManager().registerHandler(key, supplier) and block.withHandler(handler); new block or item types cannot be added because the interfaces are sealed over the static registry.

1.3 valence and ferrumc ​

  • valence (Rust, MIT, c4177cab47ae109c848a431756d5d7c0207c05ed, MC 1.21.1). Extractor output is committed under crates/valence_generated/extracted/ (6.3 MB) and crates/valence_entity/extracted/, produced by the extractor/ Fabric mod; the build scripts (crates/valence_generated/build/*.rs, 1,950 lines) parse it with include_str! and emit Rust with quote!. Blocks are BlockState(u16) and BlockKind enums; entities are bevy_ecs 0.14 bundles generated per type from entities.json (valence_entity/build.rs, 783 lines). No behavior, loot, recipes or worldgen. README.md:21-27: "You can think of Valence as a game engine for Minecraft servers ... vanilla game mechanics are all expected to be built as optional plugins." No Mojang attribution for the JSON.
  • ferrumc (Rust, MIT, 51830e99675deae1f6cdbb62bf5d32b3d25c1c76, MC 1.21.8). 42 MB of JSON committed under assets/ without provenance, including density_function.json and Pumpkin's test fixture names (multi_noise_sample_no_blend_no_beard_0_0_0.json, density_function_tests.json). bevy_ecs 0.18; generated plain structs for blocks; no behavior hooks; own climate-driven terrain (docs/world-generation/terrain.md), not the datapack pipeline. README.md:298: "Pumpkin aims to match the vanilla server as closely as possible ... FerrumC prioritizes performance and takes a clean-room approach."

1.4 cuberite ​

C++, Apache-2.0, 7fd3fa5c9345a3f1b949c0988c4849db00a68486. The pre-flattening model, kept here as the extreme of hand-maintained data in code.

  • Data: Server/items.ini (1,461 lines of name=id:damage), Server/crafting.txt (1,004 lines, grammar in the header), Server/furnace.txt (188), Server/brewing.txt (155), Server/monsters.ini; parsed by src/BlockType.cpp:30-50 and src/CraftingRecipes.cpp:383-410. Block ids are a compile-time enum ENUM_BLOCK_TYPE in src/BlockType.h. The only Mojang-derived input is the flattening palette: Tools/BlockTypePaletteGenerator/Generator.lua turns the blocks.json data generator report into Server/Protocol/1.13/base.btp.txt, and src/Registries/BlockStates.h (25,485 lines) plus src/Protocol/Palettes/Palette_1_13..1_16.cpp (55,508 lines) are generated from it.
  • Behavior: 100 Block*.h headers; BlockHandler.cpp:199-450 instantiates 251 constexpr handler objects such as cBlockDoorHandler BlockAcaciaDoorHandler(E_BLOCK_ACACIA_DOOR) and cBlockHandler::For(BLOCKTYPE) dispatches through one exhaustive switch. Items mirror this (81 headers). 37 cMonster subclasses with hand-written AI. src/Generating/ (69 files) is a cComposableGenerator configured by world.ini, not a vanilla port. Drops are ConvertToPickups overrides; no loot JSON.
  • Plugins: Lua with 72 HOOK_* entries (src/Bindings/PluginManager.h:82-144). Plugins intercept events; they cannot add block types.

1.5 Mojang's Java server, from public documentation ​

Sources: NeoForge docs (MIT), Fabric docs and wiki (CC BY-NC-SA 4.0), Minecraft Wiki (CC BY-NC-SA 3.0). No decompiled code was read. The pattern:

  • One Block singleton per id, one subclass per behavior family, tuned by BlockBehaviour.Properties (destroyTime, explosionResistance, sound, lightLevel(state -> n), friction, randomTicks(), requiresCorrectToolForDrops()). "there is only ever one of each block in the game" (https://docs.neoforged.net/docs/blocks/). About 1,300 block ids (https://minecraft.wiki/w/Java_Edition_data_values/Blocks) over fewer than 300 block classes (287 names ending in Block in the 1.21.5 net.minecraft.world.level.block index at https://mappings.dev/1.21.5/net/minecraft/world/level/block/index.html, a proxy count).
  • BlockState is immutable and interned ("== can and should be used to compare BlockStates"), built from IntegerProperty, BooleanProperty, EnumProperty in createBlockStateDefinition (https://docs.neoforged.net/docs/blocks/states). The documented pipelines name the overridable hooks: placement (canBeReplaced, getStateForPlacement, canSurvive, onPlace, setPlacedBy), breaking (attack, getDestroyProgress, playerWillDestroy, onRemove, dropResources), ticking (tick via scheduleTick, randomTick), and neighborChanged.
  • Items: Item.Properties sets data components (stacksTo is MAX_STACK_SIZE, durability is MAX_DAMAGE, food, useCooldown, enchantable, repairable, equippable). Since 1.21.2 tools are "a combination of the following fourteen data components" (TOOL, WEAPON, ATTRIBUTE_MODIFIERS, ...) rather than subclasses, with shears as the documented exception (https://docs.neoforged.net/docs/items/tools). The wiki lists 89 component headings, some grouping many variants (https://minecraft.wiki/w/Data_component_format). Storage is a prototype DataComponentMap plus a patch map per stack.
  • Entities: EntityType.Builder.of(factory, MobCategory).sized(w, h).eyeHeight(...).fireImmune().clientTrackingRange(n).updateInterval(n); attributes are doubles with defaults and modifiers; goals have integer priorities and "Mobs attempt to perform the lowest priority goal they can"; brains hold memories, sensors and activities (https://minecraft.wiki/w/Mob_AI).
  • Loot: data-driven tables with singleton and composite entries, 43 function types (https://minecraft.wiki/w/Item_modifier), 19 condition types (https://minecraft.wiki/w/Predicate), context sets per situation; mods add a type by registering a MapCodec (https://docs.neoforged.net/docs/resources/server/loottables/custom/). Recipes: RecipeType plus RecipeSerializer registries; RecipeManager reloads with /reload.
  • Worldgen: 59 hardcoded feature types configured by JSON ("They are hardcoded, thus new ones cannot be added through datapacks", https://minecraft.wiki/w/Configured_feature), 19 placement modifiers, about 54 density function types, material rules, structure sets with random_spread or concentric_rings, template pools and processor lists. Chunk statuses empty, structure_starts, structure_references, biomes, terrain, features, initialize_light, light, spawn, full.
  • Registries and packs: static registries freeze at startup; dynamic registries (worldgen/*, damage_type, enchantment, variants) load at world load and are not reloadable; loot tables, recipes, tags, predicates and functions are. "If a file exists in multiple data packs only the file in the last data pack is used ... tag files without "replace": true merge" (https://minecraft.wiki/w/Data_pack). pack.mcmeta moved from pack_format to min_format and max_format in 25w31a (https://minecraft.wiki/w/Pack_format). The data generator (net.minecraft.data.Main --all) writes the vanilla data pack and reports (reports/blocks.json with every state and its protocol id, registries.json).

1.6 Allay (AllayMC/Allay) ​

Java 21, LGPL-3.0 with data/ and codegen/ MIT, f223a12164f2fe0b594a6b83d950307012f23c64. The component model in detail.

  • Declaration. A component is any class implementing a marker interface (api/.../component/Component.java) with a static field annotated @Identifier.Component naming it (server/.../component/ComponentProvider.java:93-132 walks the class and its superclasses for that field). A block type's behavior is a ComponentClass (server/.../component/ComponentClass.java): its constructor runs every provider in list order, then injects components into the owner's component-typed fields by assignable type (exactly one match or ComponentInjectException, lines 219-247), resolves @Dependency fields by identifier or type (263-305), injects @Manager and @ComponentObject, registers each component on a per-object AllayEventBus, and finally calls @OnInitFinish methods (119-135). The docs describe it as "inspired by dependency injection framework like Guice" (docs/development/component-injection-system.md).
  • Dispatch has two tiers. BlockBehaviorImpl (server/.../block/impl/BlockBehaviorImpl.java:38-48) holds BlockBaseComponent and BlockStateDataComponent behind Lombok @Delegate, so blockType.getBlockBehavior().place(...) is a direct interface call. Inside the base component, ten lifecycle points publish events to sibling components (BlockBaseComponentImpl.java:89-380: CBlockOnNeighborUpdateEvent, CBlockRandomUpdateEvent, CBlockOnPlaceEvent, CBlockAfterPlacedEvent, CBlockOnReplaceEvent, CBlockOnPunchEvent, CBlockOnInteractEvent with a mutable success flag, ...). Handlers are @EventHandler methods found by reflection, sorted by priority and invoked with Method.invoke (eventbus/MethodEventHandler.java:72). Counts: 10 block, 10 item and 13 entity component events; 11 block, 15 item and 46 entity @EventHandler methods in the server.
  • Lifetime. Block components are singletons per block type, shared by all states: AllayBlockType.Builder.build() wraps each as ComponentProvider.of($ -> singleton, ...) (AllayBlockType.java:695), so state-dependent behavior branches on the BlockState argument. Item stacks and entities get fresh components per instance from suppliers (EntityTypeInitializer.java:286-335).
  • Composition. BlockBehavior is BlockBaseComponent plus BlockStateDataComponent (api/.../block/BlockBehavior.java:14-17). The API has 16 block component interfaces (BlockFallableComponent, BlockLiquidBaseComponent, BlockOxidationComponent, BlockSlabBaseComponent, ...), the server 197 implementation files of which 173 are *BaseComponentImpl (door 228 lines, copper door 130). A door is doorBuilder(BlockDoorBehaviorImpl.class, blockId, BlockDoorBaseComponentImpl::new).build() (BlockTypeInitializer.java:1429-1431); a copper door adds .addComponent(new BlockOxidationComponentImpl(level, fn)) (1438-1442). vanillaBlock(BlockId) (436-452) attaches per-state data from block_states.json, tags from block_types.json and the default state hash, and throws if the id has no data.
  • Coverage. BlockTypeInitializer assigns 1035 types by hand; the generated BlockTypeDefaultInitializer has 1477 null-guarded fallbacks. 366 distinct behavior classes are referenced; the largest alias groups are BlockElementBehaviorImpl (120), BlockSlabBehaviorImpl (93), BlockDoubleSlabBehaviorImpl (93), BlockStairsBehaviorImpl (89) and BlockWoodBehaviorImpl (50). codegen/.../BlockClassGen.java:95-150 writes an empty interface and impl per BlockId only if (!Files.exists(...)), then deletes files whose ids disappeared.
  • Entities. A zombie is nine components: humanlike base, container holder, baby, undead, zombie living, physics, head yaw, parallel tick, and EntityAIComponentImpl built from a BehaviorGroupImpl with a NearestPlayerSensor, two MeleeAttackExecutor behaviors at priorities 3 and 2, a FlatRandomRoamExecutor at 1, controllers and an A* route finder (EntityTypeInitializer.java:286-335). The AI runs from @EventHandler onParallelTick(CEntityParallelTickEvent) (EntityAIComponentImpl.java:84-100). Nothing reads minecraft:behavior.*.
  • Data. data/resources/block_states.json (11 MB) is one row per state with hardness, explosionResistance, friction, lightDampening, lightEmission, mapColor, collisionShape, shape, burnOdds, flameOdds, requiresCorrectToolForDrops, isSolid, translucency, liquidReactionOnTouch and blockStateHash; block_types.json has defaultBlockStateHash and tags; items.json has armorValue, attackDamage, enchantValue, furnaceBurnDuration, id, maxDamage, maxStackSize, tags. data/resources/unpacked/ (41 MB) holds the raw dumps.
  • Packs. PackRegistryLoader scans resource_packs/ and plugin directories; BehaviorPack has Type.DATA and is only forwarded to clients. Nothing interprets pack JSON server-side.
  • Plugin API. AllayBlockType.builder(MyBlockImpl.class).identifier("myplugin:ruby_block").customBlockDefinition(CustomBlockStateDefinition.ofTexture("ruby_block")).build() (docs/advanced/custom-block-api.md:44-47); state-dependent geometry via a function per state; rawComponents for anything the API lacks; requires the server module (apiOnly = false). Custom items mirror it (docs/advanced/custom-item-api.md:33-35). Plugin components get the same events as vanilla ones.
  • Tests: section 4.

1.7 Dragonfly (df-mc/dragonfly) ​

Go, MIT, 1bf4e242c81f1f66ba6bdf8fcdcc80c70125a499.

  • Interfaces. 44 block-side interfaces (server/block/block.go:17-118: Activatable, Pickable, Punchable, LightEmitter, LightDiffuser, Replaceable, EntityLander, EntityInsider, EntityStepper, Frictional, Permutable; block.go:274-288: Sleepable, Flammable; break_info.go:18-23: Breakable; hopper.go, container.go, explosion.go, ...) and world-side ones the tick loop needs (server/world/block.go:50-72 Liquid; 132-181 RandomTicker, ScheduledTicker, TickerBlock, NeighbourUpdateTicker, NBTer, LiquidDisplacer; redstone.go:55-108 ten redstone interfaces). 43 item interfaces (server/item/item.go:16-202: UsableOnBlock, UsableOnEntity, Consumable, Weapon, Cooldown, Releasable, ...; durability.go Durable, Repairable; enchantment.go Enchantable).
  • Dispatch is a type assertion at each call site: tick.go:115-121 for neighbor updates, 186-193 for random and block entity ticks, 318 guards a scheduled tick with the block hash so a replaced block does not get a stale tick; about 50 assertions in server/player/player.go and 62 in server/session/. BasicBlockRegistry.Finalize (server/world/block_registry.go:329-352) asserts each block once and packs light filter, light emission and NBT|RandomTick|Liquid|LiquidDisplacing flags into a uint16 per runtime id, so lighting and random tick selection never assert.
  • Implementer counts over 261 struct types: BreakInfo() 208, UseOnBlock 94, NeighbourUpdateTick 71, Activate 33, RandomTick 31, FlammabilityInfo 28, ScheduledTick 8. Shared behavior comes from embedded helper structs (transparent in 87 types, solid in 126, sourceWaterDisplacer in 48).
  • Identity. Two hashes derived from the Go value. server/block/hash.go is generated by cmd/blockhash (225 Hash() methods packing fields into bits, wood_door.go:1136-1138); network_block_hash.go:16-96 re-serializes {name, states} as sorted little-endian NBT and takes FNV-1a-32, with minecraft:unknown fixed at 0xfffffffe. block_state.go:24-35 registers every palette state from the embedded block_states.nbt as an unknownBlock placeholder; register.go makes 245 RegisterBlock calls that enumerate permutations (allDoors() in wood_door.go:149-163); RegisterBlock panics "block state returned is not registered" or "already registered" (block_registry.go:294,297); Finalize sorts states by fnv1.HashString64(name) to match the vanilla palette order and panics on hash collision. Decoding never parses NBT into fields: blocks[rid] is already the registered value, which is why every permutation must exist up front (Permutable docs, block.go:113-121).
  • Items, creative and recipes: RegisterItem panics when a name is missing from the embedded vanilla_items.nbt; creative.go:96-123 silently skips creative entries whose item is not registered, so the creative menu self-trims; recipe/vanilla.go:13-20 embeds crafting_data.nbt, smithing_data.nbt, smithing_trim_data.nbt, potion_data.nbt and skips any recipe with an unresolvable input.
  • Entities: 16 types (server/entity/register.go:12-29), zero mobs, behavior as config structs (ProjectileBehaviourConfig, PassiveBehaviourConfig). Worldgen: NopGenerator and generator/flat.go; 90 hand-written biome structs. Tests: 10 _test.go files, none against vanilla data; the panics are the check.
  • Custom content: CustomBlock and CustomBlockBuildable (server/world/block.go:30-47), server/block/customblock/ properties and permutations, server/server.go:398-411 emits protocol.BlockEntry with runtime ids past the vanilla palette, and server/internal/packbuilder/blocks.go:27-55 writes a resource pack from the Go values at startup.
  • No written rationale; the registry comment (block_registry.go:248-250) is the design note: "We assume all blocks must have all their states registered beforehand."

1.8 PowerNukkitX ​

Java, LGPL-3.0, 37caa9e66690f2d327b33d8a72e7621377ab96c5, root package org.powernukkitx. 1327 Block*.java, 186 Entity*.java.

  • Properties. block/BlockProperties.java:12-60 sums the bit sizes of its BlockPropertyTypes, enumerates the cartesian product when the total is at most 16 bits and throws otherwise. BlockChest.java:33 declares new BlockProperties(CHEST, CommonBlockProperties.MINECRAFT_CARDINAL_DIRECTION); BlockWoodenDoor.java:9 adds OPEN_BIT, UPPER_BLOCK_BIT, DOOR_HINGE_BIT. CommonBlockProperties has 146 shared constants. HashUtils.computeBlockStateHash is the same FNV-1a over sorted NBT. BlockRegistry.java:1508-1545 enforces by reflection that each class has a static final PROPERTIES field and overrides getProperties(), with a copy-pasteable fix in the exception message.
  • Registries. registry/Registries.java holds 22 singletons. Data comes from the Maven artifact org.powernukkitx:gamedata (r-26_u5-SNAPSHOT): gamedata/kaooot/{block_palette.nbt, block_definitions.nbt, item_palette.json, item_components.nbt, item_tags.json, creative_items.json, recipes.json, entity_identifiers.nbt, biomes.json, biome_definitions.nbt, trim_data.json, voxel_shapes.json, jigsaw_structure_data.nbt}. loadDataDrivenProperties() (1478-1490) keeps block_definitions.nbt only to "hand it on join". Nothing reads entity, loot, spawn rule or recipe JSON from a behavior pack.
  • AI is a Java builder per mob (entity/mob/EntityZombie.java:67-95: EntityAI.of(this).behavior(new MeleeAttackExecutor(...)).when(...).sensors(new AttackSensor(), new NearestPlayerSensor(40, 0, 0)).controllers(...).routeFinder(...)), with 76 executors, 19 sensors, 18 evaluators and 12 controllers. minecraft:behavior. has zero hits in main sources.
  • Loot: level/loot/VanillaLootTables.java (128 lines) hand-codes three stronghold chest tables keyed by Mojang's path strings. Trading: 167 TradeRecipeBuildUtils.of(...) calls in entity/data/profession/. Recipes: RecipeRegistry.java:672-682 reads Kaooot's recipes.json and expands tag inputs through item_tags.json.
  • Generator: 16 densityfunction/ classes each citing misode/mcmeta in an @implNote (DensityContinents.java:8 links .../worldgen/density_function/overworld/continents.json), 172 feature classes, 57 populators; no noise_settings JSON is read, the JSON was transcribed into Java.
  • Plugin API: BlockRegistry.registerCustomBlock(Plugin, Class<? extends Block>...) (1568-1620) requires CustomBlock; CustomBlockDefinition.Builder (145-312) assembles a CompoundTag of Mojang component names (minecraft:friction, minecraft:destructible_by_mining, minecraft:light_emission) for the client, while server behavior stays in the Java class.
  • Tests: section 4.

1.9 CloudburstMC/Server ​

Java, server GPL-3.0 and api LGPL-3.0, b54033d5293689b3e286ab20814163fe2ccb2d9d, active (last commit 2026-10-22, "Support importing BDS worlds"). The BlockBehavior class hierarchy the earlier design had is gone; the current design is component keys bound to lambdas:

  • api/.../block/BlockTraits.java has 120 constants mapping API names to vanilla state names (BooleanBlockTrait.of("is_open", "open_bit") at line 578, CARDINAL_DIRECTION at 48). BlockState is immutable with withTrait (BlockState.java:64-102).
  • api/.../block/BlockComponents.java declares 65 ComponentTypes (AFTER_BREAK, ATTACK, CAN_BE_USED, CAN_RANDOM_TICK, USE, ON_NEIGHBOUR_CHANGED, GET_LOOT, ON_PLACE, ON_DESTROY, ...). Handlers are stateless lambdas in 80 files under server/.../block/component/; registry/VanillaBlockBehaviors.java (1,057 lines, 379 .set(...), 118 configure(...)) binds them, for example configureDoor at 708-716 sets ON_PLACE, CAN_BE_USED, GET_LOOT, USE, ON_NEIGHBOUR_CHANGED, ON_DESTROY. Items have 38 ItemBehaviors constants; entities use EntityFactory with 168 classes and no AI package.
  • Numeric block properties are data: server/.../block/BlockPropertyData.java:28-70 loads data/blocks.json keyed by blockStateHash and throws on duplicates. data/ is the CloudburstMC/Data submodule on branch temp/26.50-geometry, the same source this repository already pins. The palette passes through the block-state-updater library (BlockPalette.java:258-283). No behavior pack JSON is read; generators are YAML presets under server/src/main/resources/generator/.
  • No README or design document explains the move or the earlier stall.

1.10 Nukkit (CloudburstMC/Nukkit) ​

Java, GPL-3.0, b3caad67d86be34aeca3937e64e8176f6760a2d4. 751 Block*.java, 322 Item*.java. block/Block.java:48-56 holds static parallel arrays (list, fullList, light, lightFilter, solid, hardness, transparent); init() (72-110) reflectively instantiates every class for every meta value and stores fullList[(id * CACHED_DATA_SIZE) | data]; Blocks.java has 712 list[STONE] = BlockStone.class entries. Overridable methods: onUpdate(int) 303, onActivate 307, getHardness 315, getResistance 319, getToolType 331, getId 405, getDrops 453. Entity.registerEntity(name, class) (1070-1074) with 116 calls. Resources are network dumps (runtime_block_states.dat, recipes.json, creative_items.json, entity_identifiers.dat, biome_definitions.dat). This is the pattern this server's block/blocks/ and entity/ follow today.

1.11 Mojang's Script API as a data model ​

From https://learn.microsoft.com/en-us/minecraft/creator/scriptapi/minecraft/server/ (page names in parentheses).

  • Blocks: BlockPermutation.resolve(name, states), getState, getAllStates, withState, matches, getTags, getItemStack (blockpermutation); BlockTypes.getAll(); BlockStates.getAll() returns BlockStateType with validValues, the global state domains (blockstatetype). BlockComponentTypes lists 15 ids (fluid_container, inventory, map_color, movable, piston, record_player, redstone_producer, sign, recipe_crafting, ...).
  • Items: ItemComponentTypes lists 11 (book, compostable, cooldown, durability, dyeable, enchantable, food, inventory, potion, ...). ItemDurabilityComponent exposes maxDurability but its page says "this component only applies to data-driven items" (itemdurabilitycomponent). ItemFoodComponent exposes nutrition, saturationModifier, canAlwaysEat, usingConvertsTo; ItemCooldownComponent exposes cooldownCategory and cooldownTicks; ItemEnchantableComponent exposes slots.
  • Entities: EntityComponentTypes has 68 members (health, movement, type_family, scale, variant, is_baby, tameable, inventory, equippable, projectile, rideable, leashable, ageable, breathable, navigation.*, movement.*, flag components is_*, can_*, fire_immune). There is no minecraft:attack. Seven are EntityAttributeComponents with currentValue, defaultValue, effectiveMax, effectiveMin (health, movement, lava and underwater movement, exhaustion, hunger, saturation). Entity.triggerEvent(name) fires JSON component group events; getProperty and setProperty read entity properties.
  • Events: world.afterEvents has 72 signals and world.beforeEvents 18 cancellable ones (playerBreakBlock, playerPlaceBlock, playerInteractWithBlock, itemUse, entityHurt, explosion, ...).
  • Extension: system.beforeEvents.startup exposes blockComponentRegistry.registerCustomComponent(name, hooks) and itemComponentRegistry.registerCustomComponent(name, hooks). Block hooks (15): beforeOnPlayerPlace, onBlockStateChange, onBreak, onEntity, onEntityFallOn, onNamedTick, onNeighborChanged, onPlace, onPlayerBreak, onPlayerInteract, onRandomTick, onRedstoneUpdate, onStepOff, onStepOn, onTick, each (event, CustomComponentParameters). Item hooks (7): onBeforeDurabilityDamage, onCompleteUse, onConsume, onHitEntity, onMineBlock, onUse, onUseOn. The overview (https://learn.microsoft.com/en-us/minecraft/creator/documents/customcomponents) states the dispatch rules: custom components are listed in components beside minecraft:loot; "for each type of event, the event is sent to the custom components in the order they are listed"; "a component on a permutation overrides the component on the base block"; "a custom component can be registered in one behavior pack, and then used from another."

1.12 PrismarineJS/minecraft-data ​

JavaScript data repository, 5e063fa99cfe8ded065ec6788b66696b7e4ef143. README.md:148-153 says "MIT" and "Some of the data was extracted manually or automatically from wiki.vg and minecraft.gamepedia.com. If required by one of the sources the license might change"; the clone has no LICENSE file and no root package.json, and the npm package is built from PrismarineJS/node-minecraft-data. The CC0 premise in the task is not supported by this checkout.

  • Coverage. data/bedrock/ has 57 version directories; the newest with game data is 1.26.30 (1,356 blocks, 16,913 states in blockStates.json, 1,933 items). 1.26.40 to 1.26.51 carry only protocol files. data/dataPaths.json for bedrock.1.26.51 aliases blocks, blockStates, blockCollisionShapes, items to 1.26.30, entities to 1.21.80, biomes to 1.21.60, recipes to 1.19.10, enchantments to 1.19.1, and materials and effects to Java pc/1.17.
  • Shape. A block: {"id": 49, "name": "oak_log", "displayName": "Oak Log", "hardness": 2, "resistance": 2, "stackSize": 64, "diggable": true, "material": "mineable/axe", "transparent": false, "emitLight": 0, "filterLight": 15, "defaultState": 1529, "minStateId": 1529, "maxStateId": 1531, "drops": [161], "boundingBox": "block"}; no Bedrock block carries states, those live in blockStates.json as {"name": "oak_log", "states": {"pillar_axis": {"type": "string", "value": "y"}}, "version": 18168865} indexed by palette position. An item: {"id": 347, "stackSize": 1, "name": "diamond_sword", "enchantCategories": [...], "repairWith": ["diamond"], "maxDurability": 1561, "nbt": {...}, "version": "none"}; data_driven items carry their component NBT. An entity: {"id": 23, "internalId": 32, "name": "zombie", "height": 1.8, "width": 0.6, "type": "hostile", "category": "Hostile mobs"}. Schemas in schemas/*_schema.json are validated by tools/js/test/test.js with Ajv, and bedrock_block_states.js checks that minStateId..maxStateId ranges tile the state list exactly.
  • Data quality. In the 1.26.30 blocks.json, none of the 22 harvestTools ids and none of the 1,004 integer drops resolve to the expected item in the 1.26.30 items.json (stone drops [62], which is lit_furnace); in 1.19.1 to 1.21.111 they do. The schema cannot catch this because drops only requires integers.
  • Provenance (doc/bedrock.md:28-46): blocks.json, items.json, entities.json and blockStates.json from "bedrock-extractor" (https://github.com/extremeheat/minecraft-data-extractor/tree/master/bedrock, not in this clone set); recipes.json from the "bedrock-protocol dumper"; biomes.json "via Amulet"; enchantments.json "via Geyser -> pc data"; blockCollisionShapes.json, materials.json, effects.json "(from pc data)"; language.json from CreeperG16/extract-minecraft-lang; protocol.json "manual updated".
  • Update process. Detection lives in bedrock-protocol: .github/workflows/update-helper.yml runs every 2 hours, .github/helper-bot/index.js:6 polls the iTunes lookup for com.mojang.minecraftpe, line 92 calls bedrockServer.getPongDetails(version) (boots a real BDS and reads the protocol number), opens an issue and dispatches minecraft-data's bedrock-version-bump.yml, which runs tools/js/incrementVersion.js to clone the previous dataPaths entry and open a PR. Humans then edit latest/proto.yml. Block and item data arrive in separate PRs ("Add bedrock block data for 1.26.0-1.26.30 (#1192)"), which is why newer versions alias older game data.

1.13 prismarine-registry, prismarine-block, prismarine-item, prismarine-entity, prismarine-chunk ​

All MIT. Commits 69d2fb05, 8c8d4e02, 2b148a38, 4f2678f7, a08dc2c6.

  • prismarine-registry/lib/bedrock/index.js adds handleStartGame(packet): loadItemStates(packet.itemstates) rebuilds the item tables from the packet, and loadHashedRuntimeIds fills blocksByRuntimeId[Block.getHash(name, states)] for every blockStates entry when block_network_ids_are_hashes is set. Its Bedrock tests are it.skip ("times out").
  • prismarine-block/index.js (434 lines) is one Block class whose constructor copies hardness, displayName, shapes, boundingBox, transparent, diggable, material, harvestTools, drops from registry.blocksByStateId[stateId] (126-156); Bedrock properties come from registry.blockStates[stateId].states (190-194); fromProperties scans minStateId..maxStateId linearly (249-256); getHash sorts keys, writes little-endian NBT and runs FNV-1a-32 (288-301, 426-433); digTime (311-385) multiplies by registry.materials[material][heldItemType], which on Bedrock comes from pc/1.17. No class per block exists anywhere in the stack.
  • prismarine-item writes the Bedrock network item (network_id, count, metadata, stack_id, extra.nbt) in toNotch (127-157). prismarine-entity is a 79-line EventEmitter with position, metadata and equipment fields.
  • prismarine-chunk/src/index.js maps Bedrock majors 1.18 to 1.21 onto src/bedrock/1.18/chunk; there is no 1.26 entry, so require('prismarine-chunk')('bedrock_1.26.51') throws. 1.3/SubChunk.js:73-139 decodes the palette type byte (usingNetworkRuntimeIds = paletteType & 1, bitsPerBlock = paletteType >> 1), resolves runtime palettes through the registry and persistent palettes through Block.fromProperties. Tests load captured level_chunk and subchunk packets for 1.16.220 to 1.19.1 and assert that re-encoding reproduces the same blob hashes (test/bedrock.test.js).

1.14 flying-squid ​

JavaScript Java server, MIT, fa022792d30e79f96c8c8dbf659fc61ae4177b4f. 44 plugin files in src/lib/plugins/. digging.js:71 uses block.digTime(...) and 158-175 drops from registry.blockLoot[name].drops or block.drops[0]; placeBlock.js maps item names to registry.blocksByName; world.js sets blocks by registry.blocks[id].minStateId. No recipes, no mob AI (spawn.js:79-95 sets gravity and health only). Tests drive real mineflayer bots against the server for 23 versions (test/mineflayer.test.js, 448 lines).

1.15 minecraft-bedrock-server and bedrock-protocol ​

  • extremeheat/minecraft-bedrock-server (MIT, 1aae0086d1ac2a702741c7e5c808780935306acc; the PrismarineJS/minecraft-bedrock-server name in the task does not exist). src/index.js (330 lines): getLatestVersions() (35-57) reads https://net-secondary.web.minecraft-services.net/api/v1.0/download/links; download() (62-118) probes https://www.minecraft.net/bedrockdedicatedserver/bin-<os>/bedrock-server-<v>.zip with HEAD requests over 30 build suffixes, downloads with a 20 s https.get, and unzips; it sets no User-Agent and has no EULA prompt. configure() (128-145) appends online-mode=false, allow-list=false, server-port=19130 and user options to server.properties; startServer() (171-200) fires when stdout contains Server started; getPongDetails() reads transport= and pings RakNet on 19130 or NetherNet on 7551. test/test.js boots 1.16.210, 1.18.0, 1.21.80 and 1.26.51 with an 80 s start budget.
  • PrismarineJS/bedrock-protocol (MIT, de199f0fe7ade6477c5637851ea814a65cffabe9). src/options.js pins CURRENT_VERSION = '1.26.51' and lists 54 versions, 19 of them tested on CI. test/internal.test.js boots BDS per version through tools/startVanillaServer.js (startServerAndWait2(version, 220000, ...)), runs a spawn test and dumpPackets in parallel, then replays the dumped packets through bedrock-protocol's own Server. CI runs on Ubuntu and Windows with timeout-minutes: 14. Its tools dump packets for tests; none produce minecraft-data blocks.json or items.json.

1.16 misode/deepslate ​

TypeScript, MIT, c33b4a65f2ad474b14aab71061fbd7766b675fad, version 0.27.2, 123 files and 13,253 lines. Another agent covers its worldgen depth; this is the overall design.

  • src/core/Registry.ts:5-21: Registry<T> keyed by Identifier with an optional parser: (obj: unknown) => T, lazy thunk values, a builtin map that survives clear(), and a lazily created tag registry. Holder.parser(registry, direct) (Holder.ts:4-18) implements "a string is a reference, anything else is inline"; HolderSet.fromJson (HolderSet.ts:28-58) parses tag files with replace and required. WorldgenRegistries.ts:9-14 registers worldgen/noise, worldgen/density_function, worldgen/noise_settings, worldgen/material_rule, worldgen/material_condition and a parserless worldgen/biome.
  • BlockState is an Identifier plus untyped string properties, with parse, fromNbt, fromJson; there is no block definition, no state domain validation. ItemStack carries Map<string, NbtTag> components and asks a consumer-supplied ItemComponentsProvider (ItemStack.ts:7-9) for base components. No loot, recipe, feature or biome objects; no ticking or interaction code.
  • Vanilla data is not shipped (files: ["lib/*", "dist/*"]); the demo fetches https://raw.githubusercontent.com/misode/mcmeta/ at runtime (demo/main.ts:65-78). Tests assert inline expected numbers (DensityFunction.test.ts:43-48 toBeCloseTo(0.3004295819443726)) and an ASCII slice of a generated chunk, not stored vanilla fixtures.

1.17 bridge. and Blockception schemas ​

  • bridge-core/editor (GPL-3.0, 8eaccaf8eceb31a2a9cce51a392b7d758220909c) holds no schemas. src/libs/data/Data.ts:46-51 fetches https://raw.githubusercontent.com/bridge-core/editor-packages/release/packages.zip at runtime. That repository is MIT (https://raw.githubusercontent.com/bridge-core/editor-packages/main/LICENSE, GitHub spdx_id: MIT), as is the compiler bridge-core/dash-compiler. The schemas select a version table with JSON Schema if/then on format_version (packages/minecraftBedrock/schema/entity/main.json: "1.8.0" ... "1.16.0" to ./v1.16.0/main.json, "1.16.100" to ./v1.16.100/main.json, 38 entity folders up to v1.26.20); formatVersions.json lists 55 versions with currentStable: "1.26.20". The dash-compiler custom component plugin (src/Plugins/BuiltIn/Components/Plugins.ts, Component.ts) is a build-time macro: a JS file under components/<block|item|entity>/ deep-merges generated vanilla JSON into the host file. That is the opposite of Mojang's runtime hooks.
  • Blockception/Minecraft-bedrock-json-schemas (BSD-3-Clause, 473d1aa94be63806a4f38345ad461b5d4e8d8a2a). 1,159 source schemas under source/behavior/ (entities 502, of which 177 behaviors and 211 components; blocks 47; items 47; loot_tables 50; features 33; spawn_rules 23; biomes 17; recipes 11) and source/general/. One schema per file type, not per version: source/general/format_version.json is a string matching ^([1-9]+)\.([0-9]+)\.([0-9]+)$ with default 1.26.0, and the payload is a latest-superset. Component descriptions carry defaults and caveats, for example behaviors/melee_attack.json documents cooldown_time 1, melee_fov 90 and marks max_dist as "Unused". Compiled by DaanV2/Json-Schema-Action-Compression in CI.

2. Patterns and trade-offs ​

Vanilla Bedrock has 1463 blocks and roughly 60 distinct behaviors (Allay's 366 behavior classes collapse to about 60 once alias groups such as the 120 element blocks, 93 slabs and 89 stairs are counted once; Pumpkin covers 1,286 Java blocks with 32 tag-wide behaviors plus 124 singles). The four patterns handle that shape differently.

(a) Class per thing with inherited methods ​

Used by Nukkit (751 classes), PowerNukkitX (1327), cuberite (251 handler objects over 100 classes, dispatched by an exhaustive switch), and this server (260 block, 65 item, 111 entity classes).

  • Shape fit: each of the 1463 blocks needs a class or a constructor argument. PNX reaches 1327 classes by subclassing per family and copying per alias; cuberite reuses one handler class with the block id as a constructor argument. Properties are overridable methods, so table-driven values need a class per row.
  • Plugins subclass and register; PNX enforces a PROPERTIES convention by reflection and needs the class to carry its own id.
  • Dispatch is a virtual call, the cheapest of the four.
  • Testability: instantiate and call; but field initializers that call overridable methods (this server's item/Item.ts:29, noted in the earlier note) make definitions arrive too late.
  • Bedrock JSON mapping: poor. Components become attributes of a class, component groups and events have no home, and permutations become if (state.x) branches inside methods.

(b) Immutable definition plus a behavior type per family ​

Used by Mojang Java (about 1,300 ids over fewer than 300 classes, builder properties per id), Pumpkin (generated constants plus BlockBehaviour per id or tag), Dragonfly (struct per block value, Go interfaces per capability, type assertions at call sites), Minestom (record plus nullable BlockHandler).

  • Shape fit: good. Pumpkin's #[pumpkin_block_from_tag("minecraft:doors")] binds one behavior to every door; Mojang's Blocks class passes the same DoorBlock::new to every wood. Dragonfly pays for Go's lack of generics over state by enumerating permutations (allDoors()) and generating Hash() per type.
  • Plugins register a behavior for ids or tags (Pumpkin) or attach a handler per placed block (Minestom). Pumpkin's registry is sealed after default_registry(), so plugins get events only.
  • Dispatch: Pumpkin uses a dense [u16; COUNT] index to an Arc<dyn>; Dragonfly asserts per call site and flattens hot flags into a uint16 per runtime id at Finalize. Both show that load-time precomputation, not the pattern, decides the hot path cost.
  • Testability: good. Definitions are data; a family behavior is unit-tested once; Pumpkin's chunk dumps test the whole pipeline.
  • Bedrock JSON mapping: the definition side maps directly onto the 1.26.50 block files (destructible_by_mining, map_color, loot, redstone_conductivity) and onto mojang-blocks.json for states. The behavior side is a code mapping keyed by family, which is what description.internal_vanilla_data.block_archetype (slab_block, stair_block) in those files already names. Component groups and events do not apply to blocks in this model.

(c) Definition plus a component list dispatched by event ​

Used by Allay (component singletons wired by reflection, per-object event bus), CloudburstMC/Server (65 ComponentType keys to lambdas), Mojang Bedrock itself (JSON components, 15 block and 7 item custom component hooks, list-order dispatch, permutation override), Java 1.21.2+ item data components, and Minestom's DataComponentMap for items (data only).

  • Shape fit: best. Allay covers 1477 types with 173 base component implementations plus small add-on components (BlockOxidationComponentImpl on copper doors, BlockBlockEntityHolderComponentImpl on chests); Cloudburst's configureDoor is six key-to-lambda bindings. Both keep one heavy base behavior per archetype and compose the rest, which is how Mojang's JSON reads too.
  • Plugins add a component to an existing type or build a new type from components (AllayBlockType.builder(...).addComponent(...), registry.configure(type).set(KEY, handler)), and plugin components receive the same events as vanilla ones. This is also the shape of Mojang's registerCustomComponent.
  • Dispatch: Cloudburst does a map lookup by key per hook call. Allay delegates the base component directly (@Delegate) and only pays reflection (Method.invoke, priority-sorted) for sibling components on ten block events; it never measured that cost in its docs. Ordering between components needs a rule: Mojang uses list order, Allay a priority integer.
  • Testability: each component is tested against a fake owner; Allay's BlockStateDataTest checks that every registered state has data. The risk is interaction bugs between components that only appear in combination.
  • Bedrock JSON mapping: direct. Component names are the keys; component groups are sets of components added or removed by events (entities); permutations are per-state component overrides, which Mojang documents for its own blocks. A component the server does not implement becomes a logged name instead of a missing class.

(d) Full ECS ​

Used by valence and ferrumc (bevy_ecs).

  • Shape fit: entities fit (a bundle per type generated from entities.json); blocks and item stacks do not, since they are values in chunk storage and inventories, not entities. Neither project implements vanilla behavior, so the pattern is unproven for parity.
  • Plugins add systems; this is the strongest extension model of the four.
  • Dispatch is archetype iteration, cache-friendly for per-tick systems and awkward for one-off interactions (a right click is an event, not a frame).
  • Testability: systems run against a World fixture.
  • Bedrock JSON mapping: entity components map one-to-one (minecraft:health to a Health component), and component groups map onto insert and remove at runtime. In TypeScript the typed-array ECS libraries force numeric component data, and the gain is per-tick throughput for thousands of entities, not correctness.

What all of them share ​

Identity is always data from a dump or generator (palette NBT, blocks.json report), hashed with FNV-1a-32 over sorted {name, states} NBT in every Bedrock project (Dragonfly network_block_hash.go, PNX HashUtils, Allay HashUtils, prismarine-block getHash). Shells are always generated (Allay BlockClassGen, Pumpkin pumpkin-codegen, Minestom code-generators, cuberite's palette Lua, Dragonfly cmd/blockhash). Behavior is always code, and the open design choice is only how that code is keyed to the data.

3. Pack loading and stacking precedents ​

What the Bedrock servers do ​

None of Allay, PowerNukkitX, Dragonfly, Cloudburst or Nukkit interprets a behavior pack server-side. Allay's PackRegistryLoader discovers packs in resource_packs/ and plugin jars and tags behavior packs Type.DATA for transfer to clients; PNX's ResourcePackManager serves packs and its only built-in behavior pack is ChemistryBehaviorPack; Dragonfly generates a resource pack from Go custom blocks at startup. The vanilla data these servers run on is compiled in.

What Pumpkin does for Java datapacks ​

DatapackManager.load_all scans <world>/datapacks, reads the enabled list from level.dat (data_packs.enabled, ids file/<dir>), loads embedded content first so disk packs override by id, and loads recipes, functions, loot tables, structures, template pools, processor lists, damage types, trades and generic dynamic registries. pack_format is read but never rejected (unwrap_or(61)). Feature flags (minecraft:vanilla, trade_rebalance) come from enabled pack names (mod.rs:602-649). Plugins can list, enable, disable and reload packs (pumpkin-plugin-api/src/datapack.rs). Minestom's only override is a same-named file in the working directory shadowing the classpath JSON.

What Mojang documents for Java ​

Last enabled pack wins per file; tags merge unless "replace": true; /reload covers loot tables, recipes, tags, predicates and functions but not dynamic registries; pack.mcmeta carries min_format and max_format since 25w31a with optional overlays for multi-version packs (https://minecraft.wiki/w/Data_pack, https://minecraft.wiki/w/Pack_format).

What Mojang documents for Bedrock ​

Schema validation of packs ​

Two models exist. bridge. keeps one schema per format_version and selects it with if/then (38 entity versions). Blockception keeps one latest-superset schema per file type with a format_version pattern and per-component defaults and caveats. A server loader needs bridge.'s version table to know what an old file means and can use Blockception's descriptions to validate the current contract. Neither validates cross-file references; bridge. does that through its "lightning cache" index of identifiers and events.

4. Parity verification precedents ​

  • Pumpkin compares generated chunks against vanilla captures. crates/pumpkin-world/src/generation/proto_chunk_test/ (2,014 lines) reads 29 *.chunk dumps from assets/tests/ (22 MB; names such as noise_no_blend_no_beard_0_0.chunk, nether_carvers_no_blend_no_beard_7_4.chunk) and compares the block state id at every position (dumps.rs:44-80), with allowed_mismatches = 6000 for the noise stage (dumps.rs:31) and separate surface, biome and carver checks; stage_parity.rs:326-347 compares per-section FNV hashes of state ids against stage_hashes.json; density_function_tests.json and multi_noise_sample_*.json pin sampled values. The captures come from the external Extractor mod; no in-repo code regenerates them.
  • Allay checks the palette in both directions and the data coverage of every state. server/src/test/java/org/allaymc/server/block/BlockStateHashTest.java:56-65 asserts every hash in block_states.json exists in Registries.BLOCK_STATE_PALETTE and every minecraft: palette entry exists in the data; BlockStateDataTest.java:101-118 walks every BlockId and every state and fails if any state hash lacks a data row. 123 test files in total.
  • Dragonfly makes parity a startup invariant: RegisterBlock panics on a state the palette lacks or a duplicate, Finalize panics on hash collisions or a missing minecraft:air, RegisterItem panics on a name missing from vanilla_items.nbt, and the creative menu and recipe loaders silently drop unregistered content. Its 10 _test.go files cover redstone scenarios and break info, not vanilla comparison.
  • PowerNukkitX logs rather than fails: BlockStateRegistry.java:30-58 reports "failed to find block state" or "BlockState <hash> was not <name>" for every palette entry at init. BlockDefinitionParityTest.java:55-85 asserts each block's no-arg constructor agrees with registry construction; ItemDefinitionParityTest spot-checks numbers (diamond sword damage 7).
  • CloudburstMC/Server throws only on duplicate blockStateHash rows in data/blocks.json (BlockPropertyData.java:28-70).
  • minecraft-data validates every file against a JSON schema with Ajv (tools/js/test/test.js) and checks that Bedrock state ranges tile the palette (bedrock_block_states.js); bedrock-ci.yml installs the PR's data into a bedrock-protocol checkout and runs its tests against real BDS builds (14-minute timeout).
  • bedrock-protocol boots BDS per version in CI, dumps packets, then replays them through its own server implementation (test/internal.test.js, tools/genPacketDumps.js); its helper bot detects new versions by booting BDS and reading the pong. prismarine-chunk re-encodes captured level_chunk and subchunk packets and asserts equal blob hashes. flying-squid drives mineflayer bots through dig, place and container flows.
  • None of the Bedrock servers compares behavior against a running BDS. The earlier note (section 7) showed BDS boots on a hosted runner in 2 to 3 seconds, and bedrock-protocol proves 19 versions per CI run is affordable. The unexploited check is a side-by-side run: the same Script API pass that dumps BlockStates.getAll(), ItemTypes.getAll() plus getComponents(), and EntityTypes.getAll() from BDS, diffed against what this server loaded, plus packet replay of StartGame, ItemRegistry, BiomeDefinitionList and CraftingData payloads.

5. Recommendation for this repository ​

Pattern (c), definitions plus component lists keyed by Mojang's component names, implemented with (b)'s one-base-behavior-per-archetype idea and Dragonfly's load-time flattening. Reasons:

  • Bedrock's own files are already in this shape: components, component_groups, events and permutations for entities and the 98 data-driven blocks, components for 74 items (earlier note, section 3). Mojang's extension model for everything else is the 15 block and 7 item hooks with list-order dispatch and permutation override. Mirroring it makes "component not implemented" a log line and keeps the vanilla JSON loadable without translation.
  • Allay proves the shape covers 1477 blocks with about 170 base behaviors, and Cloudburst proves it needs no reflection: a map from hook key to handler per type is enough. TypeScript suits that better than Java does: a definition is a plain object, a component registry is Map<string, ComponentFactory>, and hook sets are explicit interfaces, so the dispatch tables can be built once per definition at load time (an array of handlers per hook name, in component order) and hot flags (isSolid, random tick, light filter, blast resistance) can go into typed arrays indexed by runtime id, as Dragonfly's Finalize does.
  • The earlier note's section 8 lists why the current class-per-thing code resists data: constructors need legacy ids, properties are overridable methods, base-class initializers call them during construction, and instanceof checks on Item and Mob. Pattern (c) removes all four, because a block or item is a definition reference plus state, not an instance of a class.
  • Entities take Mojang's JSON as is: attribute components first (minecraft:health, minecraft:collision_box, minecraft:type_family, minecraft:loot), then minecraft:behavior.* goals mapped by name with the JSON priority onto the existing entity/ai/goals/ classes, then component groups and events as the state machine. Allay and PNX both hand-build what the JSON already says; Pumpkin shows a full goal port is 22,513 lines on the Java side, so expect the goal implementations, not the data plumbing, to be the long tail.
  • ECS stays optional for entities later and is not a fit for blocks or items.

Concrete pieces to reuse or copy, with license against this repository's MPL-2.0:

PieceWhereLicenseUse
Registry<T> with parser, lazy entries, builtin set, tags; Holder.parser reference-or-inline; HolderSet.fromJson with replacedeepslate src/core/{Registry,Holder,HolderSet,Identifier}.tsMITDependency or copied shape for the definition registries and tag merging
FNV-1a-32 network hash over sorted NBT, Finalize flattening into per-runtime-id flags, panic-on-unknown-state registration, creative and recipe self-trimming, custom block BlockEntry and pack builderDragonfly server/world/network_block_hash.go, block_registry.go:283-364, item/creative/creative.go, item/recipe/vanilla.go, server.go:398-437MITPort to TypeScript with attribution; this repo already hashes the same way in block/state/BlockRuntimeIds.ts
BDS discovery, download, server.properties edit, Server started detection, pong probeextremeheat/minecraft-bedrock-server src/index.jsMITdevDependency or copied into the capture job; add the browser User-Agent the CDN needs and an explicit EULA gate, which it lacks
Boot BDS per version in CI, dump packets, replay through own serverbedrock-protocol test/internal.test.js, tools/genPacketDumps.jsMITModel for the side-by-side check in section 4
Per-format_version schema tables and formatVersions.json; build-time custom component expansionbridge-core/editor-packages, bridge-core/dash-compilerMITValidate loaded packs; know what an old format_version means
Latest-superset schemas with component defaults and caveatsBlockception/Minecraft-bedrock-json-schemasBSD-3-ClauseCompatible with MIT given attribution; validation of vanilla and user packs
Per-state block data rows (block_states.json shape)AllayMC/BedrockData (CC0), Allay data/ (MIT)CC0, MITSame content as the CloudburstMC/Data submodule already pinned (Apache-2.0); either is fine
Component injection system, base-plus-add-on component split, BlockStateDataTest coverage check, generated null-guarded initializerAllay server/.../component/, block/type/, testsLGPL-3.0Design reference only; do not copy code
Loot evaluator shape (check_condition, LootTableHandle static or disk), chunk dump parity method, codegen from datapack JSONPumpkinGPL-3.0Design reference only
65 ComponentType hook names and configureDoor-style binding tablesCloudburstMC/Server api/.../block/BlockComponents.java, registry/VanillaBlockBehaviors.javaLGPL-3.0 api, GPL-3.0 serverReference for the hook vocabulary, compared against Mojang's 15
Hook vocabulary and dispatch rulesMojang Script API docs (BlockCustomComponent, ItemCustomComponent, custom components overview)DocumentationAdopt the hook names so a server-side component and a Script API component look alike

Avoid as a data source: minecraft-data's Bedrock game data (stops at 1.26.30, Java-derived materials and blockCollisionShapes, broken harvestTools and drops ids at 1.26.30) and prismarine-chunk for 1.26 (no implementation). Both remain useful as design references.

Open questions ​

  • ItemDurabilityComponent is documented as applying "only to data-driven items". If that holds for ItemTypes.getAll() on vanilla tools, the Script API pass the earlier note proposed as its second source cannot read tool durability, and items.json-style dumps (Allay's maxDamage, armorValue, attackDamage from a hooked binary) remain the only source for 1550 items. This needs a test against BDS.
  • Allay never measured its reflection-based component event dispatch; whether per-object event buses cost anything at 20 ticks per second with thousands of entities is unknown. A TypeScript port would avoid the question with precomputed handler arrays, but the base-versus-add-on split still needs a rule for handler order (Mojang's list order or Allay's priority).
  • Pumpkin's allowed_mismatches = 6000 tolerance for the noise stage has no documented origin. Which stage diverges and why is not written down.
  • Nothing on Learn documents the world_behavior_packs.json array order or how BDS resolves two packs that both carry a definition with the same identifier and min_engine_version; the "top of stack wins" rule is stated for the client UI. A BDS experiment would settle it.
  • minecraft-data's npm package license and the maintenance status of extremeheat/minecraft-data-extractor were not checked; the clone's README says MIT with a caveat and has no LICENSE file.
  • CloudburstMC/Server's move from BlockBehavior classes to ComponentType lambdas has no written rationale in the repository; whether it stalled or is converging is visible only in commit activity (active as of 2026-10-22).
  • Whether an ECS for entities pays off in TypeScript depends on entity counts this server does not yet reach; no project in this set has measured it.

Reproduction ​

mkdir -p /tmp/bds-research-priorart/tools && cd /tmp/bds-research-priorart
for r in Pumpkin-MC/Pumpkin:pumpkin Minestom/Minestom:minestom Minestom/MinestomDataGenerator:minestom-data-generator \
         valence-rs/valence:valence ferrumc-rs/ferrumc:ferrumc cuberite/cuberite:cuberite AllayMC/Allay:allay \
         df-mc/dragonfly:dragonfly PowerNukkitX/PowerNukkitX:pnx CloudburstMC/Server:cloudburst CloudburstMC/Nukkit:nukkit \
         PrismarineJS/minecraft-data:minecraft-data PrismarineJS/prismarine-registry:prismarine-registry \
         PrismarineJS/prismarine-block:prismarine-block PrismarineJS/prismarine-item:prismarine-item \
         PrismarineJS/prismarine-entity:prismarine-entity PrismarineJS/prismarine-chunk:prismarine-chunk \
         PrismarineJS/flying-squid:flying-squid extremeheat/minecraft-bedrock-server:minecraft-bedrock-server \
         PrismarineJS/bedrock-protocol:bedrock-protocol misode/deepslate:deepslate bridge-core/editor:bridge-editor \
         Blockception/Minecraft-bedrock-json-schemas:blockception; do
  git clone -q --depth 1 "https://github.com/${r%%:*}" "${r##*:}"
done
for d in */; do [ -d "$d/.git" ] && printf "%s %s\n" "$d" "$(git -C "$d" rev-parse HEAD)"; done

# Allay counts
grep -c 'BlockTypes\.[A-Z_0-9]* = ' allay/server/src/main/java/org/allaymc/server/block/type/BlockTypeInitializer.java   # 1035
grep -c 'if (BlockTypes\.' allay/server/src/main/java/org/allaymc/server/block/type/BlockTypeDefaultInitializer.java    # 1477
grep -o 'builder([A-Za-z]*Impl.class)' allay/server/src/main/java/org/allaymc/server/block/type/BlockTypeDefaultInitializer.java | sort -u | wc -l  # 366
find allay/server/src/main/java/org/allaymc/server/block/component -name '*BaseComponentImpl.java' | wc -l             # 173
grep -rn 'manager.callEvent' allay/server/src/main/java/org/allaymc/server/block/component/BlockBaseComponentImpl.java | wc -l  # 10

# Pumpkin
sed -n 5,6p pumpkin/crates/pumpkin-data/build.rs
grep -c 'manager.register(' pumpkin/crates/pumpkin-core/src/block/registry.rs                                            # 217
ls pumpkin/assets/tests | wc -l                                                                                          # 42
sed -n 29,32p pumpkin/crates/pumpkin-world/src/generation/proto_chunk_test/dumps.rs

# Dragonfly, Cloudburst, Minestom
sed -n 115,121p dragonfly/server/world/tick.go
grep -n 'is not registered\|already registered' dragonfly/server/world/block_registry.go
sed -n 708,716p cloudburst/server/src/main/java/org/cloudburstmc/server/registry/VanillaBlockBehaviors.java
sed -n 121,133p minestom/src/main/java/net/minestom/server/registry/RegistryData.java

# minecraft-data id-space check (script kept outside the clones, run with -I)
python3 -I /tmp/bds-research-priorart/tools/check_mcdata.py /tmp/bds-research-priorart/minecraft-data

The check_mcdata.py script loads data/bedrock/1.26.30/{blocks,items}.json, counts harvestTools keys and integer drops that resolve to an item id, and prints the dataPaths.json entry for bedrock.1.26.51.