44namespace imperazim \custom \block ;
55
66use Closure ;
7- use imperazim \custom \block \component \DestructibleByMiningComponent ;
8- use imperazim \custom \block \component \FrictionComponent ;
9- use imperazim \custom \block \component \LightDampeningComponent ;
10- use imperazim \custom \block \component \LightEmissionComponent ;
117use imperazim \custom \block \permutation \Permutable ;
128use imperazim \custom \block \permutation \Permutation ;
139use imperazim \custom \block \permutation \PermutationHelper ;
@@ -58,7 +54,20 @@ final class CustomBlockFactory {
5854 * Serializes block data to a cache file so it can be loaded on worker threads without closures.
5955 */
6056 public function addWorkerInitHook (string $ cachePath ): void {
61- if (empty ($ this ->blockFuncs )) {
57+ // Collect custom block state data so workers can re-sort their BlockStateDictionary.
58+ // This MUST happen even if no block objects can be serialized, because the main thread
59+ // already re-sorted its dictionary (which shifts ALL runtime IDs including vanilla blocks).
60+ // Workers must apply the same re-sort or chunk encoding will use wrong runtime IDs.
61+ $ customStates = [];
62+ foreach (BlockPaletteManager::getInstance ()->getCustomStates () as $ state ) {
63+ $ customStates [] = [
64+ 'name ' => $ state ->getStateName (),
65+ 'rawProperties ' => $ state ->getRawStateProperties (),
66+ 'meta ' => $ state ->getMeta (),
67+ ];
68+ }
69+
70+ if (empty ($ this ->blockFuncs ) && empty ($ customStates )) {
6271 return ;
6372 }
6473
@@ -67,35 +76,29 @@ public function addWorkerInitHook(string $cachePath): void {
6776
6877 // Build serializable cache: for each block, store the igbinary-serialized Block object
6978 // and the list of state entries (identifier + state count).
70- $ cache = [];
79+ $ blocks = [];
7180 foreach ($ this ->blockFuncs as $ identifier => [$ blockFunc , $ serializer , $ deserializer ]) {
7281 try {
7382 $ block = $ blockFunc ();
7483 $ blockData = igbinary_serialize ($ block );
7584 if ($ blockData === null ) {
7685 continue ;
7786 }
78- // Count how many states this block has in the palette.
7987 $ stateCount = 0 ;
8088 foreach ($ this ->blockPaletteEntries as $ entry ) {
8189 if ($ entry ->getName () === $ identifier ) {
8290 $ stateCount ++;
8391 }
8492 }
85- $ cache [$ identifier ] = [
93+ $ blocks [$ identifier ] = [
8694 'blockData ' => $ blockData ,
8795 'states ' => array_fill (0 , max (1 , $ stateCount ), ['name ' => $ identifier ]),
8896 ];
8997 } catch (\Throwable ) {
90- // Block cannot be serialized (e.g. anonymous class) — skip gracefully.
91- // The block will appear as INFO_UPDATE on async-encoded chunks.
9298 }
9399 }
94100
95- if (empty ($ cache )) {
96- return ;
97- }
98-
101+ $ cache = ['blocks ' => $ blocks , 'states ' => $ customStates ];
99102 file_put_contents ($ cacheFile , igbinary_serialize ($ cache ));
100103
101104 $ server = Server::getInstance ();
@@ -145,15 +148,9 @@ public function register(
145148 CustomItemFactory::getInstance ()->registerBlockItem ($ identifier , $ block );
146149 $ this ->customBlocks [$ identifier ] = $ block ;
147150
148- // Build base component NBT from block properties .
151+ // Build component NBT — only include components the block explicitly provides .
149152 $ propertiesTag = CompoundTag::create ();
150- $ components = CompoundTag::create ()
151- ->setTag ('minecraft:light_emission ' , (new LightEmissionComponent ($ block ->getLightLevel ()))->toNbt ())
152- ->setTag ('minecraft:light_dampening ' , (new LightDampeningComponent ($ block ->getLightFilter ()))->toNbt ())
153- ->setTag ('minecraft:destructible_by_mining ' , (new DestructibleByMiningComponent ($ block ->getBreakInfo ()->getHardness ()))->toNbt ())
154- ->setTag ('minecraft:friction ' , (new FrictionComponent (1 - $ block ->getFrictionFactor ()))->toNbt ());
155-
156- // Merge any BlockComponent instances the block may carry.
153+ $ components = CompoundTag::create ();
157154 if ($ block instanceof BlockComponentsHolder) {
158155 foreach ($ block ->getBlockComponents () as $ component ) {
159156 $ components ->setTag ($ component ->getName (), $ component ->toNbt ());
@@ -216,12 +213,32 @@ public function register(
216213 GlobalBlockStateHandlers::getSerializer ()->map ($ block , $ serializer );
217214 GlobalBlockStateHandlers::getDeserializer ()->map ($ identifier , $ deserializer );
218215
216+ // Map creative info to Bedrock string format (always included, defaults to "items")
217+ $ categoryStr = 'items ' ;
218+ $ groupStr = '' ;
219219 if ($ creative !== null && $ creative ->category !== null ) {
220+ $ categoryStr = match ($ creative ->category ) {
221+ CreativeCategory::CONSTRUCTION => 'construction ' ,
222+ CreativeCategory::NATURE => 'nature ' ,
223+ CreativeCategory::EQUIPMENT => 'equipment ' ,
224+ CreativeCategory::ITEMS => 'items ' ,
225+ };
226+ if ($ creative ->group !== null ) {
227+ $ name = $ creative ->group ->getName ();
228+ $ groupStr = is_string ($ name ) ? $ name : $ name ->getText ();
229+ }
220230 CreativeInventory::getInstance ()->add ($ block ->asItem (), $ creative ->category , $ creative ->group );
221231 }
222232
233+ $ components ->setTag ('minecraft:creative_category ' , CompoundTag::create ()
234+ ->setString ('category ' , $ categoryStr )
235+ ->setString ('group ' , $ groupStr ));
236+
223237 $ propertiesTag
224238 ->setTag ('components ' , $ components )
239+ ->setTag ('menu_category ' , CompoundTag::create ()
240+ ->setString ('category ' , $ categoryStr )
241+ ->setString ('group ' , $ groupStr ))
225242 ->setInt ('molangVersion ' , 1 );
226243
227244 $ this ->blockPaletteEntries [] = new BlockPaletteEntry ($ identifier , new CacheableNbt ($ propertiesTag ));
0 commit comments