From b02e734012f12cb91e88828c3c1f02108558307d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:07:04 +0200 Subject: [PATCH 01/12] feat(inventory): query native recipe row eligibility Evaluate current row availability without replacing the retained collection snapshot. Preserve the native flat/grouped thresholds and representative-header exemption, and fold the query into the shared UI patch. The full build passes 2,826 tests with 163 optional skips. Packaged production classes match 3,584 original eligibility cases and retain all 4,144 collection/history checks. Record native registry, profile and refresh references for the remaining player-controller work. --- docs/native-player-inventory.md | 87 +++++++++++- ...are-native-ui-programs-and-resources.patch | 133 ++++++++++++++++-- ...are-native-ui-programs-and-resources.pr.md | 14 ++ 3 files changed, 220 insertions(+), 14 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index ff849322..7ac3801a 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -50,9 +50,17 @@ An absent player, absent component, missing page, stale entity generation or raw The reference executes 640 cases through both the original helper and the registered callback, for 1,280 calls. Actor delivery, component backing and the fallback virtual getter remain supplied interfaces. -The constructor also stores an independent flag at offset `0x1319`. +The constructor also stores an independent profile predicate at offset `0x1319`. Its producer compares a virtual service result with `1`; the service comes from client vtable offset `0x578`, then service vtable offset `0x70`. -The service type remains unverified. The implementation must not substitute the effective creative predicate or the input mode for this flag. +The native inventory factory uses the same getter to choose the desktop root for zero and the pocket root for nonzero values. +Only value one sets the constructor predicate. Other nonzero values still select the pocket root but leave that predicate false. +This state must remain separate from effective creative mode and controller input mode. + +Seven raw signed values execute the original factory selection and constructor comparison, including the native client getter at `0x145dc2800`. +The factory slice runs from `0x14052eb9c` to `0x14052ec1b`; the constructor slice runs from `0x144e0f832` to `0x144e0f86a`. +The service getter result, context backing, allocator and CRT boundaries are supplied. +These slices do not execute complete screen construction, settings acquisition, physical input or rendering. +The service's concrete type remains unverified. The client getter reads field `0xe10`, whose shared owner occupies `0xe18`. ### Layout and tabs @@ -86,7 +94,7 @@ The raw filtering preference remains separate from the recipe model's effective The bounded constructor reference executes 3,472 combinations of layouts, tabs, filtering, both flags, screen type and controller input mode. Each case then executes sixteen original property getters, for 55,552 getter calls. -The reference supplies model-filter updates and initial tab publication. It disables the separate restricted-tab policy. +The reference supplies model-filter updates and initial collection refresh. It disables the separate restricted-tab policy. Full construction, restricted-tab availability, device delivery and visible pixels remain unverified. ### Logical item and recipe collections @@ -121,6 +129,8 @@ A fabricated empty recipe catalog cannot establish support for the default inven ## Shared recipe collections +Release 0.3.56 includes this shared collection model. + The shared `RecipeCollection` model implements the target collection ordering and retained header expansion. Cooking uses the same partitioner for its flat visible recipe prefix. The player controller still needs a complete catalog producer before it can use grouped collections. @@ -144,10 +154,77 @@ The steps cover expansion history and groups with equal icons but different iden Native vector allocation and cleanup execute. Item copy/destruction, icon equality, availability evaluation and external callbacks use supplied boundaries. These checks do not establish recipe acquisition, unlocks, craftability, full controller ownership, physical input or visible screen parity. Six focused regressions cover ordering, representative identity, filtering, invalid entries, expansion history and immutable snapshots. -The complete build passes 2,823 tests, with 163 optional skips and no failures or errors. +The collection milestone's complete build passed 2,823 tests, with 163 optional skips and no failures or errors. All 1,465 core classes match the embedded add-on and proxy copies; all 1,191 client classes match the tested add-on. The packaged core repeats all 4,144 recipe comparisons with its class origin verified. +## Current recipe-row eligibility + +The continuation after 0.3.56 adds a query for current row availability without refreshing or reordering its retained snapshot. +Flat collections disable evaluator states two and three. +Grouped collections disable only state two, with an exception for headers that match a retained representative by icon and count. +The exception applies to expanded and collapsed headers. It does not apply to member rows. +Representatives come from the last collection refresh; the query does not recompute them. + +Original getter `0x1434461c0` supplies 3,584 cases with fresh evaluator values after collection construction. +Every captured query passes byte one to the evaluator interface. +The shared API keeps that query separate from the refresh evaluator. +It requires the caller to supply current availability and exact representative/header item comparison. +Original count checks and representative traversal execute. Item copy/destruction, icon equality and evaluator results use supplied boundaries. +Three regressions cover fresh queries, unchanged snapshots, representative identity, icon/count matching and grouped member behavior. +This query supplies presentation state. It does not authorize a recipe action or establish full player-controller support. + +The eligibility milestone's complete build passes 2,826 tests, with 163 optional skips and no failures or errors. +All 1,466 core classes match both embedded copies, and all 1,191 client classes match the tested add-on. +The packaged core matches all 3,584 eligibility cases and repeats the 4,144 collection/history checks with its class origin verified. + +## Registry and catalog source reference + +The lookup at `0x143491110` locks an `ItemRegistryRef` and reads the registry's field at offset `0x430`. +The target's diagnostics identify `ItemRegistryRef::_lockRegistry()` and the downstream `NonOwnerPointer` access. +It copies an activation pointer, lifetime owner and source pointer. +The lookup does not test the activation byte itself; callers must still admit the returned handle. + +The complete original lookup passes 192 supplied registry/owner/holder cases. +Input reference counts remain balanced. Successful copies retain the output owner. +Failed-lock diagnostic calls return false at an explicit external boundary. +The checks observe returned references before caller release. +They do not construct a registry or acquire its data from a server. + +The category constructor's slice from `0x1434382ec` to `0x143438361` passes 500 capacity cases. +It sums the member-vector lengths of referenced groups, counts repeated references again and skips invalid unsigned group indices. +An absent category owner or group-data pointer produces zero capacity. +The fixture supplies category, group and member-vector backing and stops before allocation. + +The original phase table at `0x14e880e90` contains these collection/selector pairs: + +| Phase | Collection ID | Internal selector | +| --- | --- | --- | +| 0 | 18, search | 6 | +| 1 | 15, construction | 1 | +| 2 | 16, nature | 2 | +| 3 | 20, equipment | 3 | +| 4 | 17, items | 4 | + +Disassembly of `0x143439c20` uses selector six for the full search list and the other selectors for registry category groups. +These selectors describe the internal model; they do not establish network enum values. +Complete catalog registration, recipe matching, unlocks, craftability, incremental publication and live mutation still need implementation and verification. + +## Recipe collection refresh admission + +Function `0x144e1f430` refreshes a dirty selected collection. It does not change or publish the selected tab. +Left tabs one through five resolve collection IDs 15, 20, 17, 16 and 18 respectively. +The corresponding dirty bit clears only after the dispatcher locks a live collection and calls its refresh receiver. +An absent lookup result, missing weak owner, expired strong count or absent collection leaves the bit set for a later retry. +Tab six clears the inventory bit without a recipe lookup. Other unsupported tab values leave the mask unchanged. +The dispatcher forwards its refresh argument without changing its 32-bit value. + +The complete original dispatcher passes 2,880 cases across tab values, dirty masks, lookup results, owner state and refresh arguments. +Its switch, string handling, mask transitions and shared/weak reference operations execute through normal return. +Global-name lookup, model collection lookup and the final refresh receiver use supplied boundaries. +All observed reference counts remain balanced, and native temporary allocations are released. +This reference does not execute full controller construction, collection production, physical input or rendering. + ## Target and original functions The reference uses Bedrock 1.26.51.1, build 51061372, protocol 2193. @@ -208,7 +285,7 @@ The reference does not establish GPU output, clipping, physical input delivery o ## Java implementation checks -The full stack passes 2,817 tests, with 163 optional skips. +The installed-screen milestone's complete build passed 2,817 tests, with 163 optional skips. Three packaged camera regressions pass and reject four deliberately incorrect implementations. The native comparison covers framing, actor rotation and body yaw in all 672 cases. Head-yaw comparisons cover the 336 cases with that component present. diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 5f7dfea7..152ef86d 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From aa607d8a2bb3e1ae73af6f9addfed149305d37cc Mon Sep 17 00:00:00 2001 +From 2e70057804a0a5a7e2cefaf7d0b8e96ce310804b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): share bounded native UI programs and resource @@ -227,6 +227,18 @@ steps distinguish catalog identity from icon/count header identity. Item copying, icon comparison, availability evaluation and external callbacks remain supplied boundaries. Recipe acquisition, full player controller, physical input and screen parity remain separate integration work. + +Query recipe-row availability independently of refresh so presentation can +observe current evaluator state without replacing its retained snapshot. +Flat rows disable states two and three; grouped rows disable state two, +except for headers matching a retained representative by icon and count. +Member rows do not receive that exemption. Keep the evaluator and native +item comparison explicit at the integration boundary. + +Original getter 1434461c0 supplies 3,584 cases with fresh evaluator results +after construction. Three regressions retain snapshot identity, current +availability and header/member distinctions. Full catalog acquisition, +controller ownership, action admission, input and pixels remain unfinished. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -262,7 +274,7 @@ physical input and screen parity remain separate integration work. .../api/resourcepack/ui/NpcDialogueUi.java | 316 ++++ .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ .../api/resourcepack/ui/NpcEditorState.java | 322 ++++ - .../api/resourcepack/ui/RecipeCollection.java | 239 +++ + .../api/resourcepack/ui/RecipeCollection.java | 266 +++ .../api/resourcepack/ui/ServerFormUi.java | 253 +++ .../resourcepack/ui/UiAsepriteMetadata.java | 95 ++ .../api/resourcepack/ui/UiBindings.java | 43 + @@ -301,7 +313,7 @@ physical input and screen parity remain separate integration work. .../ui/NativeSearchNormalizationTest.java | 46 + .../resourcepack/ui/NpcDialogueUiTest.java | 108 ++ .../resourcepack/ui/NpcEditorStateTest.java | 209 +++ - .../resourcepack/ui/RecipeCollectionTest.java | 151 ++ + .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ .../api/resourcepack/ui/ServerFormUiTest.java | 427 +++++ @@ -310,7 +322,7 @@ physical input and screen parity remain separate integration work. .../api/resourcepack/ui/UiFontsTest.java | 192 +++ .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 82 files changed, 12748 insertions(+), 2 deletions(-) + 82 files changed, 12851 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -6485,10 +6497,10 @@ index 000000000..c5ebc59ab +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/RecipeCollection.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/RecipeCollection.java new file mode 100644 -index 000000000..bd1aee15d +index 000000000..cb7249f22 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/RecipeCollection.java -@@ -0,0 +1,239 @@ +@@ -0,0 +1,266 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -6656,6 +6668,26 @@ index 000000000..bd1aee15d + return null; + } + ++ /** Query current availability without replacing the retained collection snapshot. */ ++ public boolean disabled(final int index, final Eligibility eligibility) { ++ final Row row = this.snapshot.rows.get(index); ++ final int state = checkedState(eligibility.evaluate(row)); ++ if (!this.grouping) { ++ return state >= 2; ++ } ++ if (state != 2) { ++ return false; ++ } ++ if (row instanceof Header header) { ++ for (final T representative : this.snapshot.representatives) { ++ if (eligibility.represents(representative, header.group)) { ++ return false; ++ } ++ } ++ } ++ return true; ++ } ++ + public Snapshot snapshot() { + return this.snapshot; + } @@ -6681,6 +6713,13 @@ index 000000000..bd1aee15d + return partition.ordered.subList(0, partition.eligibleCount); + } + ++ /** Current evaluator results and native icon/count comparisons for an admitted view. */ ++ public interface Eligibility { ++ int evaluate(Row row); ++ ++ boolean represents(T representative, G group); ++ } ++ + public interface Source { + int evaluate(T entry); + @@ -12905,10 +12944,10 @@ index 000000000..a7430d107 +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/RecipeCollectionTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/RecipeCollectionTest.java new file mode 100644 -index 000000000..d59b17e88 +index 000000000..7f6264c7a --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/RecipeCollectionTest.java -@@ -0,0 +1,151 @@ +@@ -0,0 +1,227 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -12978,7 +13017,7 @@ index 000000000..d59b17e88 + final var expanded = collection.snapshot(); + assertEquals(4, expanded.rows().size()); + assertEquals(4, collection.refresh(entries.reversed(), true, true, false).rows().size()); -+ collection.refresh(entries, false, false, false); ++ collection.refresh(entries, false, false, true); + assertNull(collection.toggleHeader(first)); + assertEquals(4, collection.refresh(entries, true, false, false).rows().size()); + assertEquals(2, collection.refresh(entries, true, false, true).rows().size()); @@ -13022,6 +13061,82 @@ index 000000000..d59b17e88 + assertDoesNotThrow(() -> collection.refresh(List.of(entry(1, 4, null)), true, false, false)); + } + ++ @Test ++ void queriesCurrentFlatAvailabilityWithoutReorderingTheSnapshot() { ++ final var source = new Source(); ++ final var collection = new RecipeCollection<>(source); ++ final var snapshot = collection.refresh(List.of(entry(1, 0, null), entry(2, 1, null)), false, true, false); ++ final int[] current = {0}; ++ final int[] calls = {0}; ++ final var eligibility = new RecipeCollection.Eligibility() { ++ @Override ++ public int evaluate(final RecipeCollection.Row row) { ++ calls[0]++; ++ return current[0]; ++ } ++ ++ @Override ++ public boolean represents(final Entry representative, final Group group) { ++ throw new AssertionError(); ++ } ++ }; ++ for (int state = 0; state < 4; state++) { ++ current[0] = state; ++ assertEquals(state >= 2, collection.disabled(0, eligibility)); ++ } ++ assertEquals(4, calls[0]); ++ assertEquals(List.of(1, 2), source.evaluations); ++ assertSame(snapshot, collection.snapshot()); ++ assertEquals(List.of(1, 2), ids(snapshot.ordered())); ++ } ++ ++ @Test ++ void exemptsHeadersThatMatchTheirGroupRepresentativeByIconAndCount() { ++ for (final var group : List.of(new Group(1, 1), new Group(2, 1), new Group(1, 2))) { ++ final var collection = new RecipeCollection<>(new Source()); ++ collection.refresh(List.of(entry(1, 2, group), entry(2, 0, group)), true, true, false); ++ final boolean exempt = group.icon == 1 && group.count == 1; ++ assertEquals(!exempt, collection.disabled(0, eligibility(2))); ++ collection.toggleHeader(group); ++ assertEquals(!exempt, collection.disabled(0, eligibility(2))); ++ assertTrue(collection.disabled(1, eligibility(2))); ++ assertTrue(collection.disabled(2, eligibility(2))); ++ for (final int state : new int[]{0, 1, 3}) { ++ assertFalse(collection.disabled(0, eligibility(state))); ++ assertFalse(collection.disabled(1, eligibility(state))); ++ } ++ } ++ } ++ ++ @Test ++ void usesTheRefreshRepresentativesRatherThanRecomputingTheirAvailability() { ++ final var group = new Group(1, 1); ++ final var source = new Source(); ++ final var collection = new RecipeCollection<>(source); ++ final var entries = List.of(entry(1, 3, group), entry(2, 3, group)); ++ collection.refresh(entries, true, false, false); ++ assertFalse(collection.disabled(0, eligibility(2))); ++ assertTrue(source.evaluations.isEmpty()); ++ collection.refresh(entries, true, true, false); ++ assertTrue(collection.disabled(0, eligibility(2))); ++ assertEquals(List.of(1, 2), source.evaluations); ++ assertFalse(collection.disabled(0, eligibility(3))); ++ } ++ ++ private static RecipeCollection.Eligibility eligibility(final int state) { ++ return new RecipeCollection.Eligibility<>() { ++ @Override ++ public int evaluate(final RecipeCollection.Row row) { ++ return state; ++ } ++ ++ @Override ++ public boolean represents(final Entry representative, final Group group) { ++ return representative.identity == group.icon && group.count == 1; ++ } ++ }; ++ } ++ + private static Entry entry(final int identity, final int state, final Group group) { + return new Entry(identity, state, group, true); + } diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 480f9b33..b379a98c 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -500,3 +500,17 @@ The complete player catalog producer, unlocks, craftability, action admission, f The final four-project build passes 2,823 tests with 163 optional skips and no failures or errors. Checkstyle passes after replay. All 1,465 core classes match both embedded copies, and all 1,191 client classes match the tested add-on. The packaged core repeats all 4,144 collection comparisons with its production class origin verified. + +## Current recipe-row eligibility + +Query current availability separately from collection refresh, using a caller-supplied evaluator and native representative/header comparison. +Flat rows disable states two and three. Grouped rows disable only state two, with a representative exemption for headers. +Expanded member rows retain their own eligibility; the query does not replace or reorder the snapshot. +Original getter `1434461c0` supplies 3,584 matching cases with item-copy, icon-comparison and evaluator boundaries. +Three regressions cover fresh availability, stable snapshots, exact representative/count matching and grouped member behavior. +Additional native reset controls confirm that a flat refresh retains grouped expansion history even when its reset argument is true. +Full catalog acquisition, controller ownership, action admission, physical input and visible native parity remain unfinished. + +The full four-project build passes 2,826 tests with 163 optional skips and no failures or errors. +Checkstyle passes after replay. All 1,466 core classes match the embedded copies, and all 1,191 client classes match the tested add-on. +The packaged core matches all 3,584 eligibility cases and retains all 4,144 collection/history comparisons, with production origins verified. From dd1797c5e429867601150b3b0f59043f73c74ebd Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:38:33 +0200 Subject: [PATCH 02/12] feat(inventory): retain the native player search catalog Build the player search source from the complete server creative catalog and the constructed definition's native category. Preserve entry identity, variants, duplicates and ordering independently of group metadata and Java fallback items. Keep constructor defaults and ordered component updates consistent across runtime-ID replacement. Fold the category model and data into the registry patch, and source acquisition into the creative catalog patch. Update the add-on fixtures for the explicit connection registry constructor. All four projects build with 2,830 tests passed and 163 optional skips. The packaged core matches 3,418 new native catalog comparisons and retains 7,728 eligibility and collection/history comparisons. Embedded core classes and factory data match in the add-on and proxy. Full player UI, availability evaluation, physical input and the reported bugs remain open. --- docs/native-player-inventory.md | 52 +- ...erver-authoritative-bedrock-requests.patch | 28 +- ...08-signal-sustained-bedrock-item-use.patch | 6 +- ...ain-bedrock-creative-catalog-entries.patch | 93 +- ...rate-authored-native-ui-presentation.patch | 1986 ++++++++++------- ...rate-authored-native-ui-presentation.pr.md | 20 + ...rver-creative-items-in-a-bedrock-tab.patch | 30 +- 7 files changed, 1335 insertions(+), 880 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 7ac3801a..3f6ad38b 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -123,17 +123,65 @@ The player model requires these distinct collections: Model builder `0x1434360d0` creates the item collections and four category recipe collections through `0x143438210`. It also creates a separate search collection. These recipe collections share constructor `0x143443e50`. The category counts use the source category's group references; search uses the complete source list. -This model construction evidence comes from disassembly. Production recipe acquisition, category assignment, availability evaluation and mutation remain unfinished. +This model construction evidence comes from disassembly. The player search source now uses definition categories from the connection registry. +The four grouped category sources, recipe availability evaluation and mutation remain unfinished. The player screen must retain the complete catalog and recipes that need a crafting table. A fabricated empty recipe catalog cannot establish support for the default inventory asset. +## Player search catalog + +This section describes the unreleased player search catalog. + +The model constructor receives the local player and stores it at offset `0x30`. +The player owns the search vector at offsets `0x600` through `0x610`. +The original rebuild routine is `0x14020ec60`. +It copies creative registry entries in their existing order and admits definition categories one through four. +An absent definition or any other category excludes the entry. +Group category, item count, Java fallback identity and crafting-grid size do not select this source. + +Text search, unlocks and craftability run later through separate evaluators. + +The connection registry now retains each definition's category byte. +Built-in defaults come from complete native registration and cover all 1,949 factory identifiers in the existing table. +New ordinary and component definitions start at category four. +Explicit component construction replaces a built-in default before any ordered component updates. +Legacy component readers preserve this category. + +For component items, a compound `item_properties` reads the integer property `creative_category` and takes its low byte. +A missing or mistyped property sets the byte to zero. +An absent or mistyped `item_properties` block preserves the previous byte. +Unknown `None` registrations receive their fallback definition after component updates. +Final runtime-ID replacement selects the surviving definition. + +`CreativeCatalog` retains a separate search list when `ItemRewriter` receives the server catalog. +This list uses the connection registry and preserves the admitted entries' identities, variants, duplicates and order. +The complete creative catalog remains available independently. +The search list does not discard recipes that require a crafting table. +The authored player screen and its evaluator still need integration. + +The complete original search rebuild and vector growth execute 906 controls. +They cover all category bytes, absent definitions, duplicates, allocation boundaries and repeated replacement. +Item copy/destruction, allocator and CRT calls use supplied boundaries. + +Separate controls execute complete component construction, binary NBT parsing, property reading and destruction. +These cover 279 inputs and five retained-update steps; the complete legacy reader preserves the constructor category for the same inputs. +Built-in registration executes with the retained versioned block bootstrap and an empty packet vector. +This establishes constructor defaults rather than a server packet baseline. +Physical inventory rendering, unlock policy, recipe solving and controller input remain unverified. + +All four projects build with 2,830 tests passed, 163 optional skips and no failures or errors. +The packaged core matches 1,949 factory defaults, 563 typed-update comparisons and 906 search rebuilds. +It also retains all 7,728 original eligibility and collection/history comparisons. +All 1,466 core classes and the factory table match the embedded add-on and proxy copies. +All 1,191 client classes match the tested add-on. + ## Shared recipe collections Release 0.3.56 includes this shared collection model. The shared `RecipeCollection` model implements the target collection ordering and retained header expansion. Cooking uses the same partitioner for its flat visible recipe prefix. -The player controller still needs a complete catalog producer before it can use grouped collections. +The player controller still needs the four grouped category sources before it can use grouped collections. Entries retain four stable evaluator buckets. A contiguous catalog group moves as one run: state zero wins, then state one; otherwise the first member selects state two or three. diff --git a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index 05ee7cf1..a362bc58 100644 --- a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch +++ b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch @@ -1,4 +1,4 @@ -From 128d795d7f92c8cf0f310feda48a4fe920a8870b Mon Sep 17 00:00:00 2001 +From 57fffe626f4870508cfaee599b64db4ca0a06c49 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(inventory): translate server-authoritative Bedrock @@ -7555,7 +7555,7 @@ index a3ac81345..a3b933c2a 100644 + public static final String WORKBENCH = "workbench"; } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java -index 928f2175b..90a202d8a 100644 +index 3a5180c25..d7b5b49e7 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java @@ -5,6 +5,7 @@ @@ -7592,11 +7592,11 @@ index 928f2175b..90a202d8a 100644 for (final var name : factory.getAsJsonArray("identifiers")) { final String identifier = name.getAsString(); final String description = componentDescriptions.has(identifier) ? componentDescriptions.get(identifier).getAsString() : null; -- if (factories.putIfAbsent(identifier, new Definition(version, stacking, description, names.get(identifier))) != null) { +- if (factories.putIfAbsent(identifier, new Definition(version, stacking, description, names.get(identifier), factory.get("creativeCategory").getAsInt())) != null) { + final String block = blocks.contains(identifier) ? identifier + : blockAliases.has(identifier) ? blockAliases.get(identifier).getAsString() : null; + final var traits = new ItemCookingProperties(food.contains(identifier), block, replacements.contains(identifier)); -+ if (factories.putIfAbsent(identifier, new Definition(version, stacking, description, names.get(identifier), traits)) != null) { ++ if (factories.putIfAbsent(identifier, new Definition(version, stacking, description, names.get(identifier), factory.get("creativeCategory").getAsInt(), traits)) != null) { throw new IllegalStateException("Duplicate native item factory: " + identifier); } } @@ -7616,8 +7616,8 @@ index 928f2175b..90a202d8a 100644 + private BuiltinItemFactories() { } -- record Definition(ItemVersion version, ItemStackingProperties stacking, String componentDescription, BuiltinItemName ordinaryName) { } -+ record Definition(ItemVersion version, ItemStackingProperties stacking, String componentDescription, BuiltinItemName ordinaryName, ItemCookingProperties cooking) { } +- record Definition(ItemVersion version, ItemStackingProperties stacking, String componentDescription, BuiltinItemName ordinaryName, int creativeCategory) { } ++ record Definition(ItemVersion version, ItemStackingProperties stacking, String componentDescription, BuiltinItemName ordinaryName, int creativeCategory, ItemCookingProperties cooking) { } } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemBlockPlacer.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemBlockPlacer.java @@ -7747,10 +7747,10 @@ index 000000000..19e0148c5 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -index 8e1217d5a..88f258dbb 100644 +index 4a15ae8e0..2b41a4e79 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -@@ -12,6 +12,8 @@ import com.viaversion.nbt.tag.CompoundTag; +@@ -13,6 +13,8 @@ import com.viaversion.nbt.tag.IntTag; import com.viaversion.nbt.tag.StringTag; import com.viaversion.viaversion.util.Key; import net.raphimc.viabedrock.api.model.NativeItemNames; @@ -7759,7 +7759,7 @@ index 8e1217d5a..88f258dbb 100644 import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; import java.util.HashMap; -@@ -20,6 +22,8 @@ import java.util.LinkedHashMap; +@@ -21,6 +23,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -7768,8 +7768,8 @@ index 8e1217d5a..88f258dbb 100644 import java.util.stream.Collectors; /** One connection's item IDs and the complete definitions that own those IDs. */ -@@ -78,6 +82,75 @@ public final class ItemRegistry { - return identifier != null ? this.definitions.get(identifier) : null; +@@ -120,6 +124,75 @@ public final class ItemRegistry { + return Map.copyOf(result); } + /** Named descriptors narrow to signed-short metadata before resolving concrete identity. */ @@ -14560,7 +14560,7 @@ index 000000000..80861941e + +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java -index eaf0eb96f..91dd1e997 100644 +index 2e960c452..69094a65f 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java @@ -6,13 +6,24 @@ @@ -14893,8 +14893,8 @@ index eaf0eb96f..91dd1e997 100644 @Test void legacyReferencesPreserveExistingBuiltinFactoriesAndTheirComponentLayout() { final String identifier = "minecraft:hopper"; -@@ -331,6 +626,24 @@ class ItemRegistryTest { - assertTrue(apple.componentItems().isEmpty()); +@@ -401,6 +696,24 @@ class ItemRegistryTest { + return new ItemEntry(identifier, id, true, version, data); } + @Test diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index 948f6197..f45febfe 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From 2e9b38e77f0d2e76d29b80e4be170c655057f8c9 Mon Sep 17 00:00:00 2001 +From ab2cc63124ce483a79ce1910370d199e8697abcd Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): signal sustained Bedrock item use @@ -99,10 +99,10 @@ index 000000000..c4e63834c + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -index 88f258dbb..33a3cd66c 100644 +index 2b41a4e79..0f480838d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -@@ -223,6 +223,20 @@ public final class ItemRegistry { +@@ -265,6 +265,20 @@ public final class ItemRegistry { return "item." + Key.stripMinecraftNamespace(identifier); } diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index 6b8a1f90..c0a124ce 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From dc5bd4b9362ba5cc60c93f4d572481cf7676c6bd Mon Sep 17 00:00:00 2001 +From 46baa49d1180225f6709a60620b55e0b8191f153 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -26,24 +26,39 @@ bytecode emits this slot for creative-menu Q. Geyser PlayerInventoryTranslator documents the same native created-output slot 50 flow since Bedrock 1.18.2. Actual packet tests cover plate and stone counts, pending-close queuing, and invalid stack bounds. + +Build the player search source from the current connection's complete +creative registry. Native Player rebuild14020ec60 admits definition +categories one through four in registry order; it does not use group +category, Java fallback identity, stack count or crafting-grid size. +Keep exact entry references, duplicates, variants and source order in a +separate immutable list, alongside the complete creative palette. +ItemRewriter supplies the connection registry on every catalog receipt. + +The complete original rebuild and vector growth/cleanup execute 906 +controls, including every category byte, absent definitions, duplicates, +allocation boundaries and repeated replacement. Item copy/destruction, +allocation and CRT boundaries are supplied. Text, unlock and craftability +evaluators, grouped category sources and authored player screen/input +integration remain separate work. --- - .../protocol/model/CreativeCatalog.java | 142 ++++++++++ + .../protocol/model/CreativeCatalog.java | 152 ++++++++++ .../protocol/packet/InventoryPackets.java | 198 ++++++++----- .../protocol/rewriter/ItemRewriter.java | 31 +- .../storage/InventoryRequestTracker.java | 24 ++ - .../protocol/model/CreativeCatalogTest.java | 109 +++++++ + .../protocol/model/CreativeCatalogTest.java | 145 ++++++++++ .../protocol/packet/InventoryPacketsTest.java | 266 ++++++++++++++---- .../storage/InventoryRequestTrackerTest.java | 83 ++++++ - 7 files changed, 715 insertions(+), 138 deletions(-) + 7 files changed, 761 insertions(+), 138 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java new file mode 100644 -index 000000000..fcbaa4e46 +index 000000000..afece9faa --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java -@@ -0,0 +1,142 @@ +@@ -0,0 +1,152 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -79,14 +94,15 @@ index 000000000..fcbaa4e46 +public final class CreativeCatalog { + + private static final String ENTRY_KEY = "viabedrock:creative_entry"; -+ private static final CreativeCatalog EMPTY = new CreativeCatalog(List.of(), List.of()); ++ private static final CreativeCatalog EMPTY = new CreativeCatalog(List.of(), List.of(), new ItemRegistry(new ItemEntry[0])); + + private final List groups; + private final List entries; ++ private final List searchEntries; + private final Int2ObjectMap byEntryId = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap byJavaItemId = new Int2ObjectOpenHashMap<>(); + -+ public CreativeCatalog(final List groups, final List entries) { ++ public CreativeCatalog(final List groups, final List entries, final ItemRegistry registry) { + this.groups = List.copyOf(groups); + final List marked = new ArrayList<>(entries.size()); + for (final Entry entry : entries) { @@ -106,6 +122,10 @@ index 000000000..fcbaa4e46 + } + } + this.entries = List.copyOf(marked); ++ this.searchEntries = this.entries.stream().filter(entry -> { ++ final int category = registry.creativeCategory(entry.bedrockItem().identifier()); ++ return category >= 1 && category <= 4; ++ }).toList(); + } + + public static CreativeCatalog empty() { @@ -120,6 +140,11 @@ index 000000000..fcbaa4e46 + return this.entries; + } + ++ /** Complete player search source before text, unlock and craftability evaluation. */ ++ public List searchEntries() { ++ return this.searchEntries; ++ } ++ + public boolean containsJavaItem(final int identifier) { + return this.byJavaItemId.containsKey(identifier); + } @@ -451,7 +476,7 @@ index a1a76a4d9..4cec1d2cb 100644 public static boolean handleLegacyOffhandClick(final UserConnection user, final Container container, diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index ef32aca46..a9540e7a7 100644 +index ef32aca46..28475b5d9 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -50,6 +50,7 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; @@ -512,7 +537,7 @@ index ef32aca46..a9540e7a7 100644 - return result; + public void setCreativeCatalog(final List groups, + final List entries) { -+ this.creativeCatalog = new CreativeCatalog(groups, entries); ++ this.creativeCatalog = new CreativeCatalog(groups, entries, this.registry); } public BedrockItem[] bedrockItems(final Item[] javaItems) { @@ -567,13 +592,14 @@ index d08578907..8544e1ab3 100644 diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java new file mode 100644 -index 000000000..dfb8c95ff +index 000000000..5a946d77c --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java -@@ -0,0 +1,109 @@ +@@ -0,0 +1,145 @@ +package net.raphimc.viabedrock.protocol.model; + +import com.viaversion.nbt.tag.CompoundTag; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; +import com.viaversion.viaversion.api.data.FullMappings; +import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer; +import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; @@ -583,11 +609,14 @@ index 000000000..dfb8c95ff +import com.viaversion.viaversion.api.minecraft.item.data.PotionEffect; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; ++import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; ++import static org.junit.jupiter.api.Assertions.assertSame; ++import static org.junit.jupiter.api.Assertions.assertThrows; + +class CreativeCatalogTest { + @@ -596,7 +625,7 @@ index 000000000..dfb8c95ff + final Item paper = javaItem(1); + final CreativeCatalog catalog = new CreativeCatalog(List.of(), List.of( + new CreativeCatalog.Entry(11, 0, new BedrockItem(100), paper, true, false), -+ new CreativeCatalog.Entry(12, 0, new BedrockItem(101), paper, true, false))); ++ new CreativeCatalog.Entry(12, 0, new BedrockItem(101), paper, true, false)), new ItemRegistry(new ItemEntry[0])); + + assertEquals(100, catalog.creativeEntry(catalog.entries().get(0).javaItem()).bedrockItem().identifier()); + assertEquals(101, catalog.creativeEntry(catalog.entries().get(1).javaItem()).bedrockItem().identifier()); @@ -613,7 +642,7 @@ index 000000000..dfb8c95ff + final Item mapped = javaItem(2); + final CreativeCatalog catalog = new CreativeCatalog(List.of(), List.of( + new CreativeCatalog.Entry(21, 0, new BedrockItem(200), mapped, true, false), -+ new CreativeCatalog.Entry(22, 0, new BedrockItem(201), mapped, false, true))); ++ new CreativeCatalog.Entry(22, 0, new BedrockItem(201), mapped, false, true)), new ItemRegistry(new ItemEntry[0])); + + assertEquals(201, catalog.creativeEntry(mapped).bedrockItem().identifier()); + assertEquals(22, catalog.creativeEntry(mapped).netId()); @@ -635,7 +664,7 @@ index 000000000..dfb8c95ff + final BedrockItem nativeOozing = new BedrockItem(100, (short) 43, (byte) 1, null); + final CreativeCatalog catalog = new CreativeCatalog(List.of(), List.of( + new CreativeCatalog.Entry(11, 0, nativeStrength, strength, false, true), -+ new CreativeCatalog.Entry(12, 0, nativeOozing, oozing, false, true))); ++ new CreativeCatalog.Entry(12, 0, nativeOozing, oozing, false, true)), new ItemRegistry(new ItemEntry[0])); + assertEquals(11, catalog.creativeEntry(strength.copy()).netId()); + assertEquals(12, catalog.creativeEntry(oozing.copy()).netId()); + assertNull(catalog.creativeEntry(javaItem(3))); @@ -658,7 +687,7 @@ index 000000000..dfb8c95ff + final var second = new BedrockItem(100, (short) 0, (byte) 1, secondTag); + final var catalog = new CreativeCatalog(List.of(), List.of( + new CreativeCatalog.Entry(31, 0, first, item, false, true), -+ new CreativeCatalog.Entry(32, 0, second, item, false, true))); ++ new CreativeCatalog.Entry(32, 0, second, item, false, true)), new ItemRegistry(new ItemEntry[0])); + assertNull(catalog.creativeEntry(item)); + final Item owned = javaItem(5); + final var context = new CompoundTag(); @@ -669,6 +698,38 @@ index 000000000..dfb8c95ff + assertEquals(31, catalog.creativeEntry(catalog.entries().getFirst().javaItem()).netId()); + } + ++ @Test ++ void searchRetainsDefinitionCategoriesInRegistryOrderAndPreservesEntryIdentity() throws ReflectiveOperationException { ++ final var definitions = new ArrayList(); ++ for (int category = 0; category <= 6; category++) { ++ final var properties = new CompoundTag(); ++ properties.putInt("creative_category", category); ++ final var components = new CompoundTag(); ++ components.put("item_properties", properties); ++ final var data = new CompoundTag(); ++ data.put("components", components); ++ definitions.add(new ItemEntry("test:category" + category, 100 + category, true, ++ ItemVersion.DataDriven, data)); ++ } ++ final var registry = new ItemRegistry(definitions.toArray(ItemEntry[]::new)); ++ final var entries = new ArrayList(); ++ for (final int identifier : new int[]{104, 100, 103, 101, 104, 200, 102, 105, 106}) { ++ entries.add(new CreativeCatalog.Entry(entries.size() + 1, 999, ++ new BedrockItem(identifier, (short) 7, (byte) (identifier == 102 ? 0 : 64)), javaItem(1), true, false)); ++ } ++ final var catalog = new CreativeCatalog(List.of(new CreativeCatalog.Group(5, "", javaItem(1))), entries, registry); ++ assertEquals(List.of(1, 3, 4, 5, 7), catalog.searchEntries().stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertEquals(9, catalog.entries().size()); ++ assertEquals(0, catalog.searchEntries().getLast().bedrockItem().amount()); ++ for (final var entry : catalog.searchEntries()) { ++ assertSame(catalog.entries().get(entry.netId() - 1), entry); ++ } ++ assertThrows(UnsupportedOperationException.class, () -> catalog.searchEntries().clear()); ++ final var replaced = new CreativeCatalog(catalog.groups(), catalog.entries(), new ItemRegistry(new ItemEntry[0])); ++ assertEquals(0, replaced.searchEntries().size()); ++ assertEquals(5, catalog.searchEntries().size()); ++ } ++ + private static Item javaItem(final int identifier) throws ReflectiveOperationException { + final StructuredDataContainer data = new StructuredDataContainer(); + final FullMappings mappings = (FullMappings) Proxy.newProxyInstance(FullMappings.class.getClassLoader(), diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch index 7ae4a96a..bc4bc9a5 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch @@ -1,4 +1,4 @@ -From fa3c6f67fb2e9dca5ea41b27d8dd05ca61998c64 Mon Sep 17 00:00:00 2001 +From cc990851f6ce237c8b950a180188f4c85bd4552f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): resolve authored Bedrock server UI scenes @@ -524,6 +524,22 @@ recipe hashes are disjoint from every 16-bit auxiliary value of the actual compound constructor. Keep this provider bound to that constructor and retain component reconstruction and current runtime-owner admission before dispatch. The general cross-item compound classifier remains a separate native concern. + +Retain the definition-owned category byte for the player search source. +The pinned native registry initializer supplies all 1,949 factory defaults. +New definitions start at category four, and explicit component construction +replaces builtin state before ordered component updates. Only a compound +item_properties rewrites creative_category, using the integer's low byte +or zero for absent/mistyped values. Legacy readers preserve the category; +unknown None entries receive their fallback after updates. + +Complete native component construction, binary NBT parsing, property +loading and destruction cover 279 inputs and five retained-update steps. +The complete legacy reader preserves its constructor category across the +same inputs. Builtin initialization uses the retained native block +bootstrap and an empty network vector, establishing constructor defaults +rather than a server packet baseline. Recipe availability and player +screen integration remain separate work. --- Data Asset Sources.md | 33 + .../chunk/blockstate/BuiltinBlockNames.java | 46 + @@ -548,7 +564,7 @@ The general cross-item compound classifier remains a separate native concern. .../ComponentItemStackingProperties.java | 40 + .../viabedrock/protocol/model/ItemEntry.java | 10 + .../protocol/model/ItemNameContext.java | 23 + - .../protocol/model/ItemRegistry.java | 231 + + .../protocol/model/ItemRegistry.java | 273 + .../model/ItemStackingProperties.java | 36 + .../model/LegacyItemStackingProperties.java | 54 + .../protocol/packet/HudPackets.java | 51 +- @@ -566,7 +582,7 @@ The general cross-item compound classifier remains a separate native concern. .../storage/NativeHudActionbarState.java | 37 + .../protocol/storage/NativeHudTitleState.java | 81 + .../data/bedrock/block_name_keys.json | 1385 +++ - .../data/bedrock/item_factories.json | 10012 ++++++++++++++++ + .../data/bedrock/item_factories.json | 10208 ++++++++++++++++ .../viabedrock/data/bedrock/potion_names.json | 193 + .../api/model/NativeItemNamesTest.java | 62 + .../api/model/NativeItemStackingTest.java | 149 + @@ -578,13 +594,13 @@ The general cross-item compound classifier remains a separate native concern. .../protocol/model/BuiltinItemNameTest.java | 159 + .../model/ItemRegistryComponentNamesTest.java | 84 + .../model/ItemRegistryOrdinaryNamesTest.java | 72 + - .../protocol/model/ItemRegistryTest.java | 338 + + .../protocol/model/ItemRegistryTest.java | 408 + .../packet/ModalFormTransitionTest.java | 123 + .../NativeContainerOpeningPacketTest.java | 485 + .../protocol/packet/NativeHudPacketTest.java | 160 + .../packet/NpcDialoguePacketTest.java | 301 +- .../storage/NativeContainerUiStateTest.java | 85 + - 59 files changed, 17166 insertions(+), 346 deletions(-) + 59 files changed, 17474 insertions(+), 346 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BuiltinBlockNames.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemStacking.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeContainerActionSlots.java @@ -1495,7 +1511,7 @@ index a17eab494..20cf478fb 100644 public boolean isDifferent(final BedrockItem o) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java new file mode 100644 -index 000000000..928f2175b +index 000000000..3a5180c25 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemFactories.java @@ -0,0 +1,89 @@ @@ -1572,7 +1588,7 @@ index 000000000..928f2175b + for (final var name : factory.getAsJsonArray("identifiers")) { + final String identifier = name.getAsString(); + final String description = componentDescriptions.has(identifier) ? componentDescriptions.get(identifier).getAsString() : null; -+ if (factories.putIfAbsent(identifier, new Definition(version, stacking, description, names.get(identifier))) != null) { ++ if (factories.putIfAbsent(identifier, new Definition(version, stacking, description, names.get(identifier), factory.get("creativeCategory").getAsInt())) != null) { + throw new IllegalStateException("Duplicate native item factory: " + identifier); + } + } @@ -1585,7 +1601,7 @@ index 000000000..928f2175b + + private BuiltinItemFactories() { } + -+ record Definition(ItemVersion version, ItemStackingProperties stacking, String componentDescription, BuiltinItemName ordinaryName) { } ++ record Definition(ItemVersion version, ItemStackingProperties stacking, String componentDescription, BuiltinItemName ordinaryName, int creativeCategory) { } + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemName.java b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemName.java @@ -2003,10 +2019,10 @@ index 000000000..20d57645c +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java new file mode 100644 -index 000000000..8e1217d5a +index 000000000..4a15ae8e0 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -@@ -0,0 +1,231 @@ +@@ -0,0 +1,273 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2018,6 +2034,7 @@ index 000000000..8e1217d5a +import com.google.common.collect.HashBiMap; +import com.google.common.collect.ImmutableBiMap; +import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.nbt.tag.IntTag; +import com.viaversion.nbt.tag.StringTag; +import com.viaversion.viaversion.util.Key; +import net.raphimc.viabedrock.api.model.NativeItemNames; @@ -2040,6 +2057,7 @@ index 000000000..8e1217d5a + private final List registrations; + private final boolean writtenBookNames; + private final Map componentDescriptions; ++ private final Map creativeCategories; + + public ItemRegistry(final ItemEntry[] entries) { + final BiMap ids = HashBiMap.create(entries.length); @@ -2063,6 +2081,7 @@ index 000000000..8e1217d5a + this.entries = List.copyOf(definitions.values()); + this.writtenBookNames = this.factories().get("minecraft:written_book") == ItemVersion.Legacy; + this.componentDescriptions = this.resolveComponentDescriptions(); ++ this.creativeCategories = this.resolveCreativeCategories(); + } + + public BiMap ids() { @@ -2087,6 +2106,45 @@ index 000000000..8e1217d5a + return identifier != null ? this.definitions.get(identifier) : null; + } + ++ /** The constructed definition owns search admission, independently of creative group metadata. */ ++ public int creativeCategory(final int id) { ++ final short narrowed = (short) id; ++ final String identifier = narrowed == 0 || narrowed == -1 ? null : this.ids.inverse().get((int) narrowed); ++ return identifier != null ? this.creativeCategories.get(identifier) : 0; ++ } ++ ++ private Map resolveCreativeCategories() { ++ final Map categories = new HashMap<>(); ++ for (final ItemEntry entry : this.registrations) { ++ final BuiltinItemFactories.Definition builtin = BuiltinItemFactories.get(entry.identifier()); ++ if (builtin != null) { ++ categories.putIfAbsent(entry.identifier(), builtin.creativeCategory()); ++ } ++ } ++ for (final var construction : this.constructionVersions().entrySet()) { ++ if (construction.getValue() == ItemVersion.DataDriven) { ++ categories.put(construction.getKey(), 4); ++ } else { ++ categories.putIfAbsent(construction.getKey(), 4); ++ } ++ } ++ final Map factories = this.factories(); ++ for (final ItemEntry entry : this.registrations) { ++ final CompoundTag data = entry.componentData(); ++ if (categories.containsKey(entry.identifier()) && factories.get(entry.identifier()) == ItemVersion.DataDriven ++ && data != null && data.get("components") instanceof CompoundTag components ++ && components.get("item_properties") instanceof CompoundTag properties) { ++ categories.put(entry.identifier(), properties.get("creative_category") instanceof IntTag category ? category.asInt() & 0xFF : 0); ++ } ++ } ++ final Map result = new HashMap<>(); ++ for (final String identifier : this.ids.keySet()) { ++ // Unknown None entries receive their fallback definition after component updates. ++ result.put(identifier, categories.getOrDefault(identifier, 4)); ++ } ++ return Map.copyOf(result); ++ } ++ + /** The constructed item class owns custom-name dispatch, independently of its final network flags. */ + public String customName(final String identifier, final CompoundTag data) { + if (!this.ids.containsKey(identifier)) { @@ -5814,10 +5872,10 @@ index 000000000..5269489a9 +} diff --git a/src/main/resources/assets/viabedrock/data/bedrock/item_factories.json b/src/main/resources/assets/viabedrock/data/bedrock/item_factories.json new file mode 100644 -index 000000000..a5147cdb6 +index 000000000..0a650c834 --- /dev/null +++ b/src/main/resources/assets/viabedrock/data/bedrock/item_factories.json -@@ -0,0 +1,10012 @@ +@@ -0,0 +1,10208 @@ +{ + "format": 1, + "gameVersion": "1.26.51.1", @@ -5829,6 +5887,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:black_harness", + "minecraft:black_shulker_box", @@ -5838,7 +5897,6 @@ index 000000000..a5147cdb6 + "minecraft:brown_harness", + "minecraft:brown_shulker_box", + "minecraft:chest_minecart", -+ "minecraft:command_block_minecart", + "minecraft:copper_horse_armor", + "minecraft:copper_nautilus_armor", + "minecraft:creeper_banner_pattern", @@ -5877,11 +5935,9 @@ index 000000000..a5147cdb6 + "minecraft:netherite_nautilus_armor", + "minecraft:orange_harness", + "minecraft:orange_shulker_box", -+ "minecraft:photo_item", + "minecraft:piglin_banner_pattern", + "minecraft:pink_harness", + "minecraft:pink_shulker_box", -+ "minecraft:portfolio", + "minecraft:purple_harness", + "minecraft:purple_shulker_box", + "minecraft:red_harness", @@ -5904,8 +5960,32 @@ index 000000000..a5147cdb6 + "version": 0, + "maximum": 1, + "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 5, ++ "identifiers": [ ++ "minecraft:command_block_minecart" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 1, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 6, ++ "identifiers": [ ++ "minecraft:photo_item", ++ "minecraft:portfolio" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 1, ++ "stackedByData": false, + "maximumDamage": 195, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:chainmail_boots", + "minecraft:iron_boots" @@ -5917,6 +5997,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 240, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:chainmail_chestplate", + "minecraft:iron_chestplate" @@ -5928,6 +6009,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 165, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:chainmail_helmet", + "minecraft:copper_leggings", @@ -5940,6 +6022,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 225, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:chainmail_leggings", + "minecraft:iron_leggings" @@ -5951,6 +6034,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 143, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:copper_boots" + ] @@ -5961,6 +6045,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 176, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:copper_chestplate" + ] @@ -5971,6 +6056,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 121, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:copper_helmet" + ] @@ -5981,6 +6067,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 429, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:diamond_boots" + ] @@ -5991,6 +6078,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 528, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:diamond_chestplate" + ] @@ -6001,6 +6089,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 363, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:diamond_helmet" + ] @@ -6011,6 +6100,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 495, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:diamond_leggings" + ] @@ -6021,6 +6111,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 432, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:elytra" + ] @@ -6031,6 +6122,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 91, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:golden_boots" + ] @@ -6041,6 +6133,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 112, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:golden_chestplate" + ] @@ -6051,6 +6144,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 77, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:golden_helmet" + ] @@ -6061,6 +6155,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 105, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:golden_leggings" + ] @@ -6071,6 +6166,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 65, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:leather_boots" + ] @@ -6081,6 +6177,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 80, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:leather_chestplate" + ] @@ -6091,6 +6188,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 55, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:leather_helmet" + ] @@ -6101,6 +6199,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 75, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:leather_leggings" + ] @@ -6111,6 +6210,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 481, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:netherite_boots" + ] @@ -6121,6 +6221,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 592, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:netherite_chestplate" + ] @@ -6131,6 +6232,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 407, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:netherite_helmet" + ] @@ -6141,6 +6243,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 555, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:netherite_leggings" + ] @@ -6151,6 +6254,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 275, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:turtle_helmet" + ] @@ -6162,6 +6266,7 @@ index 000000000..a5147cdb6 + "maximumDamage": 0, + "damageable": false, + "forcedSubtype": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:acacia_boat", + "minecraft:acacia_chest_boat", @@ -6193,6 +6298,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 26, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:carrot_on_a_stick" + ] @@ -6203,6 +6309,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 32, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:golden_axe", + "minecraft:golden_hoe", @@ -6217,6 +6324,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 59, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:wooden_axe", + "minecraft:wooden_hoe", @@ -6231,6 +6339,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 64, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:brush", + "minecraft:flint_and_steel" @@ -6242,6 +6351,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 131, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:stone_axe", + "minecraft:stone_hoe", @@ -6256,6 +6366,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 190, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:copper_axe", + "minecraft:copper_hoe", @@ -6270,6 +6381,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 238, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:shears" + ] @@ -6280,6 +6392,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 250, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:iron_axe", + "minecraft:iron_hoe", @@ -6295,6 +6408,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 336, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:shield" + ] @@ -6305,6 +6419,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 384, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:bow" + ] @@ -6315,6 +6430,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 464, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:crossbow" + ] @@ -6325,6 +6441,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 500, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:mace" + ] @@ -6335,6 +6452,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 1561, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:diamond_axe", + "minecraft:diamond_hoe", @@ -6349,6 +6467,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 2031, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:netherite_axe", + "minecraft:netherite_hoe", @@ -6363,10 +6482,10 @@ index 000000000..a5147cdb6 + "stackedByData": true, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:bed", + "minecraft:lingering_potion", -+ "minecraft:medicine", + "minecraft:potion", + "minecraft:splash_potion" + ] @@ -6375,8 +6494,20 @@ index 000000000..a5147cdb6 + "version": 0, + "maximum": 1, + "stackedByData": true, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 6, ++ "identifiers": [ ++ "minecraft:medicine" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 1, ++ "stackedByData": true, + "maximumDamage": 100, + "damageable": true, ++ "creativeCategory": 6, + "identifiers": [ + "minecraft:glow_stick", + "minecraft:sparkler" @@ -6388,6 +6519,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:acacia_hanging_sign", + "minecraft:acacia_sign", @@ -6411,7 +6543,6 @@ index 000000000..a5147cdb6 + "minecraft:ender_pearl", + "minecraft:gray_cushion", + "minecraft:green_cushion", -+ "minecraft:ice_bomb", + "minecraft:jungle_hanging_sign", + "minecraft:jungle_sign", + "minecraft:light_blue_cushion", @@ -6436,18 +6567,40 @@ index 000000000..a5147cdb6 + "minecraft:warped_hanging_sign", + "minecraft:warped_sign", + "minecraft:white_cushion", -+ "minecraft:written_book", + "minecraft:yellow_cushion" + ] + }, + { + "version": 0, + "maximum": 16, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 6, ++ "identifiers": [ ++ "minecraft:ice_bomb", ++ "minecraft:written_book" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 16, ++ "stackedByData": true, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 4, ++ "identifiers": [ ++ "minecraft:banner" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 16, + "stackedByData": true, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 6, + "identifiers": [ -+ "minecraft:banner", + "minecraft:board" + ] + }, @@ -6457,241 +6610,675 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 0, ++ "identifiers": [ ++ "minecraft:bamboo_sapling", ++ "minecraft:bubble_column", ++ "minecraft:chiseled_cinnabar", ++ "minecraft:cinnabar", ++ "minecraft:cinnabar_brick_stairs", ++ "minecraft:cinnabar_brick_wall", ++ "minecraft:cinnabar_bricks", ++ "minecraft:cinnabar_stairs", ++ "minecraft:cinnabar_wall", ++ "minecraft:end_portal", ++ "minecraft:golden_dandelion", ++ "minecraft:item.brewing_stand", ++ "minecraft:lit_deepslate_redstone_ore", ++ "minecraft:lit_redstone_lamp", ++ "minecraft:lit_redstone_ore", ++ "minecraft:piston_arm_collision", ++ "minecraft:polished_cinnabar", ++ "minecraft:polished_cinnabar_stairs", ++ "minecraft:polished_cinnabar_wall", ++ "minecraft:powered_comparator", ++ "minecraft:sticky_piston_arm_collision", ++ "minecraft:unknown", ++ "minecraft:unpowered_comparator" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 1, + "identifiers": [ -+ "minecraft:acacia_button", -+ "minecraft:acacia_door", -+ "minecraft:acacia_fence", + "minecraft:acacia_fence_gate", -+ "minecraft:acacia_leaves", -+ "minecraft:acacia_log", -+ "minecraft:acacia_planks", -+ "minecraft:acacia_pressure_plate", -+ "minecraft:acacia_shelf", + "minecraft:acacia_stairs", -+ "minecraft:acacia_standing_sign", + "minecraft:acacia_trapdoor", -+ "minecraft:acacia_wall_sign", -+ "minecraft:acacia_wood", -+ "minecraft:activator_rail", -+ "minecraft:air", -+ "minecraft:allium", -+ "minecraft:allow", -+ "minecraft:amethyst_block", -+ "minecraft:amethyst_cluster", -+ "minecraft:amethyst_shard", -+ "minecraft:ancient_debris", -+ "minecraft:andesite", + "minecraft:andesite_stairs", -+ "minecraft:andesite_wall", -+ "minecraft:angler_pottery_sherd", -+ "minecraft:anvil", -+ "minecraft:apple", -+ "minecraft:archer_pottery_sherd", -+ "minecraft:armadillo_scute", -+ "minecraft:armor_stand", -+ "minecraft:arms_up_pottery_sherd", -+ "minecraft:azalea", -+ "minecraft:azure_bluet", -+ "minecraft:baked_potato", -+ "minecraft:bamboo", -+ "minecraft:bamboo_block", -+ "minecraft:bamboo_button", -+ "minecraft:bamboo_door", + "minecraft:bamboo_fence", + "minecraft:bamboo_fence_gate", + "minecraft:bamboo_mosaic", + "minecraft:bamboo_mosaic_stairs", + "minecraft:bamboo_planks", -+ "minecraft:bamboo_pressure_plate", -+ "minecraft:bamboo_sapling", -+ "minecraft:bamboo_shelf", + "minecraft:bamboo_stairs", -+ "minecraft:bamboo_standing_sign", + "minecraft:bamboo_trapdoor", -+ "minecraft:bamboo_wall_sign", -+ "minecraft:banner_pattern", -+ "minecraft:barrel", -+ "minecraft:barrier", -+ "minecraft:basalt", -+ "minecraft:beacon", -+ "minecraft:bedrock", -+ "minecraft:bee_nest", -+ "minecraft:beef", -+ "minecraft:beehive", -+ "minecraft:beetroot", -+ "minecraft:beetroot_seeds", -+ "minecraft:beetroot_soup", -+ "minecraft:bell", -+ "minecraft:big_dripleaf", -+ "minecraft:birch_button", -+ "minecraft:birch_door", -+ "minecraft:birch_fence", + "minecraft:birch_fence_gate", -+ "minecraft:birch_leaves", -+ "minecraft:birch_log", -+ "minecraft:birch_planks", -+ "minecraft:birch_pressure_plate", -+ "minecraft:birch_shelf", + "minecraft:birch_stairs", -+ "minecraft:birch_standing_sign", + "minecraft:birch_trapdoor", -+ "minecraft:birch_wall_sign", -+ "minecraft:birch_wood", -+ "minecraft:black_candle", -+ "minecraft:black_candle_cake", -+ "minecraft:black_carpet", -+ "minecraft:black_concrete", + "minecraft:black_concrete_powder", + "minecraft:black_glazed_terracotta", -+ "minecraft:black_stained_glass", -+ "minecraft:black_stained_glass_pane", -+ "minecraft:black_terracotta", -+ "minecraft:black_wool", -+ "minecraft:blackstone", + "minecraft:blackstone_stairs", + "minecraft:blackstone_wall", -+ "minecraft:blade_pottery_sherd", -+ "minecraft:blast_furnace", -+ "minecraft:blaze_powder", -+ "minecraft:blaze_rod", -+ "minecraft:bleach", -+ "minecraft:blue_candle", -+ "minecraft:blue_candle_cake", -+ "minecraft:blue_carpet", -+ "minecraft:blue_concrete", + "minecraft:blue_concrete_powder", + "minecraft:blue_glazed_terracotta", -+ "minecraft:blue_ice", -+ "minecraft:blue_orchid", -+ "minecraft:blue_stained_glass", -+ "minecraft:blue_stained_glass_pane", -+ "minecraft:blue_terracotta", -+ "minecraft:blue_wool", -+ "minecraft:boat", -+ "minecraft:bolt_armor_trim_smithing_template", -+ "minecraft:bone", + "minecraft:bone_block", -+ "minecraft:book", -+ "minecraft:bookshelf", -+ "minecraft:border_block", -+ "minecraft:bowl", -+ "minecraft:brain_coral", -+ "minecraft:brain_coral_block", -+ "minecraft:brain_coral_wall_fan", -+ "minecraft:bread", -+ "minecraft:breeze_rod", -+ "minecraft:brewer_pottery_sherd", -+ "minecraft:brick", + "minecraft:brick_block", + "minecraft:brick_stairs", -+ "minecraft:brick_wall", -+ "minecraft:brown_candle", -+ "minecraft:brown_candle_cake", -+ "minecraft:brown_carpet", -+ "minecraft:brown_concrete", + "minecraft:brown_concrete_powder", + "minecraft:brown_glazed_terracotta", -+ "minecraft:brown_mushroom", -+ "minecraft:brown_stained_glass", -+ "minecraft:brown_stained_glass_pane", -+ "minecraft:brown_terracotta", -+ "minecraft:brown_wool", -+ "minecraft:bubble_column", -+ "minecraft:bubble_coral", -+ "minecraft:bubble_coral_block", -+ "minecraft:bubble_coral_wall_fan", -+ "minecraft:budding_amethyst", -+ "minecraft:burn_pottery_sherd", -+ "minecraft:bush", -+ "minecraft:cactus", -+ "minecraft:cactus_flower", -+ "minecraft:calcite", -+ "minecraft:calibrated_sculk_sensor", -+ "minecraft:camera", -+ "minecraft:candle", -+ "minecraft:candle_cake", -+ "minecraft:carpet", -+ "minecraft:carrot", -+ "minecraft:carrots", -+ "minecraft:cartography_table", -+ "minecraft:carved_pumpkin", -+ "minecraft:cave_vines", -+ "minecraft:cave_vines_body_with_berries", -+ "minecraft:cave_vines_head_with_berries", -+ "minecraft:chain_command_block", -+ "minecraft:chalkboard", -+ "minecraft:chemical_heat", -+ "minecraft:chemistry_table", -+ "minecraft:cherry_button", -+ "minecraft:cherry_door", + "minecraft:cherry_fence", + "minecraft:cherry_fence_gate", -+ "minecraft:cherry_leaves", -+ "minecraft:cherry_log", + "minecraft:cherry_planks", -+ "minecraft:cherry_pressure_plate", -+ "minecraft:cherry_sapling", -+ "minecraft:cherry_shelf", + "minecraft:cherry_stairs", -+ "minecraft:cherry_standing_sign", + "minecraft:cherry_trapdoor", -+ "minecraft:cherry_wall_sign", -+ "minecraft:cherry_wood", -+ "minecraft:chest", -+ "minecraft:chest_boat", ++ "minecraft:chiseled_copper", ++ "minecraft:chiseled_deepslate", ++ "minecraft:chiseled_nether_bricks", ++ "minecraft:chiseled_polished_blackstone", ++ "minecraft:chiseled_resin_bricks", ++ "minecraft:chiseled_tuff", ++ "minecraft:chiseled_tuff_bricks", ++ "minecraft:coal_block", ++ "minecraft:cobbled_deepslate", ++ "minecraft:cobbled_deepslate_stairs", ++ "minecraft:cobbled_deepslate_wall", ++ "minecraft:cobblestone", ++ "minecraft:copper_bars", ++ "minecraft:copper_bulb", ++ "minecraft:copper_chain", ++ "minecraft:copper_golem_statue", ++ "minecraft:copper_grate", ++ "minecraft:copper_trapdoor", ++ "minecraft:cracked_deepslate_bricks", ++ "minecraft:cracked_deepslate_tiles", ++ "minecraft:cracked_nether_bricks", ++ "minecraft:cracked_polished_blackstone_bricks", ++ "minecraft:crimson_fence", ++ "minecraft:crimson_fence_gate", ++ "minecraft:crimson_planks", ++ "minecraft:crimson_stairs", ++ "minecraft:crimson_trapdoor", ++ "minecraft:cyan_concrete_powder", ++ "minecraft:cyan_glazed_terracotta", ++ "minecraft:dark_oak_fence_gate", ++ "minecraft:dark_oak_stairs", ++ "minecraft:dark_oak_trapdoor", ++ "minecraft:dark_prismarine_stairs", ++ "minecraft:deepslate_brick_stairs", ++ "minecraft:deepslate_brick_wall", ++ "minecraft:deepslate_bricks", ++ "minecraft:deepslate_tile_stairs", ++ "minecraft:deepslate_tile_wall", ++ "minecraft:deepslate_tiles", ++ "minecraft:diamond_block", ++ "minecraft:diorite_stairs", ++ "minecraft:dried_kelp_block", ++ "minecraft:emerald_block", ++ "minecraft:end_brick_stairs", ++ "minecraft:end_bricks", ++ "minecraft:exposed_chiseled_copper", ++ "minecraft:exposed_copper", ++ "minecraft:exposed_copper_bars", ++ "minecraft:exposed_copper_bulb", ++ "minecraft:exposed_copper_chain", ++ "minecraft:exposed_copper_golem_statue", ++ "minecraft:exposed_copper_grate", ++ "minecraft:exposed_copper_trapdoor", ++ "minecraft:exposed_cut_copper", ++ "minecraft:exposed_cut_copper_stairs", ++ "minecraft:fence_gate", ++ "minecraft:gilded_blackstone", ++ "minecraft:glass", ++ "minecraft:glass_pane", ++ "minecraft:gold_block", ++ "minecraft:granite_stairs", ++ "minecraft:gray_concrete_powder", ++ "minecraft:gray_glazed_terracotta", ++ "minecraft:green_concrete_powder", ++ "minecraft:green_glazed_terracotta", ++ "minecraft:hardened_clay", ++ "minecraft:hay_block", ++ "minecraft:honey_block", ++ "minecraft:honeycomb_block", ++ "minecraft:iron_bars", ++ "minecraft:iron_block", ++ "minecraft:iron_chain", ++ "minecraft:iron_trapdoor", ++ "minecraft:item.nether_wart", ++ "minecraft:jungle_fence_gate", ++ "minecraft:jungle_stairs", ++ "minecraft:jungle_trapdoor", ++ "minecraft:ladder", ++ "minecraft:lapis_block", ++ "minecraft:light_blue_concrete_powder", ++ "minecraft:light_blue_glazed_terracotta", ++ "minecraft:light_gray_concrete_powder", ++ "minecraft:lime_concrete_powder", ++ "minecraft:lime_glazed_terracotta", ++ "minecraft:lodestone", ++ "minecraft:magenta_concrete_powder", ++ "minecraft:magenta_glazed_terracotta", ++ "minecraft:mangrove_fence", ++ "minecraft:mangrove_fence_gate", ++ "minecraft:mangrove_planks", ++ "minecraft:mangrove_stairs", ++ "minecraft:mangrove_trapdoor", ++ "minecraft:mossy_cobblestone", ++ "minecraft:mossy_cobblestone_stairs", ++ "minecraft:mossy_stone_brick_stairs", ++ "minecraft:mud_brick_stairs", ++ "minecraft:mud_brick_wall", ++ "minecraft:mud_bricks", ++ "minecraft:nether_brick", ++ "minecraft:nether_brick_fence", ++ "minecraft:nether_brick_stairs", ++ "minecraft:normal_stone_stairs", ++ "minecraft:oak_stairs", ++ "minecraft:orange_concrete_powder", ++ "minecraft:orange_glazed_terracotta", ++ "minecraft:oxidized_chiseled_copper", ++ "minecraft:oxidized_copper", ++ "minecraft:oxidized_copper_bars", ++ "minecraft:oxidized_copper_bulb", ++ "minecraft:oxidized_copper_chain", ++ "minecraft:oxidized_copper_golem_statue", ++ "minecraft:oxidized_copper_grate", ++ "minecraft:oxidized_copper_trapdoor", ++ "minecraft:oxidized_cut_copper", ++ "minecraft:oxidized_cut_copper_stairs", ++ "minecraft:packed_mud", ++ "minecraft:pale_oak_fence", ++ "minecraft:pale_oak_fence_gate", ++ "minecraft:pale_oak_planks", ++ "minecraft:pale_oak_stairs", ++ "minecraft:pale_oak_trapdoor", ++ "minecraft:pink_concrete_powder", ++ "minecraft:pink_glazed_terracotta", ++ "minecraft:polished_andesite_stairs", ++ "minecraft:polished_blackstone_brick_stairs", ++ "minecraft:polished_blackstone_brick_wall", ++ "minecraft:polished_blackstone_bricks", ++ "minecraft:polished_blackstone_stairs", ++ "minecraft:polished_blackstone_wall", ++ "minecraft:polished_deepslate_stairs", ++ "minecraft:polished_deepslate_wall", ++ "minecraft:polished_diorite_stairs", ++ "minecraft:polished_granite_stairs", ++ "minecraft:polished_sulfur_stairs", ++ "minecraft:polished_sulfur_wall", ++ "minecraft:polished_tuff_stairs", ++ "minecraft:polished_tuff_wall", ++ "minecraft:poplar_fence", ++ "minecraft:poplar_fence_gate", ++ "minecraft:poplar_planks", ++ "minecraft:poplar_stairs", ++ "minecraft:poplar_trapdoor", ++ "minecraft:prismarine_bricks_stairs", ++ "minecraft:prismarine_stairs", ++ "minecraft:purple_concrete_powder", ++ "minecraft:purple_glazed_terracotta", ++ "minecraft:purpur_stairs", ++ "minecraft:quartz_bricks", ++ "minecraft:quartz_stairs", ++ "minecraft:raw_copper_block", ++ "minecraft:raw_gold_block", ++ "minecraft:raw_iron_block", ++ "minecraft:red_concrete_powder", ++ "minecraft:red_glazed_terracotta", ++ "minecraft:red_nether_brick", ++ "minecraft:red_nether_brick_stairs", ++ "minecraft:red_sandstone_stairs", ++ "minecraft:resin_block", ++ "minecraft:resin_brick_stairs", ++ "minecraft:resin_brick_wall", ++ "minecraft:resin_bricks", ++ "minecraft:sandstone_stairs", ++ "minecraft:silver_glazed_terracotta", ++ "minecraft:slime", ++ "minecraft:smooth_quartz_stairs", ++ "minecraft:smooth_red_sandstone_stairs", ++ "minecraft:smooth_sandstone_stairs", ++ "minecraft:smooth_stone", ++ "minecraft:spruce_fence_gate", ++ "minecraft:spruce_stairs", ++ "minecraft:spruce_trapdoor", ++ "minecraft:stone_brick_stairs", ++ "minecraft:stone_stairs", ++ "minecraft:sulfur_brick_stairs", ++ "minecraft:sulfur_brick_wall", ++ "minecraft:sulfur_stairs", ++ "minecraft:sulfur_wall", ++ "minecraft:tinted_glass", ++ "minecraft:trapdoor", ++ "minecraft:tuff_brick_stairs", ++ "minecraft:tuff_brick_wall", ++ "minecraft:tuff_bricks", ++ "minecraft:tuff_stairs", ++ "minecraft:tuff_wall", ++ "minecraft:warped_fence", ++ "minecraft:warped_fence_gate", ++ "minecraft:warped_planks", ++ "minecraft:warped_stairs", ++ "minecraft:warped_trapdoor", ++ "minecraft:waxed_chiseled_copper", ++ "minecraft:waxed_copper", ++ "minecraft:waxed_copper_bars", ++ "minecraft:waxed_copper_bulb", ++ "minecraft:waxed_copper_chain", ++ "minecraft:waxed_copper_golem_statue", ++ "minecraft:waxed_copper_grate", ++ "minecraft:waxed_copper_trapdoor", ++ "minecraft:waxed_cut_copper", ++ "minecraft:waxed_cut_copper_stairs", ++ "minecraft:waxed_exposed_chiseled_copper", ++ "minecraft:waxed_exposed_copper", ++ "minecraft:waxed_exposed_copper_bars", ++ "minecraft:waxed_exposed_copper_bulb", ++ "minecraft:waxed_exposed_copper_chain", ++ "minecraft:waxed_exposed_copper_golem_statue", ++ "minecraft:waxed_exposed_copper_grate", ++ "minecraft:waxed_exposed_copper_trapdoor", ++ "minecraft:waxed_exposed_cut_copper", ++ "minecraft:waxed_exposed_cut_copper_stairs", ++ "minecraft:waxed_oxidized_chiseled_copper", ++ "minecraft:waxed_oxidized_copper", ++ "minecraft:waxed_oxidized_copper_bars", ++ "minecraft:waxed_oxidized_copper_bulb", ++ "minecraft:waxed_oxidized_copper_chain", ++ "minecraft:waxed_oxidized_copper_golem_statue", ++ "minecraft:waxed_oxidized_copper_grate", ++ "minecraft:waxed_oxidized_copper_trapdoor", ++ "minecraft:waxed_oxidized_cut_copper", ++ "minecraft:waxed_oxidized_cut_copper_stairs", ++ "minecraft:waxed_weathered_chiseled_copper", ++ "minecraft:waxed_weathered_copper", ++ "minecraft:waxed_weathered_copper_bars", ++ "minecraft:waxed_weathered_copper_bulb", ++ "minecraft:waxed_weathered_copper_chain", ++ "minecraft:waxed_weathered_copper_golem_statue", ++ "minecraft:waxed_weathered_copper_grate", ++ "minecraft:waxed_weathered_copper_trapdoor", ++ "minecraft:waxed_weathered_cut_copper", ++ "minecraft:waxed_weathered_cut_copper_stairs", ++ "minecraft:weathered_chiseled_copper", ++ "minecraft:weathered_copper", ++ "minecraft:weathered_copper_bars", ++ "minecraft:weathered_copper_bulb", ++ "minecraft:weathered_copper_chain", ++ "minecraft:weathered_copper_golem_statue", ++ "minecraft:weathered_copper_grate", ++ "minecraft:weathered_copper_trapdoor", ++ "minecraft:weathered_cut_copper", ++ "minecraft:weathered_cut_copper_stairs", ++ "minecraft:white_concrete_powder", ++ "minecraft:white_glazed_terracotta", ++ "minecraft:yellow_concrete_powder", ++ "minecraft:yellow_glazed_terracotta" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 2, ++ "identifiers": [ ++ "minecraft:amethyst_block", ++ "minecraft:amethyst_cluster", ++ "minecraft:andesite", ++ "minecraft:azalea", ++ "minecraft:bamboo_block", ++ "minecraft:basalt", ++ "minecraft:bedrock", ++ "minecraft:bee_nest", ++ "minecraft:big_dripleaf", ++ "minecraft:black_candle_cake", ++ "minecraft:blackstone", ++ "minecraft:blue_candle_cake", ++ "minecraft:blue_ice", ++ "minecraft:brain_coral_wall_fan", ++ "minecraft:brown_candle_cake", ++ "minecraft:brown_mushroom", ++ "minecraft:bubble_coral_wall_fan", ++ "minecraft:budding_amethyst", ++ "minecraft:bush", ++ "minecraft:cactus", ++ "minecraft:cactus_flower", ++ "minecraft:calcite", ++ "minecraft:calibrated_sculk_sensor", ++ "minecraft:candle_cake", ++ "minecraft:carrots", ++ "minecraft:cave_vines", ++ "minecraft:cave_vines_body_with_berries", ++ "minecraft:cave_vines_head_with_berries", ++ "minecraft:cherry_leaves", ++ "minecraft:cherry_log", ++ "minecraft:cherry_sapling", ++ "minecraft:cherry_wood", ++ "minecraft:chiseled_sulfur", ++ "minecraft:chorus_flower", ++ "minecraft:chorus_plant", ++ "minecraft:clay", ++ "minecraft:closed_eyeblossom", ++ "minecraft:coal_ore", ++ "minecraft:cocoa", ++ "minecraft:copper_ore", ++ "minecraft:creaking_heart", ++ "minecraft:crimson_fungus", ++ "minecraft:crimson_hyphae", ++ "minecraft:crimson_nylium", ++ "minecraft:crimson_stem", ++ "minecraft:cyan_candle_cake", ++ "minecraft:dead_brain_coral_wall_fan", ++ "minecraft:dead_bubble_coral_wall_fan", ++ "minecraft:dead_fire_coral_wall_fan", ++ "minecraft:dead_horn_coral_wall_fan", ++ "minecraft:dead_tube_coral_wall_fan", ++ "minecraft:deadbush", ++ "minecraft:deepslate", ++ "minecraft:deepslate_coal_ore", ++ "minecraft:deepslate_copper_ore", ++ "minecraft:deepslate_diamond_ore", ++ "minecraft:deepslate_emerald_ore", ++ "minecraft:deepslate_gold_ore", ++ "minecraft:deepslate_iron_ore", ++ "minecraft:deepslate_lapis_ore", ++ "minecraft:deepslate_redstone_ore", ++ "minecraft:diamond_ore", ++ "minecraft:diorite", ++ "minecraft:dirt_with_roots", ++ "minecraft:dried_ghast", ++ "minecraft:dripstone_block", ++ "minecraft:emerald_ore", ++ "minecraft:end_stone", ++ "minecraft:fire", ++ "minecraft:fire_coral_wall_fan", ++ "minecraft:firefly_bush", ++ "minecraft:flowering_azalea", ++ "minecraft:flowing_lava", ++ "minecraft:flowing_water", ++ "minecraft:glow_lichen", ++ "minecraft:gold_ore", ++ "minecraft:granite", ++ "minecraft:grass_block", ++ "minecraft:grass_path", ++ "minecraft:gravel", ++ "minecraft:gray_candle_cake", ++ "minecraft:green_candle_cake", ++ "minecraft:hanging_roots", ++ "minecraft:horn_coral_wall_fan", ++ "minecraft:ice", ++ "minecraft:infested_deepslate", ++ "minecraft:iron_ore", ++ "minecraft:item.beetroot", ++ "minecraft:item.cake", ++ "minecraft:item.kelp", ++ "minecraft:item.nether_sprouts", ++ "minecraft:item.reeds", ++ "minecraft:item.wheat", ++ "minecraft:lapis_ore", ++ "minecraft:large_amethyst_bud", ++ "minecraft:lava", ++ "minecraft:leaf_litter", ++ "minecraft:light_blue_candle_cake", ++ "minecraft:light_gray_candle_cake", ++ "minecraft:lime_candle_cake", ++ "minecraft:magenta_candle_cake", ++ "minecraft:magma", ++ "minecraft:mangrove_log", ++ "minecraft:mangrove_wood", ++ "minecraft:medium_amethyst_bud", ++ "minecraft:melon_block", ++ "minecraft:melon_stem", ++ "minecraft:mob_spawner", ++ "minecraft:moss_block", ++ "minecraft:moss_carpet", ++ "minecraft:mud", ++ "minecraft:mycelium", ++ "minecraft:nether_gold_ore", ++ "minecraft:nether_wart_block", ++ "minecraft:netherrack", ++ "minecraft:obsidian", ++ "minecraft:ochre_froglight", ++ "minecraft:open_eyeblossom", ++ "minecraft:orange_candle_cake", ++ "minecraft:orange_poplar_leaves", ++ "minecraft:packed_ice", ++ "minecraft:pale_hanging_moss", ++ "minecraft:pale_moss_block", ++ "minecraft:pale_oak_leaves", ++ "minecraft:pale_oak_log", ++ "minecraft:pale_oak_sapling", ++ "minecraft:pale_oak_wood", ++ "minecraft:pearlescent_froglight", ++ "minecraft:pink_candle_cake", ++ "minecraft:pink_petals", ++ "minecraft:pitcher_crop", ++ "minecraft:pitcher_plant", ++ "minecraft:podzol", ++ "minecraft:pointed_dripstone", ++ "minecraft:polished_andesite", ++ "minecraft:polished_basalt", ++ "minecraft:polished_blackstone", ++ "minecraft:polished_deepslate", ++ "minecraft:polished_diorite", ++ "minecraft:polished_granite", ++ "minecraft:polished_sulfur", ++ "minecraft:polished_tuff", ++ "minecraft:poplar_log", ++ "minecraft:poplar_sapling", ++ "minecraft:poplar_wood", ++ "minecraft:potatoes", ++ "minecraft:potent_sulfur", ++ "minecraft:powder_snow", ++ "minecraft:pumpkin_stem", ++ "minecraft:purple_candle_cake", ++ "minecraft:quartz_ore", ++ "minecraft:red_candle_cake", ++ "minecraft:red_mushroom", ++ "minecraft:red_poplar_leaves", ++ "minecraft:redstone_ore", ++ "minecraft:reinforced_deepslate", ++ "minecraft:sculk", ++ "minecraft:sculk_catalyst", ++ "minecraft:sculk_sensor", ++ "minecraft:sculk_shrieker", ++ "minecraft:sculk_vein", ++ "minecraft:shroomlight", ++ "minecraft:small_amethyst_bud", ++ "minecraft:small_dripleaf_block", ++ "minecraft:smooth_basalt", ++ "minecraft:snow", ++ "minecraft:soul_fire", ++ "minecraft:soul_sand", ++ "minecraft:soul_soil", ++ "minecraft:spore_blossom", ++ "minecraft:stone", ++ "minecraft:stripped_acacia_log", ++ "minecraft:stripped_bamboo_block", ++ "minecraft:stripped_birch_log", ++ "minecraft:stripped_cherry_log", ++ "minecraft:stripped_cherry_wood", ++ "minecraft:stripped_crimson_hyphae", ++ "minecraft:stripped_crimson_stem", ++ "minecraft:stripped_dark_oak_log", ++ "minecraft:stripped_jungle_log", ++ "minecraft:stripped_mangrove_log", ++ "minecraft:stripped_mangrove_wood", ++ "minecraft:stripped_oak_log", ++ "minecraft:stripped_pale_oak_log", ++ "minecraft:stripped_pale_oak_wood", ++ "minecraft:stripped_poplar_log", ++ "minecraft:stripped_poplar_wood", ++ "minecraft:stripped_spruce_log", ++ "minecraft:stripped_warped_hyphae", ++ "minecraft:stripped_warped_stem", ++ "minecraft:sulfur", ++ "minecraft:sulfur_bricks", ++ "minecraft:sulfur_spike", ++ "minecraft:sweet_berry_bush", ++ "minecraft:torchflower", ++ "minecraft:torchflower_crop", ++ "minecraft:trial_spawner", ++ "minecraft:tube_coral_wall_fan", ++ "minecraft:tuff", ++ "minecraft:twisting_vines", ++ "minecraft:vault", ++ "minecraft:verdant_froglight", ++ "minecraft:vine", ++ "minecraft:warped_fungus", ++ "minecraft:warped_hyphae", ++ "minecraft:warped_nylium", ++ "minecraft:warped_stem", ++ "minecraft:warped_wart_block", ++ "minecraft:water", ++ "minecraft:web", ++ "minecraft:weeping_vines", ++ "minecraft:white_candle_cake", ++ "minecraft:wildflowers", ++ "minecraft:wither_rose", ++ "minecraft:yellow_candle_cake", ++ "minecraft:yellow_poplar_leaves" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 4, ++ "identifiers": [ ++ "minecraft:acacia_button", ++ "minecraft:acacia_door", ++ "minecraft:acacia_fence", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_log", ++ "minecraft:acacia_planks", ++ "minecraft:acacia_pressure_plate", ++ "minecraft:acacia_shelf", ++ "minecraft:acacia_standing_sign", ++ "minecraft:acacia_wall_sign", ++ "minecraft:acacia_wood", ++ "minecraft:activator_rail", ++ "minecraft:allium", ++ "minecraft:amethyst_shard", ++ "minecraft:ancient_debris", ++ "minecraft:andesite_wall", ++ "minecraft:angler_pottery_sherd", ++ "minecraft:anvil", ++ "minecraft:apple", ++ "minecraft:archer_pottery_sherd", ++ "minecraft:armadillo_scute", ++ "minecraft:armor_stand", ++ "minecraft:arms_up_pottery_sherd", ++ "minecraft:azure_bluet", ++ "minecraft:baked_potato", ++ "minecraft:bamboo", ++ "minecraft:bamboo_button", ++ "minecraft:bamboo_door", ++ "minecraft:bamboo_pressure_plate", ++ "minecraft:bamboo_shelf", ++ "minecraft:bamboo_standing_sign", ++ "minecraft:bamboo_wall_sign", ++ "minecraft:barrel", ++ "minecraft:beacon", ++ "minecraft:beef", ++ "minecraft:beehive", ++ "minecraft:beetroot", ++ "minecraft:beetroot_seeds", ++ "minecraft:beetroot_soup", ++ "minecraft:bell", ++ "minecraft:birch_button", ++ "minecraft:birch_door", ++ "minecraft:birch_fence", ++ "minecraft:birch_leaves", ++ "minecraft:birch_log", ++ "minecraft:birch_planks", ++ "minecraft:birch_pressure_plate", ++ "minecraft:birch_shelf", ++ "minecraft:birch_standing_sign", ++ "minecraft:birch_wall_sign", ++ "minecraft:birch_wood", ++ "minecraft:black_candle", ++ "minecraft:black_carpet", ++ "minecraft:black_concrete", ++ "minecraft:black_stained_glass", ++ "minecraft:black_stained_glass_pane", ++ "minecraft:black_terracotta", ++ "minecraft:black_wool", ++ "minecraft:blade_pottery_sherd", ++ "minecraft:blast_furnace", ++ "minecraft:blaze_powder", ++ "minecraft:blaze_rod", ++ "minecraft:blue_candle", ++ "minecraft:blue_carpet", ++ "minecraft:blue_concrete", ++ "minecraft:blue_orchid", ++ "minecraft:blue_stained_glass", ++ "minecraft:blue_stained_glass_pane", ++ "minecraft:blue_terracotta", ++ "minecraft:blue_wool", ++ "minecraft:bolt_armor_trim_smithing_template", ++ "minecraft:bone", ++ "minecraft:book", ++ "minecraft:bookshelf", ++ "minecraft:bowl", ++ "minecraft:brain_coral", ++ "minecraft:brain_coral_block", ++ "minecraft:bread", ++ "minecraft:breeze_rod", ++ "minecraft:brewer_pottery_sherd", ++ "minecraft:brick", ++ "minecraft:brick_wall", ++ "minecraft:brown_candle", ++ "minecraft:brown_carpet", ++ "minecraft:brown_concrete", ++ "minecraft:brown_stained_glass", ++ "minecraft:brown_stained_glass_pane", ++ "minecraft:brown_terracotta", ++ "minecraft:brown_wool", ++ "minecraft:bubble_coral", ++ "minecraft:bubble_coral_block", ++ "minecraft:burn_pottery_sherd", ++ "minecraft:candle", ++ "minecraft:carrot", ++ "minecraft:cartography_table", ++ "minecraft:carved_pumpkin", ++ "minecraft:chemical_heat", ++ "minecraft:cherry_button", ++ "minecraft:cherry_door", ++ "minecraft:cherry_pressure_plate", ++ "minecraft:cherry_shelf", ++ "minecraft:cherry_standing_sign", ++ "minecraft:cherry_wall_sign", ++ "minecraft:chest", + "minecraft:chicken", + "minecraft:chipped_anvil", + "minecraft:chiseled_bookshelf", -+ "minecraft:chiseled_cinnabar", -+ "minecraft:chiseled_copper", -+ "minecraft:chiseled_deepslate", -+ "minecraft:chiseled_nether_bricks", -+ "minecraft:chiseled_polished_blackstone", + "minecraft:chiseled_quartz_block", + "minecraft:chiseled_red_sandstone", -+ "minecraft:chiseled_resin_bricks", + "minecraft:chiseled_sandstone", + "minecraft:chiseled_stone_bricks", -+ "minecraft:chiseled_sulfur", -+ "minecraft:chiseled_tuff", -+ "minecraft:chiseled_tuff_bricks", -+ "minecraft:chorus_flower", + "minecraft:chorus_fruit", -+ "minecraft:chorus_plant", -+ "minecraft:cinnabar", -+ "minecraft:cinnabar_brick_stairs", -+ "minecraft:cinnabar_brick_wall", -+ "minecraft:cinnabar_bricks", -+ "minecraft:cinnabar_stairs", -+ "minecraft:cinnabar_wall", -+ "minecraft:clay", + "minecraft:clay_ball", -+ "minecraft:client_request_placeholder_block", + "minecraft:clock", -+ "minecraft:closed_eyeblossom", -+ "minecraft:coal_block", -+ "minecraft:coal_ore", + "minecraft:coarse_dirt", + "minecraft:coast_armor_trim_smithing_template", -+ "minecraft:cobbled_deepslate", -+ "minecraft:cobbled_deepslate_stairs", -+ "minecraft:cobbled_deepslate_wall", -+ "minecraft:cobblestone", + "minecraft:cobblestone_wall", -+ "minecraft:cocoa", + "minecraft:cod", + "minecraft:colored_torch_blue", -+ "minecraft:colored_torch_bp", + "minecraft:colored_torch_green", + "minecraft:colored_torch_purple", + "minecraft:colored_torch_red", -+ "minecraft:colored_torch_rg", -+ "minecraft:command_block", + "minecraft:compass", + "minecraft:composter", + "minecraft:compound_creator", -+ "minecraft:concrete", -+ "minecraft:concrete_powder", + "minecraft:conduit", + "minecraft:cooked_beef", + "minecraft:cooked_chicken", @@ -6701,56 +7288,28 @@ index 000000000..a5147cdb6 + "minecraft:cooked_rabbit", + "minecraft:cooked_salmon", + "minecraft:cookie", -+ "minecraft:copper_bars", -+ "minecraft:copper_bulb", -+ "minecraft:copper_chain", + "minecraft:copper_chest", + "minecraft:copper_door", -+ "minecraft:copper_golem_statue", -+ "minecraft:copper_grate", + "minecraft:copper_ingot", + "minecraft:copper_lantern", + "minecraft:copper_nugget", -+ "minecraft:copper_ore", + "minecraft:copper_torch", -+ "minecraft:copper_trapdoor", -+ "minecraft:coral", -+ "minecraft:coral_block", -+ "minecraft:coral_fan", -+ "minecraft:coral_fan_dead", + "minecraft:cornflower", -+ "minecraft:cracked_deepslate_bricks", -+ "minecraft:cracked_deepslate_tiles", -+ "minecraft:cracked_nether_bricks", -+ "minecraft:cracked_polished_blackstone_bricks", + "minecraft:cracked_stone_bricks", + "minecraft:crafter", + "minecraft:crafting_table", -+ "minecraft:creaking_heart", + "minecraft:crimson_button", + "minecraft:crimson_door", -+ "minecraft:crimson_fence", -+ "minecraft:crimson_fence_gate", -+ "minecraft:crimson_fungus", -+ "minecraft:crimson_hyphae", -+ "minecraft:crimson_nylium", -+ "minecraft:crimson_planks", + "minecraft:crimson_pressure_plate", + "minecraft:crimson_shelf", -+ "minecraft:crimson_stairs", + "minecraft:crimson_standing_sign", -+ "minecraft:crimson_stem", -+ "minecraft:crimson_trapdoor", + "minecraft:crimson_wall_sign", + "minecraft:crying_obsidian", + "minecraft:cut_red_sandstone", + "minecraft:cut_sandstone", + "minecraft:cyan_candle", -+ "minecraft:cyan_candle_cake", + "minecraft:cyan_carpet", + "minecraft:cyan_concrete", -+ "minecraft:cyan_concrete_powder", -+ "minecraft:cyan_glazed_terracotta", + "minecraft:cyan_stained_glass", + "minecraft:cyan_stained_glass_pane", + "minecraft:cyan_terracotta", @@ -6760,180 +7319,95 @@ index 000000000..a5147cdb6 + "minecraft:dark_oak_button", + "minecraft:dark_oak_door", + "minecraft:dark_oak_fence", -+ "minecraft:dark_oak_fence_gate", + "minecraft:dark_oak_leaves", + "minecraft:dark_oak_log", + "minecraft:dark_oak_planks", + "minecraft:dark_oak_pressure_plate", + "minecraft:dark_oak_shelf", -+ "minecraft:dark_oak_stairs", -+ "minecraft:dark_oak_trapdoor", + "minecraft:dark_oak_wood", + "minecraft:dark_prismarine", -+ "minecraft:dark_prismarine_stairs", + "minecraft:darkoak_standing_sign", + "minecraft:darkoak_wall_sign", + "minecraft:daylight_detector", + "minecraft:daylight_detector_inverted", + "minecraft:dead_brain_coral", + "minecraft:dead_brain_coral_block", -+ "minecraft:dead_brain_coral_wall_fan", + "minecraft:dead_bubble_coral", + "minecraft:dead_bubble_coral_block", -+ "minecraft:dead_bubble_coral_wall_fan", + "minecraft:dead_fire_coral", + "minecraft:dead_fire_coral_block", -+ "minecraft:dead_fire_coral_wall_fan", + "minecraft:dead_horn_coral", + "minecraft:dead_horn_coral_block", -+ "minecraft:dead_horn_coral_wall_fan", + "minecraft:dead_tube_coral", + "minecraft:dead_tube_coral_block", -+ "minecraft:dead_tube_coral_wall_fan", -+ "minecraft:deadbush", + "minecraft:decorated_pot", -+ "minecraft:deepslate", -+ "minecraft:deepslate_brick_stairs", -+ "minecraft:deepslate_brick_wall", -+ "minecraft:deepslate_bricks", -+ "minecraft:deepslate_coal_ore", -+ "minecraft:deepslate_copper_ore", -+ "minecraft:deepslate_diamond_ore", -+ "minecraft:deepslate_emerald_ore", -+ "minecraft:deepslate_gold_ore", -+ "minecraft:deepslate_iron_ore", -+ "minecraft:deepslate_lapis_ore", -+ "minecraft:deepslate_redstone_ore", -+ "minecraft:deepslate_tile_stairs", -+ "minecraft:deepslate_tile_wall", -+ "minecraft:deepslate_tiles", -+ "minecraft:deny", + "minecraft:deprecated_anvil", + "minecraft:deprecated_purpur_block_1", + "minecraft:deprecated_purpur_block_2", + "minecraft:detector_rail", + "minecraft:diamond", -+ "minecraft:diamond_block", -+ "minecraft:diamond_ore", -+ "minecraft:diorite", -+ "minecraft:diorite_stairs", + "minecraft:diorite_wall", + "minecraft:dirt", -+ "minecraft:dirt_with_roots", + "minecraft:disc_fragment_5", + "minecraft:dispenser", -+ "minecraft:double_plant", -+ "minecraft:double_stone_block_slab", -+ "minecraft:double_stone_block_slab2", -+ "minecraft:double_stone_block_slab3", -+ "minecraft:double_stone_block_slab4", + "minecraft:dragon_breath", + "minecraft:dragon_egg", -+ "minecraft:dried_ghast", + "minecraft:dried_kelp", -+ "minecraft:dried_kelp_block", -+ "minecraft:dripstone_block", + "minecraft:dropper", + "minecraft:dune_armor_trim_smithing_template", -+ "minecraft:dye", + "minecraft:echo_shard", + "minecraft:element_constructor", + "minecraft:emerald", -+ "minecraft:emerald_block", -+ "minecraft:emerald_ore", + "minecraft:enchanted_golden_apple", + "minecraft:enchanting_table", -+ "minecraft:end_brick_stairs", -+ "minecraft:end_bricks", -+ "minecraft:end_gateway", -+ "minecraft:end_portal", + "minecraft:end_rod", -+ "minecraft:end_stone", + "minecraft:end_stone_brick_wall", + "minecraft:ender_chest", + "minecraft:ender_eye", + "minecraft:experience_bottle", + "minecraft:explorer_pottery_sherd", -+ "minecraft:exposed_chiseled_copper", -+ "minecraft:exposed_copper", -+ "minecraft:exposed_copper_bars", -+ "minecraft:exposed_copper_bulb", -+ "minecraft:exposed_copper_chain", + "minecraft:exposed_copper_chest", + "minecraft:exposed_copper_door", -+ "minecraft:exposed_copper_golem_statue", -+ "minecraft:exposed_copper_grate", + "minecraft:exposed_copper_lantern", -+ "minecraft:exposed_copper_trapdoor", -+ "minecraft:exposed_cut_copper", -+ "minecraft:exposed_cut_copper_stairs", + "minecraft:exposed_lightning_rod", + "minecraft:eye_armor_trim_smithing_template", + "minecraft:feather", -+ "minecraft:fence", -+ "minecraft:fence_gate", + "minecraft:fermented_spider_eye", + "minecraft:fern", -+ "minecraft:fire", + "minecraft:fire_charge", + "minecraft:fire_coral", + "minecraft:fire_coral_block", -+ "minecraft:fire_coral_wall_fan", -+ "minecraft:firefly_bush", + "minecraft:firework_rocket", + "minecraft:firework_star", + "minecraft:fletching_table", + "minecraft:flint", + "minecraft:flow_armor_trim_smithing_template", + "minecraft:flow_pottery_sherd", -+ "minecraft:flowering_azalea", -+ "minecraft:flowing_lava", -+ "minecraft:flowing_water", + "minecraft:friend_pottery_sherd", + "minecraft:frog_spawn", -+ "minecraft:frosted_ice", + "minecraft:furnace", + "minecraft:ghast_tear", -+ "minecraft:gilded_blackstone", -+ "minecraft:glass", + "minecraft:glass_bottle", -+ "minecraft:glass_pane", + "minecraft:glistering_melon_slice", + "minecraft:glow_ink_sac", -+ "minecraft:glow_lichen", -+ "minecraft:glowingobsidian", + "minecraft:glowstone", + "minecraft:glowstone_dust", -+ "minecraft:gold_block", + "minecraft:gold_ingot", + "minecraft:gold_nugget", -+ "minecraft:gold_ore", + "minecraft:golden_apple", + "minecraft:golden_carrot", -+ "minecraft:golden_dandelion", + "minecraft:golden_rail", -+ "minecraft:granite", -+ "minecraft:granite_stairs", + "minecraft:granite_wall", -+ "minecraft:grass_block", -+ "minecraft:grass_path", -+ "minecraft:gravel", + "minecraft:gray_candle", -+ "minecraft:gray_candle_cake", + "minecraft:gray_carpet", + "minecraft:gray_concrete", -+ "minecraft:gray_concrete_powder", -+ "minecraft:gray_glazed_terracotta", + "minecraft:gray_stained_glass", + "minecraft:gray_stained_glass_pane", + "minecraft:gray_terracotta", + "minecraft:gray_wool", + "minecraft:green_candle", -+ "minecraft:green_candle_cake", + "minecraft:green_carpet", + "minecraft:green_concrete", -+ "minecraft:green_concrete_powder", -+ "minecraft:green_glazed_terracotta", + "minecraft:green_stained_glass", + "minecraft:green_stained_glass_pane", + "minecraft:green_terracotta", @@ -6941,7 +7415,6 @@ index 000000000..a5147cdb6 + "minecraft:grindstone", + "minecraft:gunpowder", + "minecraft:guster_pottery_sherd", -+ "minecraft:hanging_roots", + "minecraft:hard_black_stained_glass", + "minecraft:hard_black_stained_glass_pane", + "minecraft:hard_blue_stained_glass", @@ -6972,51 +7445,31 @@ index 000000000..a5147cdb6 + "minecraft:hard_purple_stained_glass_pane", + "minecraft:hard_red_stained_glass", + "minecraft:hard_red_stained_glass_pane", -+ "minecraft:hard_stained_glass", -+ "minecraft:hard_stained_glass_pane", + "minecraft:hard_white_stained_glass", + "minecraft:hard_white_stained_glass_pane", + "minecraft:hard_yellow_stained_glass", + "minecraft:hard_yellow_stained_glass_pane", -+ "minecraft:hardened_clay", -+ "minecraft:hay_block", + "minecraft:heart_of_the_sea", + "minecraft:heart_pottery_sherd", + "minecraft:heartbreak_pottery_sherd", + "minecraft:heavy_core", + "minecraft:heavy_weighted_pressure_plate", -+ "minecraft:honey_block", + "minecraft:honey_bottle", + "minecraft:honeycomb", -+ "minecraft:honeycomb_block", + "minecraft:horn_coral", + "minecraft:horn_coral_block", -+ "minecraft:horn_coral_wall_fan", + "minecraft:host_armor_trim_smithing_template", + "minecraft:howl_pottery_sherd", -+ "minecraft:ice", + "minecraft:infested_chiseled_stone_bricks", + "minecraft:infested_cobblestone", + "minecraft:infested_cracked_stone_bricks", -+ "minecraft:infested_deepslate", + "minecraft:infested_mossy_stone_bricks", + "minecraft:infested_stone", + "minecraft:infested_stone_bricks", -+ "minecraft:info_update", -+ "minecraft:info_update2", -+ "minecraft:invisible_bedrock", -+ "minecraft:iron_bars", -+ "minecraft:iron_block", -+ "minecraft:iron_chain", + "minecraft:iron_door", + "minecraft:iron_ingot", + "minecraft:iron_nugget", -+ "minecraft:iron_ore", -+ "minecraft:iron_trapdoor", + "minecraft:item.bed", -+ "minecraft:item.beetroot", -+ "minecraft:item.brewing_stand", -+ "minecraft:item.cake", + "minecraft:item.camera", + "minecraft:item.campfire", + "minecraft:item.cauldron", @@ -7024,76 +7477,37 @@ index 000000000..a5147cdb6 + "minecraft:item.frame", + "minecraft:item.glow_frame", + "minecraft:item.hopper", -+ "minecraft:item.kelp", -+ "minecraft:item.nether_sprouts", -+ "minecraft:item.nether_wart", -+ "minecraft:item.reeds", + "minecraft:item.soul_campfire", + "minecraft:item.straw_bed", -+ "minecraft:item.wheat", -+ "minecraft:jigsaw", + "minecraft:jukebox", + "minecraft:jungle_button", + "minecraft:jungle_door", + "minecraft:jungle_fence", -+ "minecraft:jungle_fence_gate", + "minecraft:jungle_leaves", + "minecraft:jungle_log", + "minecraft:jungle_planks", + "minecraft:jungle_pressure_plate", + "minecraft:jungle_shelf", -+ "minecraft:jungle_stairs", + "minecraft:jungle_standing_sign", -+ "minecraft:jungle_trapdoor", + "minecraft:jungle_wall_sign", + "minecraft:jungle_wood", -+ "minecraft:lab_table", -+ "minecraft:ladder", -+ "minecraft:lantern", -+ "minecraft:lapis_block", -+ "minecraft:lapis_ore", -+ "minecraft:large_amethyst_bud", ++ "minecraft:lab_table", ++ "minecraft:lantern", + "minecraft:large_fern", -+ "minecraft:lava", + "minecraft:lead", -+ "minecraft:leaf_litter", + "minecraft:leather", -+ "minecraft:leaves", -+ "minecraft:leaves2", + "minecraft:lectern", + "minecraft:lever", -+ "minecraft:light_block", -+ "minecraft:light_block_0", -+ "minecraft:light_block_1", -+ "minecraft:light_block_10", -+ "minecraft:light_block_11", -+ "minecraft:light_block_12", -+ "minecraft:light_block_13", -+ "minecraft:light_block_14", -+ "minecraft:light_block_15", -+ "minecraft:light_block_2", -+ "minecraft:light_block_3", -+ "minecraft:light_block_4", -+ "minecraft:light_block_5", -+ "minecraft:light_block_6", -+ "minecraft:light_block_7", -+ "minecraft:light_block_8", -+ "minecraft:light_block_9", + "minecraft:light_blue_candle", -+ "minecraft:light_blue_candle_cake", + "minecraft:light_blue_carpet", + "minecraft:light_blue_concrete", -+ "minecraft:light_blue_concrete_powder", -+ "minecraft:light_blue_glazed_terracotta", + "minecraft:light_blue_stained_glass", + "minecraft:light_blue_stained_glass_pane", + "minecraft:light_blue_terracotta", + "minecraft:light_blue_wool", + "minecraft:light_gray_candle", -+ "minecraft:light_gray_candle_cake", + "minecraft:light_gray_carpet", + "minecraft:light_gray_concrete", -+ "minecraft:light_gray_concrete_powder", + "minecraft:light_gray_stained_glass", + "minecraft:light_gray_stained_glass_pane", + "minecraft:light_gray_terracotta", @@ -7103,298 +7517,151 @@ index 000000000..a5147cdb6 + "minecraft:lilac", + "minecraft:lily_of_the_valley", + "minecraft:lime_candle", -+ "minecraft:lime_candle_cake", + "minecraft:lime_carpet", + "minecraft:lime_concrete", -+ "minecraft:lime_concrete_powder", -+ "minecraft:lime_glazed_terracotta", + "minecraft:lime_stained_glass", + "minecraft:lime_stained_glass_pane", + "minecraft:lime_terracotta", + "minecraft:lime_wool", + "minecraft:lit_blast_furnace", -+ "minecraft:lit_deepslate_redstone_ore", + "minecraft:lit_furnace", + "minecraft:lit_pumpkin", -+ "minecraft:lit_redstone_lamp", -+ "minecraft:lit_redstone_ore", + "minecraft:lit_smoker", -+ "minecraft:lodestone", -+ "minecraft:lodestone_compass", -+ "minecraft:log", -+ "minecraft:log2", + "minecraft:loom", + "minecraft:magenta_candle", -+ "minecraft:magenta_candle_cake", + "minecraft:magenta_carpet", + "minecraft:magenta_concrete", -+ "minecraft:magenta_concrete_powder", -+ "minecraft:magenta_glazed_terracotta", + "minecraft:magenta_stained_glass", + "minecraft:magenta_stained_glass_pane", + "minecraft:magenta_terracotta", + "minecraft:magenta_wool", -+ "minecraft:magma", + "minecraft:magma_cream", + "minecraft:mangrove_button", + "minecraft:mangrove_door", -+ "minecraft:mangrove_fence", -+ "minecraft:mangrove_fence_gate", -+ "minecraft:mangrove_log", -+ "minecraft:mangrove_planks", + "minecraft:mangrove_pressure_plate", + "minecraft:mangrove_propagule", + "minecraft:mangrove_roots", + "minecraft:mangrove_shelf", -+ "minecraft:mangrove_stairs", + "minecraft:mangrove_standing_sign", -+ "minecraft:mangrove_trapdoor", + "minecraft:mangrove_wall_sign", -+ "minecraft:mangrove_wood", + "minecraft:material_reducer", -+ "minecraft:medium_amethyst_bud", -+ "minecraft:melon_block", + "minecraft:melon_seeds", + "minecraft:melon_slice", -+ "minecraft:melon_stem", + "minecraft:miner_pottery_sherd", -+ "minecraft:mob_spawner", -+ "minecraft:monster_egg", -+ "minecraft:moss_block", -+ "minecraft:moss_carpet", -+ "minecraft:mossy_cobblestone", -+ "minecraft:mossy_cobblestone_stairs", + "minecraft:mossy_cobblestone_wall", -+ "minecraft:mossy_stone_brick_stairs", + "minecraft:mossy_stone_brick_wall", + "minecraft:mossy_stone_bricks", + "minecraft:mourner_pottery_sherd", -+ "minecraft:moving_block", -+ "minecraft:mud", -+ "minecraft:mud_brick_stairs", -+ "minecraft:mud_brick_wall", -+ "minecraft:mud_bricks", + "minecraft:muddy_mangrove_roots", + "minecraft:mushroom_stem", + "minecraft:mushroom_stew", + "minecraft:mutton", -+ "minecraft:mycelium", + "minecraft:name_tag", + "minecraft:nautilus_shell", -+ "minecraft:nether_brick", -+ "minecraft:nether_brick_fence", -+ "minecraft:nether_brick_stairs", + "minecraft:nether_brick_wall", -+ "minecraft:nether_gold_ore", + "minecraft:nether_star", + "minecraft:nether_wart", -+ "minecraft:nether_wart_block", + "minecraft:netherbrick", + "minecraft:netherite_block", + "minecraft:netherite_ingot", + "minecraft:netherite_scrap", + "minecraft:netherite_upgrade_smithing_template", -+ "minecraft:netherrack", -+ "minecraft:netherreactor", -+ "minecraft:normal_stone_stairs", + "minecraft:noteblock", + "minecraft:oak_fence", + "minecraft:oak_leaves", + "minecraft:oak_log", + "minecraft:oak_planks", + "minecraft:oak_shelf", -+ "minecraft:oak_stairs", + "minecraft:oak_wood", + "minecraft:observer", -+ "minecraft:obsidian", -+ "minecraft:ochre_froglight", -+ "minecraft:open_eyeblossom", + "minecraft:orange_candle", -+ "minecraft:orange_candle_cake", + "minecraft:orange_carpet", + "minecraft:orange_concrete", -+ "minecraft:orange_concrete_powder", -+ "minecraft:orange_glazed_terracotta", -+ "minecraft:orange_poplar_leaves", + "minecraft:orange_stained_glass", + "minecraft:orange_stained_glass_pane", + "minecraft:orange_terracotta", + "minecraft:orange_tulip", + "minecraft:orange_wool", + "minecraft:oxeye_daisy", -+ "minecraft:oxidized_chiseled_copper", -+ "minecraft:oxidized_copper", -+ "minecraft:oxidized_copper_bars", -+ "minecraft:oxidized_copper_bulb", -+ "minecraft:oxidized_copper_chain", + "minecraft:oxidized_copper_chest", + "minecraft:oxidized_copper_door", -+ "minecraft:oxidized_copper_golem_statue", -+ "minecraft:oxidized_copper_grate", + "minecraft:oxidized_copper_lantern", -+ "minecraft:oxidized_copper_trapdoor", -+ "minecraft:oxidized_cut_copper", -+ "minecraft:oxidized_cut_copper_stairs", + "minecraft:oxidized_lightning_rod", -+ "minecraft:packed_ice", -+ "minecraft:packed_mud", + "minecraft:painting", -+ "minecraft:pale_hanging_moss", -+ "minecraft:pale_moss_block", + "minecraft:pale_moss_carpet", + "minecraft:pale_oak_button", + "minecraft:pale_oak_door", -+ "minecraft:pale_oak_fence", -+ "minecraft:pale_oak_fence_gate", -+ "minecraft:pale_oak_leaves", -+ "minecraft:pale_oak_log", -+ "minecraft:pale_oak_planks", + "minecraft:pale_oak_pressure_plate", -+ "minecraft:pale_oak_sapling", + "minecraft:pale_oak_shelf", -+ "minecraft:pale_oak_stairs", + "minecraft:pale_oak_standing_sign", -+ "minecraft:pale_oak_trapdoor", + "minecraft:pale_oak_wall_sign", -+ "minecraft:pale_oak_wood", + "minecraft:paper", -+ "minecraft:pearlescent_froglight", + "minecraft:peony", + "minecraft:phantom_membrane", + "minecraft:pink_candle", -+ "minecraft:pink_candle_cake", + "minecraft:pink_carpet", + "minecraft:pink_concrete", -+ "minecraft:pink_concrete_powder", -+ "minecraft:pink_glazed_terracotta", -+ "minecraft:pink_petals", + "minecraft:pink_stained_glass", + "minecraft:pink_stained_glass_pane", + "minecraft:pink_terracotta", + "minecraft:pink_tulip", + "minecraft:pink_wool", + "minecraft:piston", -+ "minecraft:piston_arm_collision", -+ "minecraft:pitcher_crop", -+ "minecraft:pitcher_plant", + "minecraft:pitcher_pod", -+ "minecraft:planks", + "minecraft:plenty_pottery_sherd", -+ "minecraft:podzol", -+ "minecraft:pointed_dripstone", + "minecraft:poisonous_potato", -+ "minecraft:polished_andesite", -+ "minecraft:polished_andesite_stairs", -+ "minecraft:polished_basalt", -+ "minecraft:polished_blackstone", -+ "minecraft:polished_blackstone_brick_stairs", -+ "minecraft:polished_blackstone_brick_wall", -+ "minecraft:polished_blackstone_bricks", + "minecraft:polished_blackstone_button", + "minecraft:polished_blackstone_pressure_plate", -+ "minecraft:polished_blackstone_stairs", -+ "minecraft:polished_blackstone_wall", -+ "minecraft:polished_cinnabar", -+ "minecraft:polished_cinnabar_stairs", -+ "minecraft:polished_cinnabar_wall", -+ "minecraft:polished_deepslate", -+ "minecraft:polished_deepslate_stairs", -+ "minecraft:polished_deepslate_wall", -+ "minecraft:polished_diorite", -+ "minecraft:polished_diorite_stairs", -+ "minecraft:polished_granite", -+ "minecraft:polished_granite_stairs", -+ "minecraft:polished_sulfur", -+ "minecraft:polished_sulfur_stairs", -+ "minecraft:polished_sulfur_wall", -+ "minecraft:polished_tuff", -+ "minecraft:polished_tuff_stairs", -+ "minecraft:polished_tuff_wall", + "minecraft:poplar_button", + "minecraft:poplar_door", -+ "minecraft:poplar_fence", -+ "minecraft:poplar_fence_gate", -+ "minecraft:poplar_log", -+ "minecraft:poplar_planks", + "minecraft:poplar_pressure_plate", -+ "minecraft:poplar_sapling", + "minecraft:poplar_shelf", -+ "minecraft:poplar_stairs", + "minecraft:poplar_standing_sign", -+ "minecraft:poplar_trapdoor", + "minecraft:poplar_wall_sign", -+ "minecraft:poplar_wood", + "minecraft:popped_chorus_fruit", + "minecraft:poppy", + "minecraft:porkchop", + "minecraft:portal", + "minecraft:potato", -+ "minecraft:potatoes", -+ "minecraft:potent_sulfur", -+ "minecraft:powder_snow", -+ "minecraft:powered_comparator", + "minecraft:powered_repeater", + "minecraft:prismarine", + "minecraft:prismarine_bricks", -+ "minecraft:prismarine_bricks_stairs", + "minecraft:prismarine_crystals", + "minecraft:prismarine_shard", -+ "minecraft:prismarine_stairs", + "minecraft:prismarine_wall", + "minecraft:prize_pottery_sherd", + "minecraft:pufferfish", + "minecraft:pumpkin", + "minecraft:pumpkin_pie", + "minecraft:pumpkin_seeds", -+ "minecraft:pumpkin_stem", + "minecraft:purple_candle", -+ "minecraft:purple_candle_cake", + "minecraft:purple_carpet", + "minecraft:purple_concrete", -+ "minecraft:purple_concrete_powder", -+ "minecraft:purple_glazed_terracotta", + "minecraft:purple_stained_glass", + "minecraft:purple_stained_glass_pane", + "minecraft:purple_terracotta", + "minecraft:purple_wool", + "minecraft:purpur_block", + "minecraft:purpur_pillar", -+ "minecraft:purpur_stairs", + "minecraft:quartz", + "minecraft:quartz_block", -+ "minecraft:quartz_bricks", -+ "minecraft:quartz_ore", + "minecraft:quartz_pillar", -+ "minecraft:quartz_stairs", + "minecraft:rabbit", + "minecraft:rabbit_foot", + "minecraft:rabbit_hide", + "minecraft:rabbit_stew", + "minecraft:rail", + "minecraft:raiser_armor_trim_smithing_template", -+ "minecraft:rapid_fertilizer", + "minecraft:raw_copper", -+ "minecraft:raw_copper_block", + "minecraft:raw_gold", -+ "minecraft:raw_gold_block", + "minecraft:raw_iron", -+ "minecraft:raw_iron_block", + "minecraft:recovery_compass", + "minecraft:red_candle", -+ "minecraft:red_candle_cake", + "minecraft:red_carpet", + "minecraft:red_concrete", -+ "minecraft:red_concrete_powder", -+ "minecraft:red_flower", -+ "minecraft:red_glazed_terracotta", -+ "minecraft:red_mushroom", -+ "minecraft:red_nether_brick", -+ "minecraft:red_nether_brick_stairs", + "minecraft:red_nether_brick_wall", -+ "minecraft:red_poplar_leaves", + "minecraft:red_sand", + "minecraft:red_sandstone", -+ "minecraft:red_sandstone_stairs", + "minecraft:red_sandstone_wall", + "minecraft:red_stained_glass", + "minecraft:red_stained_glass_pane", @@ -7404,17 +7671,9 @@ index 000000000..a5147cdb6 + "minecraft:redstone", + "minecraft:redstone_block", + "minecraft:redstone_lamp", -+ "minecraft:redstone_ore", + "minecraft:redstone_torch", + "minecraft:redstone_wire", -+ "minecraft:reinforced_deepslate", -+ "minecraft:repeating_command_block", -+ "minecraft:reserved6", -+ "minecraft:resin_block", + "minecraft:resin_brick", -+ "minecraft:resin_brick_stairs", -+ "minecraft:resin_brick_wall", -+ "minecraft:resin_bricks", + "minecraft:resin_clump", + "minecraft:rib_armor_trim_smithing_template", + "minecraft:rose_bush", @@ -7422,16 +7681,9 @@ index 000000000..a5147cdb6 + "minecraft:salmon", + "minecraft:sand", + "minecraft:sandstone", -+ "minecraft:sandstone_stairs", + "minecraft:sandstone_wall", -+ "minecraft:sapling", + "minecraft:scaffolding", + "minecraft:scrape_pottery_sherd", -+ "minecraft:sculk", -+ "minecraft:sculk_catalyst", -+ "minecraft:sculk_sensor", -+ "minecraft:sculk_shrieker", -+ "minecraft:sculk_vein", + "minecraft:sea_lantern", + "minecraft:sea_pickle", + "minecraft:sentry_armor_trim_smithing_template", @@ -7440,280 +7692,123 @@ index 000000000..a5147cdb6 + "minecraft:shelter_pottery_sherd", + "minecraft:short_dry_grass", + "minecraft:short_grass", -+ "minecraft:shroomlight", -+ "minecraft:shulker_box", + "minecraft:shulker_shell", + "minecraft:silence_armor_trim_smithing_template", -+ "minecraft:silver_glazed_terracotta", -+ "minecraft:skull", + "minecraft:skull_pottery_sherd", -+ "minecraft:slime", + "minecraft:slime_ball", -+ "minecraft:small_amethyst_bud", -+ "minecraft:small_dripleaf_block", + "minecraft:smithing_table", + "minecraft:smoker", -+ "minecraft:smooth_basalt", + "minecraft:smooth_quartz", -+ "minecraft:smooth_quartz_stairs", + "minecraft:smooth_red_sandstone", -+ "minecraft:smooth_red_sandstone_stairs", + "minecraft:smooth_sandstone", -+ "minecraft:smooth_sandstone_stairs", -+ "minecraft:smooth_stone", + "minecraft:sniffer_egg", + "minecraft:snort_pottery_sherd", + "minecraft:snout_armor_trim_smithing_template", -+ "minecraft:snow", -+ "minecraft:soul_fire", + "minecraft:soul_lantern", -+ "minecraft:soul_sand", -+ "minecraft:soul_soil", + "minecraft:soul_torch", -+ "minecraft:spawn_egg", + "minecraft:spider_eye", + "minecraft:spire_armor_trim_smithing_template", + "minecraft:sponge", -+ "minecraft:spore_blossom", + "minecraft:spruce_button", + "minecraft:spruce_door", + "minecraft:spruce_fence", -+ "minecraft:spruce_fence_gate", + "minecraft:spruce_leaves", + "minecraft:spruce_log", + "minecraft:spruce_planks", + "minecraft:spruce_pressure_plate", + "minecraft:spruce_shelf", -+ "minecraft:spruce_stairs", + "minecraft:spruce_standing_sign", -+ "minecraft:spruce_trapdoor", + "minecraft:spruce_wall_sign", + "minecraft:spruce_wood", -+ "minecraft:stained_glass", -+ "minecraft:stained_glass_pane", -+ "minecraft:stained_hardened_clay", + "minecraft:standing_banner", + "minecraft:standing_sign", + "minecraft:stick", + "minecraft:sticky_piston", -+ "minecraft:sticky_piston_arm_collision", -+ "minecraft:stone", -+ "minecraft:stone_block_slab", -+ "minecraft:stone_block_slab2", -+ "minecraft:stone_block_slab3", -+ "minecraft:stone_block_slab4", -+ "minecraft:stone_brick_stairs", + "minecraft:stone_brick_wall", + "minecraft:stone_bricks", + "minecraft:stone_button", + "minecraft:stone_pressure_plate", -+ "minecraft:stone_stairs", -+ "minecraft:stonebrick", -+ "minecraft:stonecutter", + "minecraft:stonecutter_block", -+ "minecraft:stripped_acacia_log", + "minecraft:stripped_acacia_wood", -+ "minecraft:stripped_bamboo_block", -+ "minecraft:stripped_birch_log", + "minecraft:stripped_birch_wood", -+ "minecraft:stripped_cherry_log", -+ "minecraft:stripped_cherry_wood", -+ "minecraft:stripped_crimson_hyphae", -+ "minecraft:stripped_crimson_stem", -+ "minecraft:stripped_dark_oak_log", + "minecraft:stripped_dark_oak_wood", -+ "minecraft:stripped_jungle_log", + "minecraft:stripped_jungle_wood", -+ "minecraft:stripped_mangrove_log", -+ "minecraft:stripped_mangrove_wood", -+ "minecraft:stripped_oak_log", + "minecraft:stripped_oak_wood", -+ "minecraft:stripped_pale_oak_log", -+ "minecraft:stripped_pale_oak_wood", -+ "minecraft:stripped_poplar_log", -+ "minecraft:stripped_poplar_wood", -+ "minecraft:stripped_spruce_log", + "minecraft:stripped_spruce_wood", -+ "minecraft:stripped_warped_hyphae", -+ "minecraft:stripped_warped_stem", -+ "minecraft:structure_block", -+ "minecraft:structure_void", + "minecraft:sugar", -+ "minecraft:sulfur", -+ "minecraft:sulfur_brick_stairs", -+ "minecraft:sulfur_brick_wall", -+ "minecraft:sulfur_bricks", -+ "minecraft:sulfur_spike", -+ "minecraft:sulfur_stairs", -+ "minecraft:sulfur_wall", + "minecraft:sunflower", + "minecraft:suspicious_gravel", + "minecraft:suspicious_sand", + "minecraft:sweet_berries", -+ "minecraft:sweet_berry_bush", + "minecraft:tall_dry_grass", + "minecraft:tall_grass", -+ "minecraft:tallgrass", + "minecraft:target", + "minecraft:tide_armor_trim_smithing_template", -+ "minecraft:tinted_glass", + "minecraft:tnt", + "minecraft:torch", -+ "minecraft:torchflower", -+ "minecraft:torchflower_crop", + "minecraft:torchflower_seeds", -+ "minecraft:trapdoor", + "minecraft:trapped_chest", -+ "minecraft:trial_spawner", + "minecraft:trip_wire", + "minecraft:tripwire_hook", + "minecraft:tropical_fish", + "minecraft:tube_coral", + "minecraft:tube_coral_block", -+ "minecraft:tube_coral_wall_fan", -+ "minecraft:tuff", -+ "minecraft:tuff_brick_stairs", -+ "minecraft:tuff_brick_wall", -+ "minecraft:tuff_bricks", -+ "minecraft:tuff_stairs", -+ "minecraft:tuff_wall", + "minecraft:turtle_egg", + "minecraft:turtle_scute", -+ "minecraft:twisting_vines", + "minecraft:underwater_tnt", + "minecraft:underwater_torch", -+ "minecraft:unknown", + "minecraft:unlit_redstone_torch", -+ "minecraft:unpowered_comparator", + "minecraft:unpowered_repeater", -+ "minecraft:vault", -+ "minecraft:verdant_froglight", + "minecraft:vex_armor_trim_smithing_template", -+ "minecraft:vine", + "minecraft:wall_banner", + "minecraft:wall_sign", + "minecraft:ward_armor_trim_smithing_template", + "minecraft:warped_button", + "minecraft:warped_door", -+ "minecraft:warped_fence", -+ "minecraft:warped_fence_gate", -+ "minecraft:warped_fungus", -+ "minecraft:warped_hyphae", -+ "minecraft:warped_nylium", -+ "minecraft:warped_planks", + "minecraft:warped_pressure_plate", + "minecraft:warped_shelf", -+ "minecraft:warped_stairs", + "minecraft:warped_standing_sign", -+ "minecraft:warped_stem", -+ "minecraft:warped_trapdoor", + "minecraft:warped_wall_sign", -+ "minecraft:warped_wart_block", -+ "minecraft:water", + "minecraft:waterlily", -+ "minecraft:waxed_chiseled_copper", -+ "minecraft:waxed_copper", -+ "minecraft:waxed_copper_bars", -+ "minecraft:waxed_copper_bulb", -+ "minecraft:waxed_copper_chain", + "minecraft:waxed_copper_chest", + "minecraft:waxed_copper_door", -+ "minecraft:waxed_copper_golem_statue", -+ "minecraft:waxed_copper_grate", + "minecraft:waxed_copper_lantern", -+ "minecraft:waxed_copper_trapdoor", -+ "minecraft:waxed_cut_copper", -+ "minecraft:waxed_cut_copper_stairs", -+ "minecraft:waxed_exposed_chiseled_copper", -+ "minecraft:waxed_exposed_copper", -+ "minecraft:waxed_exposed_copper_bars", -+ "minecraft:waxed_exposed_copper_bulb", -+ "minecraft:waxed_exposed_copper_chain", + "minecraft:waxed_exposed_copper_chest", + "minecraft:waxed_exposed_copper_door", -+ "minecraft:waxed_exposed_copper_golem_statue", -+ "minecraft:waxed_exposed_copper_grate", + "minecraft:waxed_exposed_copper_lantern", -+ "minecraft:waxed_exposed_copper_trapdoor", -+ "minecraft:waxed_exposed_cut_copper", -+ "minecraft:waxed_exposed_cut_copper_stairs", + "minecraft:waxed_exposed_lightning_rod", + "minecraft:waxed_lightning_rod", -+ "minecraft:waxed_oxidized_chiseled_copper", -+ "minecraft:waxed_oxidized_copper", -+ "minecraft:waxed_oxidized_copper_bars", -+ "minecraft:waxed_oxidized_copper_bulb", -+ "minecraft:waxed_oxidized_copper_chain", + "minecraft:waxed_oxidized_copper_chest", + "minecraft:waxed_oxidized_copper_door", -+ "minecraft:waxed_oxidized_copper_golem_statue", -+ "minecraft:waxed_oxidized_copper_grate", + "minecraft:waxed_oxidized_copper_lantern", -+ "minecraft:waxed_oxidized_copper_trapdoor", -+ "minecraft:waxed_oxidized_cut_copper", -+ "minecraft:waxed_oxidized_cut_copper_stairs", + "minecraft:waxed_oxidized_lightning_rod", -+ "minecraft:waxed_weathered_chiseled_copper", -+ "minecraft:waxed_weathered_copper", -+ "minecraft:waxed_weathered_copper_bars", -+ "minecraft:waxed_weathered_copper_bulb", -+ "minecraft:waxed_weathered_copper_chain", + "minecraft:waxed_weathered_copper_chest", + "minecraft:waxed_weathered_copper_door", -+ "minecraft:waxed_weathered_copper_golem_statue", -+ "minecraft:waxed_weathered_copper_grate", + "minecraft:waxed_weathered_copper_lantern", -+ "minecraft:waxed_weathered_copper_trapdoor", -+ "minecraft:waxed_weathered_cut_copper", -+ "minecraft:waxed_weathered_cut_copper_stairs", + "minecraft:waxed_weathered_lightning_rod", + "minecraft:wayfinder_armor_trim_smithing_template", -+ "minecraft:weathered_chiseled_copper", -+ "minecraft:weathered_copper", -+ "minecraft:weathered_copper_bars", -+ "minecraft:weathered_copper_bulb", -+ "minecraft:weathered_copper_chain", + "minecraft:weathered_copper_chest", + "minecraft:weathered_copper_door", -+ "minecraft:weathered_copper_golem_statue", -+ "minecraft:weathered_copper_grate", + "minecraft:weathered_copper_lantern", -+ "minecraft:weathered_copper_trapdoor", -+ "minecraft:weathered_cut_copper", -+ "minecraft:weathered_cut_copper_stairs", + "minecraft:weathered_lightning_rod", -+ "minecraft:web", -+ "minecraft:weeping_vines", + "minecraft:wet_sponge", + "minecraft:wheat", + "minecraft:wheat_seeds", + "minecraft:white_candle", -+ "minecraft:white_candle_cake", + "minecraft:white_carpet", + "minecraft:white_concrete", -+ "minecraft:white_concrete_powder", -+ "minecraft:white_glazed_terracotta", + "minecraft:white_stained_glass", + "minecraft:white_stained_glass_pane", + "minecraft:white_terracotta", + "minecraft:white_tulip", + "minecraft:white_wool", + "minecraft:wild_armor_trim_smithing_template", -+ "minecraft:wildflowers", -+ "minecraft:wither_rose", -+ "minecraft:wood", + "minecraft:wooden_button", + "minecraft:wooden_door", + "minecraft:wooden_pressure_plate", -+ "minecraft:wooden_slab", -+ "minecraft:wool", + "minecraft:yellow_candle", -+ "minecraft:yellow_candle_cake", + "minecraft:yellow_carpet", + "minecraft:yellow_concrete", -+ "minecraft:yellow_concrete_powder", -+ "minecraft:yellow_glazed_terracotta", -+ "minecraft:yellow_poplar_leaves", + "minecraft:yellow_stained_glass", + "minecraft:yellow_stained_glass_pane", + "minecraft:yellow_terracotta", @@ -7723,103 +7818,154 @@ index 000000000..a5147cdb6 + { + "version": 0, + "maximum": 64, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 5, ++ "identifiers": [ ++ "minecraft:air", ++ "minecraft:allow", ++ "minecraft:banner_pattern", ++ "minecraft:barrier", ++ "minecraft:boat", ++ "minecraft:border_block", ++ "minecraft:carpet", ++ "minecraft:chain_command_block", ++ "minecraft:chemistry_table", ++ "minecraft:chest_boat", ++ "minecraft:colored_torch_bp", ++ "minecraft:colored_torch_rg", ++ "minecraft:command_block", ++ "minecraft:concrete", ++ "minecraft:concrete_powder", ++ "minecraft:coral", ++ "minecraft:coral_block", ++ "minecraft:coral_fan", ++ "minecraft:coral_fan_dead", ++ "minecraft:deny", ++ "minecraft:double_plant", ++ "minecraft:dye", ++ "minecraft:fence", ++ "minecraft:frosted_ice", ++ "minecraft:jigsaw", ++ "minecraft:leaves", ++ "minecraft:leaves2", ++ "minecraft:light_block", ++ "minecraft:light_block_0", ++ "minecraft:light_block_1", ++ "minecraft:light_block_10", ++ "minecraft:light_block_11", ++ "minecraft:light_block_12", ++ "minecraft:light_block_13", ++ "minecraft:light_block_14", ++ "minecraft:light_block_15", ++ "minecraft:light_block_2", ++ "minecraft:light_block_3", ++ "minecraft:light_block_4", ++ "minecraft:light_block_5", ++ "minecraft:light_block_6", ++ "minecraft:light_block_7", ++ "minecraft:light_block_8", ++ "minecraft:light_block_9", ++ "minecraft:lodestone_compass", ++ "minecraft:log", ++ "minecraft:log2", ++ "minecraft:monster_egg", ++ "minecraft:planks", ++ "minecraft:red_flower", ++ "minecraft:repeating_command_block", ++ "minecraft:sapling", ++ "minecraft:shulker_box", ++ "minecraft:skull", ++ "minecraft:spawn_egg", ++ "minecraft:stained_glass", ++ "minecraft:stained_glass_pane", ++ "minecraft:stained_hardened_clay", ++ "minecraft:stone_block_slab", ++ "minecraft:stone_block_slab2", ++ "minecraft:stone_block_slab3", ++ "minecraft:stone_block_slab4", ++ "minecraft:stonebrick", ++ "minecraft:structure_block", ++ "minecraft:structure_void", ++ "minecraft:tallgrass", ++ "minecraft:wood", ++ "minecraft:wooden_slab", ++ "minecraft:wool" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": false, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 6, ++ "identifiers": [ ++ "minecraft:bleach", ++ "minecraft:camera", ++ "minecraft:chalkboard", ++ "minecraft:client_request_placeholder_block", ++ "minecraft:double_stone_block_slab", ++ "minecraft:double_stone_block_slab2", ++ "minecraft:double_stone_block_slab3", ++ "minecraft:double_stone_block_slab4", ++ "minecraft:end_gateway", ++ "minecraft:glowingobsidian", ++ "minecraft:hard_stained_glass", ++ "minecraft:hard_stained_glass_pane", ++ "minecraft:info_update", ++ "minecraft:info_update2", ++ "minecraft:invisible_bedrock", ++ "minecraft:moving_block", ++ "minecraft:netherreactor", ++ "minecraft:rapid_fertilizer", ++ "minecraft:reserved6", ++ "minecraft:stonecutter" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": true, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 0, ++ "identifiers": [ ++ "minecraft:cinnabar_brick_double_slab", ++ "minecraft:cinnabar_double_slab", ++ "minecraft:polished_cinnabar_double_slab" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, + "stackedByData": true, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 1, + "identifiers": [ + "minecraft:acacia_double_slab", -+ "minecraft:acacia_sapling", -+ "minecraft:acacia_slab", -+ "minecraft:agent_spawn_egg", -+ "minecraft:allay_spawn_egg", + "minecraft:andesite_double_slab", -+ "minecraft:andesite_slab", -+ "minecraft:armadillo_spawn_egg", -+ "minecraft:axolotl_spawn_egg", -+ "minecraft:azalea_leaves", -+ "minecraft:azalea_leaves_flowered", -+ "minecraft:balloon", + "minecraft:bamboo_double_slab", + "minecraft:bamboo_mosaic_double_slab", -+ "minecraft:bamboo_mosaic_slab", -+ "minecraft:bamboo_slab", -+ "minecraft:bat_spawn_egg", -+ "minecraft:bee_spawn_egg", + "minecraft:birch_double_slab", -+ "minecraft:birch_sapling", -+ "minecraft:birch_slab", -+ "minecraft:black_dye", + "minecraft:blackstone_double_slab", -+ "minecraft:blackstone_slab", -+ "minecraft:blaze_spawn_egg", -+ "minecraft:blue_dye", -+ "minecraft:bogged_spawn_egg", -+ "minecraft:bone_meal", -+ "minecraft:brain_coral_fan", -+ "minecraft:breeze_spawn_egg", + "minecraft:brick_double_slab", -+ "minecraft:brick_slab", -+ "minecraft:brown_dye", -+ "minecraft:brown_mushroom_block", -+ "minecraft:bubble_coral_fan", -+ "minecraft:camel_husk_spawn_egg", -+ "minecraft:camel_spawn_egg", -+ "minecraft:cat_spawn_egg", -+ "minecraft:cave_spider_spawn_egg", + "minecraft:cherry_double_slab", -+ "minecraft:cherry_slab", -+ "minecraft:chicken_spawn_egg", -+ "minecraft:cinnabar_brick_double_slab", + "minecraft:cinnabar_brick_slab", -+ "minecraft:cinnabar_double_slab", + "minecraft:cinnabar_slab", + "minecraft:cobbled_deepslate_double_slab", -+ "minecraft:cobbled_deepslate_slab", -+ "minecraft:cobblestone_double_slab", -+ "minecraft:cobblestone_slab", -+ "minecraft:cocoa_beans", -+ "minecraft:cod_spawn_egg", -+ "minecraft:compound", -+ "minecraft:copper_block", -+ "minecraft:copper_golem_spawn_egg", -+ "minecraft:cow_spawn_egg", -+ "minecraft:creaking_spawn_egg", -+ "minecraft:creeper_head", -+ "minecraft:creeper_spawn_egg", -+ "minecraft:crimson_double_slab", -+ "minecraft:crimson_roots", -+ "minecraft:crimson_slab", -+ "minecraft:cut_copper", -+ "minecraft:cut_copper_slab", -+ "minecraft:cut_copper_stairs", ++ "minecraft:cobblestone_double_slab", ++ "minecraft:crimson_double_slab", + "minecraft:cut_red_sandstone_double_slab", -+ "minecraft:cut_red_sandstone_slab", + "minecraft:cut_sandstone_double_slab", -+ "minecraft:cut_sandstone_slab", -+ "minecraft:cyan_dye", -+ "minecraft:dandelion", + "minecraft:dark_oak_double_slab", -+ "minecraft:dark_oak_sapling", -+ "minecraft:dark_oak_slab", + "minecraft:dark_prismarine_double_slab", -+ "minecraft:dark_prismarine_slab", -+ "minecraft:dead_brain_coral_fan", -+ "minecraft:dead_bubble_coral_fan", -+ "minecraft:dead_fire_coral_fan", -+ "minecraft:dead_horn_coral_fan", -+ "minecraft:dead_tube_coral_fan", + "minecraft:deepslate_brick_double_slab", -+ "minecraft:deepslate_brick_slab", + "minecraft:deepslate_tile_double_slab", -+ "minecraft:deepslate_tile_slab", + "minecraft:diorite_double_slab", -+ "minecraft:diorite_slab", -+ "minecraft:dolphin_spawn_egg", -+ "minecraft:donkey_spawn_egg", + "minecraft:double_cut_copper_slab", -+ "minecraft:dragon_head", -+ "minecraft:drowned_spawn_egg", -+ "minecraft:elder_guardian_spawn_egg", + "minecraft:element_0", + "minecraft:element_1", + "minecraft:element_10", @@ -7939,25 +8085,150 @@ index 000000000..a5147cdb6 + "minecraft:element_97", + "minecraft:element_98", + "minecraft:element_99", ++ "minecraft:end_stone_brick_double_slab", ++ "minecraft:exposed_double_cut_copper_slab", ++ "minecraft:granite_double_slab", ++ "minecraft:jungle_double_slab", ++ "minecraft:mangrove_double_slab", ++ "minecraft:mossy_cobblestone_double_slab", ++ "minecraft:mossy_stone_brick_double_slab", ++ "minecraft:mud_brick_double_slab", ++ "minecraft:nether_brick_double_slab", ++ "minecraft:normal_stone_double_slab", ++ "minecraft:oak_double_slab", ++ "minecraft:oxidized_double_cut_copper_slab", ++ "minecraft:pale_oak_double_slab", ++ "minecraft:pale_oak_slab", ++ "minecraft:petrified_oak_double_slab", ++ "minecraft:polished_andesite_double_slab", ++ "minecraft:polished_blackstone_brick_double_slab", ++ "minecraft:polished_blackstone_double_slab", ++ "minecraft:polished_cinnabar_slab", ++ "minecraft:polished_deepslate_double_slab", ++ "minecraft:polished_diorite_double_slab", ++ "minecraft:polished_granite_double_slab", ++ "minecraft:polished_sulfur_double_slab", ++ "minecraft:polished_sulfur_slab", ++ "minecraft:polished_tuff_double_slab", ++ "minecraft:poplar_double_slab", ++ "minecraft:poplar_slab", ++ "minecraft:prismarine_brick_double_slab", ++ "minecraft:prismarine_double_slab", ++ "minecraft:purpur_double_slab", ++ "minecraft:quartz_double_slab", ++ "minecraft:red_nether_brick_double_slab", ++ "minecraft:red_sandstone_double_slab", ++ "minecraft:resin_brick_double_slab", ++ "minecraft:sandstone_double_slab", ++ "minecraft:smooth_quartz_double_slab", ++ "minecraft:smooth_red_sandstone_double_slab", ++ "minecraft:smooth_sandstone_double_slab", ++ "minecraft:smooth_stone_double_slab", ++ "minecraft:spruce_double_slab", ++ "minecraft:stone_brick_double_slab", ++ "minecraft:sulfur_brick_double_slab", ++ "minecraft:sulfur_brick_slab", ++ "minecraft:sulfur_double_slab", ++ "minecraft:sulfur_slab", ++ "minecraft:tuff_brick_double_slab", ++ "minecraft:tuff_double_slab", ++ "minecraft:warped_double_slab", ++ "minecraft:waxed_double_cut_copper_slab", ++ "minecraft:waxed_exposed_double_cut_copper_slab", ++ "minecraft:waxed_oxidized_double_cut_copper_slab", ++ "minecraft:waxed_weathered_double_cut_copper_slab", ++ "minecraft:weathered_double_cut_copper_slab" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": true, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 4, ++ "identifiers": [ ++ "minecraft:acacia_sapling", ++ "minecraft:acacia_slab", ++ "minecraft:allay_spawn_egg", ++ "minecraft:andesite_slab", ++ "minecraft:armadillo_spawn_egg", ++ "minecraft:axolotl_spawn_egg", ++ "minecraft:azalea_leaves", ++ "minecraft:azalea_leaves_flowered", ++ "minecraft:bamboo_mosaic_slab", ++ "minecraft:bamboo_slab", ++ "minecraft:bat_spawn_egg", ++ "minecraft:bee_spawn_egg", ++ "minecraft:birch_sapling", ++ "minecraft:birch_slab", ++ "minecraft:black_dye", ++ "minecraft:blackstone_slab", ++ "minecraft:blaze_spawn_egg", ++ "minecraft:blue_dye", ++ "minecraft:bogged_spawn_egg", ++ "minecraft:bone_meal", ++ "minecraft:brain_coral_fan", ++ "minecraft:breeze_spawn_egg", ++ "minecraft:brick_slab", ++ "minecraft:brown_dye", ++ "minecraft:brown_mushroom_block", ++ "minecraft:bubble_coral_fan", ++ "minecraft:camel_husk_spawn_egg", ++ "minecraft:camel_spawn_egg", ++ "minecraft:cat_spawn_egg", ++ "minecraft:cave_spider_spawn_egg", ++ "minecraft:cherry_slab", ++ "minecraft:chicken_spawn_egg", ++ "minecraft:cobbled_deepslate_slab", ++ "minecraft:cobblestone_slab", ++ "minecraft:cocoa_beans", ++ "minecraft:cod_spawn_egg", ++ "minecraft:copper_block", ++ "minecraft:copper_golem_spawn_egg", ++ "minecraft:cow_spawn_egg", ++ "minecraft:creaking_spawn_egg", ++ "minecraft:creeper_head", ++ "minecraft:creeper_spawn_egg", ++ "minecraft:crimson_roots", ++ "minecraft:crimson_slab", ++ "minecraft:cut_copper", ++ "minecraft:cut_copper_slab", ++ "minecraft:cut_copper_stairs", ++ "minecraft:cut_red_sandstone_slab", ++ "minecraft:cut_sandstone_slab", ++ "minecraft:cyan_dye", ++ "minecraft:dandelion", ++ "minecraft:dark_oak_sapling", ++ "minecraft:dark_oak_slab", ++ "minecraft:dark_prismarine_slab", ++ "minecraft:dead_brain_coral_fan", ++ "minecraft:dead_bubble_coral_fan", ++ "minecraft:dead_fire_coral_fan", ++ "minecraft:dead_horn_coral_fan", ++ "minecraft:dead_tube_coral_fan", ++ "minecraft:deepslate_brick_slab", ++ "minecraft:deepslate_tile_slab", ++ "minecraft:diorite_slab", ++ "minecraft:dolphin_spawn_egg", ++ "minecraft:donkey_spawn_egg", ++ "minecraft:dragon_head", ++ "minecraft:drowned_spawn_egg", ++ "minecraft:elder_guardian_spawn_egg", + "minecraft:empty_map", + "minecraft:end_portal_frame", -+ "minecraft:end_stone_brick_double_slab", + "minecraft:end_stone_brick_slab", -+ "minecraft:ender_dragon_spawn_egg", + "minecraft:enderman_spawn_egg", + "minecraft:endermite_spawn_egg", + "minecraft:evoker_spawn_egg", + "minecraft:exposed_cut_copper_slab", -+ "minecraft:exposed_double_cut_copper_slab", + "minecraft:farmland", -+ "minecraft:filled_map", + "minecraft:fire_coral_fan", + "minecraft:fox_spawn_egg", + "minecraft:frog_spawn_egg", + "minecraft:ghast_spawn_egg", + "minecraft:glow_squid_spawn_egg", + "minecraft:goat_spawn_egg", -+ "minecraft:granite_double_slab", + "minecraft:granite_slab", + "minecraft:gray_dye", + "minecraft:green_dye", @@ -7969,7 +8240,6 @@ index 000000000..a5147cdb6 + "minecraft:husk_spawn_egg", + "minecraft:ink_sac", + "minecraft:iron_golem_spawn_egg", -+ "minecraft:jungle_double_slab", + "minecraft:jungle_sapling", + "minecraft:jungle_slab", + "minecraft:lapis_lazuli", @@ -7979,38 +8249,25 @@ index 000000000..a5147cdb6 + "minecraft:llama_spawn_egg", + "minecraft:magenta_dye", + "minecraft:magma_cube_spawn_egg", -+ "minecraft:mangrove_double_slab", + "minecraft:mangrove_leaves", + "minecraft:mangrove_slab", + "minecraft:mooshroom_spawn_egg", -+ "minecraft:mossy_cobblestone_double_slab", + "minecraft:mossy_cobblestone_slab", -+ "minecraft:mossy_stone_brick_double_slab", + "minecraft:mossy_stone_brick_slab", -+ "minecraft:mud_brick_double_slab", + "minecraft:mud_brick_slab", + "minecraft:mule_spawn_egg", + "minecraft:nautilus_spawn_egg", -+ "minecraft:nether_brick_double_slab", + "minecraft:nether_brick_slab", -+ "minecraft:normal_stone_double_slab", + "minecraft:normal_stone_slab", -+ "minecraft:npc_spawn_egg", -+ "minecraft:oak_double_slab", + "minecraft:oak_sapling", + "minecraft:oak_slab", + "minecraft:ocelot_spawn_egg", + "minecraft:ominous_bottle", + "minecraft:orange_dye", + "minecraft:oxidized_cut_copper_slab", -+ "minecraft:oxidized_double_cut_copper_slab", -+ "minecraft:pale_oak_double_slab", -+ "minecraft:pale_oak_slab", + "minecraft:panda_spawn_egg", + "minecraft:parched_spawn_egg", + "minecraft:parrot_spawn_egg", -+ "minecraft:petrified_oak_double_slab", -+ "minecraft:petrified_oak_slab", + "minecraft:phantom_spawn_egg", + "minecraft:pig_spawn_egg", + "minecraft:piglin_brute_spawn_egg", @@ -8020,49 +8277,28 @@ index 000000000..a5147cdb6 + "minecraft:pink_dye", + "minecraft:player_head", + "minecraft:polar_bear_spawn_egg", -+ "minecraft:polished_andesite_double_slab", + "minecraft:polished_andesite_slab", -+ "minecraft:polished_blackstone_brick_double_slab", + "minecraft:polished_blackstone_brick_slab", -+ "minecraft:polished_blackstone_double_slab", + "minecraft:polished_blackstone_slab", -+ "minecraft:polished_cinnabar_double_slab", -+ "minecraft:polished_cinnabar_slab", -+ "minecraft:polished_deepslate_double_slab", + "minecraft:polished_deepslate_slab", -+ "minecraft:polished_diorite_double_slab", + "minecraft:polished_diorite_slab", -+ "minecraft:polished_granite_double_slab", + "minecraft:polished_granite_slab", -+ "minecraft:polished_sulfur_double_slab", -+ "minecraft:polished_sulfur_slab", -+ "minecraft:polished_tuff_double_slab", + "minecraft:polished_tuff_slab", -+ "minecraft:poplar_double_slab", -+ "minecraft:poplar_slab", -+ "minecraft:prismarine_brick_double_slab", + "minecraft:prismarine_brick_slab", -+ "minecraft:prismarine_double_slab", + "minecraft:prismarine_slab", + "minecraft:pufferfish_spawn_egg", + "minecraft:purple_dye", -+ "minecraft:purpur_double_slab", + "minecraft:purpur_slab", -+ "minecraft:quartz_double_slab", + "minecraft:quartz_slab", + "minecraft:rabbit_spawn_egg", + "minecraft:ravager_spawn_egg", + "minecraft:red_dye", + "minecraft:red_mushroom_block", -+ "minecraft:red_nether_brick_double_slab", + "minecraft:red_nether_brick_slab", -+ "minecraft:red_sandstone_double_slab", + "minecraft:red_sandstone_slab", -+ "minecraft:resin_brick_double_slab", + "minecraft:resin_brick_slab", + "minecraft:respawn_anchor", + "minecraft:salmon_spawn_egg", -+ "minecraft:sandstone_double_slab", + "minecraft:sandstone_slab", + "minecraft:seagrass", + "minecraft:sheep_spawn_egg", @@ -8072,38 +8308,26 @@ index 000000000..a5147cdb6 + "minecraft:skeleton_skull", + "minecraft:skeleton_spawn_egg", + "minecraft:slime_spawn_egg", -+ "minecraft:smooth_quartz_double_slab", + "minecraft:smooth_quartz_slab", -+ "minecraft:smooth_red_sandstone_double_slab", + "minecraft:smooth_red_sandstone_slab", -+ "minecraft:smooth_sandstone_double_slab", + "minecraft:smooth_sandstone_slab", -+ "minecraft:smooth_stone_double_slab", + "minecraft:smooth_stone_slab", + "minecraft:sniffer_spawn_egg", + "minecraft:snow_golem_spawn_egg", + "minecraft:snow_layer", + "minecraft:spider_spawn_egg", -+ "minecraft:spruce_double_slab", + "minecraft:spruce_sapling", + "minecraft:spruce_slab", + "minecraft:squid_spawn_egg", -+ "minecraft:stone_brick_double_slab", + "minecraft:stone_brick_slab", + "minecraft:stray_spawn_egg", + "minecraft:strider_spawn_egg", -+ "minecraft:sulfur_brick_double_slab", -+ "minecraft:sulfur_brick_slab", + "minecraft:sulfur_cube_spawn_egg", -+ "minecraft:sulfur_double_slab", -+ "minecraft:sulfur_slab", + "minecraft:tadpole_spawn_egg", + "minecraft:trader_llama_spawn_egg", + "minecraft:tropical_fish_spawn_egg", + "minecraft:tube_coral_fan", -+ "minecraft:tuff_brick_double_slab", + "minecraft:tuff_brick_slab", -+ "minecraft:tuff_double_slab", + "minecraft:tuff_slab", + "minecraft:turtle_spawn_egg", + "minecraft:vex_spawn_egg", @@ -8111,24 +8335,17 @@ index 000000000..a5147cdb6 + "minecraft:vindicator_spawn_egg", + "minecraft:wandering_trader_spawn_egg", + "minecraft:warden_spawn_egg", -+ "minecraft:warped_double_slab", + "minecraft:warped_roots", + "minecraft:warped_slab", + "minecraft:waxed_cut_copper_slab", -+ "minecraft:waxed_double_cut_copper_slab", + "minecraft:waxed_exposed_cut_copper_slab", -+ "minecraft:waxed_exposed_double_cut_copper_slab", + "minecraft:waxed_oxidized_cut_copper_slab", -+ "minecraft:waxed_oxidized_double_cut_copper_slab", + "minecraft:waxed_weathered_cut_copper_slab", -+ "minecraft:waxed_weathered_double_cut_copper_slab", + "minecraft:weathered_cut_copper_slab", -+ "minecraft:weathered_double_cut_copper_slab", + "minecraft:white_dye", + "minecraft:witch_spawn_egg", + "minecraft:wither_skeleton_skull", + "minecraft:wither_skeleton_spawn_egg", -+ "minecraft:wither_spawn_egg", + "minecraft:wolf_spawn_egg", + "minecraft:yellow_dye", + "minecraft:zoglin_spawn_egg", @@ -8142,11 +8359,40 @@ index 000000000..a5147cdb6 + }, + { + "version": 0, ++ "maximum": 64, ++ "stackedByData": true, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 5, ++ "identifiers": [ ++ "minecraft:ender_dragon_spawn_egg", ++ "minecraft:filled_map", ++ "minecraft:petrified_oak_slab", ++ "minecraft:wither_spawn_egg" ++ ] ++ }, ++ { ++ "version": 0, ++ "maximum": 64, ++ "stackedByData": true, ++ "maximumDamage": 0, ++ "damageable": false, ++ "creativeCategory": 6, ++ "identifiers": [ ++ "minecraft:agent_spawn_egg", ++ "minecraft:balloon", ++ "minecraft:compound", ++ "minecraft:npc_spawn_egg" ++ ] ++ }, ++ { ++ "version": 0, + "maximum": 1, + "stackedByData": true, + "maximumDamage": 0, + "damageable": false, + "fixedMaximum": 1, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:axolotl_bucket", + "minecraft:cod_bucket", @@ -8168,6 +8414,7 @@ index 000000000..a5147cdb6 + "maximumDamage": 0, + "damageable": false, + "fixedMaximum": 16, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:bucket" + ] @@ -8178,6 +8425,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:music_disc_11", + "minecraft:music_disc_13", @@ -8209,6 +8457,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 384, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:fishing_rod" + ] @@ -8219,6 +8468,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 100, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:warped_fungus_on_a_stick" + ] @@ -8229,6 +8479,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 64, + "damageable": true, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:wolf_armor" + ] @@ -8239,6 +8490,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:snowball" + ] @@ -8249,6 +8501,7 @@ index 000000000..a5147cdb6 + "stackedByData": false, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:brewing_stand", + "minecraft:cake", @@ -8272,6 +8525,7 @@ index 000000000..a5147cdb6 + "stackedByData": true, + "maximumDamage": 0, + "damageable": false, ++ "creativeCategory": 4, + "identifiers": [ + "minecraft:arrow", + "minecraft:cauldron", @@ -17481,10 +17735,10 @@ index 000000000..fc21ff889 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java new file mode 100644 -index 000000000..eaf0eb96f +index 000000000..2e960c452 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryTest.java -@@ -0,0 +1,338 @@ +@@ -0,0 +1,408 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -17818,6 +18072,76 @@ index 000000000..eaf0eb96f + assertTrue(apple.componentItems().isEmpty()); + } + ++ @Test ++ void creativeCategoriesFollowConstructionBeforeOrderedComponentUpdates() { ++ final String identifier = "test:category"; ++ final var first = categoryEntry(identifier, 20, ItemVersion.None, 2); ++ final var constructed = categoryEntry(identifier, 21, ItemVersion.DataDriven, 259); ++ final var ignoredVersion = categoryEntry(identifier, 22, ItemVersion.Legacy, 260); ++ final var registry = new ItemRegistry(new ItemEntry[]{first, constructed, ignoredVersion}); ++ assertEquals(0, registry.creativeCategory(20)); ++ assertEquals(0, registry.creativeCategory(21)); ++ assertEquals(4, registry.creativeCategory(22)); ++ assertEquals(4, new ItemRegistry(new ItemEntry[]{first}).creativeCategory(20)); ++ assertEquals(4, new ItemRegistry(new ItemEntry[]{categoryEntry(identifier, 20, ItemVersion.Legacy, 1)}).creativeCategory(20)); ++ assertEquals(3, new ItemRegistry(new ItemEntry[]{first, constructed}).creativeCategory(21)); ++ } ++ ++ @Test ++ void categoryUpdatesRequireIntegerPropertiesAndRetainOmittedProperties() { ++ final String identifier = "test:category"; ++ final var first = categoryEntry(identifier, 20, ItemVersion.DataDriven, -253); ++ for (final int type : new int[]{0, 1, 3, 4, 8, 10}) { ++ final var properties = new CompoundTag(); ++ switch (type) { ++ case 1 -> properties.putByte("creative_category", (byte) 2); ++ case 3 -> properties.putInt("creative_category", 258); ++ case 4 -> properties.putLong("creative_category", 2); ++ case 8 -> properties.putString("creative_category", "2"); ++ case 10 -> properties.put("creative_category", new CompoundTag()); ++ default -> { } ++ } ++ final var components = new CompoundTag(); ++ components.put("item_properties", properties); ++ final var data = new CompoundTag(); ++ data.put("components", components); ++ final var update = new ItemEntry(identifier, 21, true, ItemVersion.None, data); ++ assertEquals(type == 3 ? 2 : 0, new ItemRegistry(new ItemEntry[]{first, update}).creativeCategory(21)); ++ } ++ final var components = new CompoundTag(); ++ components.putInt("item_properties", 4); ++ final var data = new CompoundTag(); ++ data.put("components", components); ++ final var malformed = new ItemEntry(identifier, 21, true, ItemVersion.None, data); ++ assertEquals(3, new ItemRegistry(new ItemEntry[]{first, malformed}).creativeCategory(21)); ++ assertEquals(3, new ItemRegistry(new ItemEntry[]{first, entry(identifier, 21, false, ItemVersion.None)}).creativeCategory(21)); ++ } ++ ++ @Test ++ void categoryLookupUsesTheFinalDefinitionAndNativeShortIdentifier() { ++ final var first = categoryEntry("test:first", -302, ItemVersion.DataDriven, 1); ++ final var replacement = categoryEntry("test:replacement", -302, ItemVersion.DataDriven, 3); ++ final var invalid = categoryEntry("test:replacement", -1, ItemVersion.None, 4); ++ final var registry = new ItemRegistry(new ItemEntry[]{first, replacement, invalid}); ++ assertEquals(4, registry.creativeCategory(-302)); ++ assertEquals(4, registry.creativeCategory(65536 - 302)); ++ assertEquals(0, registry.creativeCategory(-1)); ++ assertEquals(0, registry.creativeCategory(0)); ++ assertEquals(0, registry.creativeCategory(123)); ++ final var recreated = new ItemRegistry(new ItemEntry[]{first, replacement}); ++ assertEquals(3, recreated.creativeCategory(-302)); ++ } ++ ++ private static ItemEntry categoryEntry(final String identifier, final int id, final ItemVersion version, final int category) { ++ final var properties = new CompoundTag(); ++ properties.putInt("creative_category", category); ++ final var components = new CompoundTag(); ++ components.put("item_properties", properties); ++ final var data = new CompoundTag(); ++ data.put("components", components); ++ return new ItemEntry(identifier, id, true, version, data); ++ } ++ + private static ItemEntry entry(final String identifier, final int id, final boolean componentBased, final ItemVersion version) { + return new ItemEntry(identifier, id, componentBased, version, null); + } diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md index 2697244b..d9a7224b 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md @@ -715,3 +715,23 @@ A fresh BDS run passes all three cooking families and same-world reconnect with Both launches match its checksum. All nine observed owned identities retire, and the private display stops normally. Java cooking screens handle these controls. Unsupported authored preference mappings still prevent native screen acceptance. The client source tree is unchanged by this naming update. + +## Definition-owned creative categories + +Retain the definition category used by the native player search source. +The existing factory table now includes all 1,949 constructor defaults from complete pinned native registry initialization. +New definitions start at category four. Explicit component construction replaces the built-in default before ordered component readers run. + +A compound `item_properties` sets the low byte of integer `creative_category`, with zero for absent or mistyped values. +Omitted or mistyped property blocks preserve the previous category. Legacy readers leave it unchanged. +Unknown `None` entries receive their fallback after component readers, and final runtime-ID replacement selects the surviving definition. + +Complete native component construction, binary NBT parsing, property loading and destruction cover 279 typed inputs and five retained-update steps. +The complete legacy reader preserves its category across the same inputs. +The built-in reference uses the retained native block bootstrap and an empty network vector; it establishes defaults rather than a normal server packet baseline. +These facts support catalog acquisition. They do not establish recipe unlocks, craftability, authored player screens or physical input. + +The complete four-project build passes 2,830 tests with 163 optional skips and no failures or errors. +The packaged core matches all 1,949 category defaults, 563 typed-update comparisons and 906 search rebuilds. +All 1,466 core classes and the factory table match the add-on and proxy copies; all 1,191 client classes match the tested add-on. +The previous 7,728 eligibility and collection/history comparisons still pass against the packaged core. diff --git a/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch b/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch index 93dd8113..702808be 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch @@ -1,4 +1,4 @@ -From a33ac728417bcec61ec20b58ac9a59f884d2b58b Mon Sep 17 00:00:00 2001 +From afb75aa2fa0e1984205efbfee203f69023d898b1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(bedrock): show server creative items in a Bedrock tab @@ -14,8 +14,8 @@ Retain the exact native catalog entry and complete item snapshot through Java cr .../world/item/BedrockCreativeTab.java | 55 ++++++ .../viafabricplus-bedrock/lang/en_us.json | 2 + .../viafabricplus-bedrock.mixins.json | 3 + - .../creative/BedrockCreativeCatalogTest.java | 158 ++++++++++++++++++ - 8 files changed, 460 insertions(+) + .../creative/BedrockCreativeCatalogTest.java | 160 ++++++++++++++++++ + 8 files changed, 462 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalog.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeInventoryScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTab_ItemDisplayBuilder.java @@ -25,7 +25,7 @@ Retain the exact native catalog entry and complete item snapshot through Java cr diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalog.java b/src/main/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalog.java new file mode 100644 -index 0000000..c50c8b4 +index 00000000..c50c8b4a --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalog.java @@ -0,0 +1,82 @@ @@ -113,7 +113,7 @@ index 0000000..c50c8b4 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeInventoryScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeInventoryScreen.java new file mode 100644 -index 0000000..447b4a7 +index 00000000..447b4a77 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeInventoryScreen.java @@ -0,0 +1,26 @@ @@ -145,7 +145,7 @@ index 0000000..447b4a7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTab_ItemDisplayBuilder.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTab_ItemDisplayBuilder.java new file mode 100644 -index 0000000..1526ed5 +index 00000000..1526ed53 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTab_ItemDisplayBuilder.java @@ -0,0 +1,74 @@ @@ -225,7 +225,7 @@ index 0000000..1526ed5 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTabs.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTabs.java new file mode 100644 -index 0000000..2af5f4e +index 00000000..2af5f4ed --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTabs.java @@ -0,0 +1,60 @@ @@ -291,7 +291,7 @@ index 0000000..2af5f4e +} diff --git a/src/main/java/net/minecraft/world/item/BedrockCreativeTab.java b/src/main/java/net/minecraft/world/item/BedrockCreativeTab.java new file mode 100644 -index 0000000..e8f418f +index 00000000..e8f418f1 --- /dev/null +++ b/src/main/java/net/minecraft/world/item/BedrockCreativeTab.java @@ -0,0 +1,55 @@ @@ -351,7 +351,7 @@ index 0000000..e8f418f + } +} diff --git a/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json b/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json -index ab90032..db52332 100644 +index ab900320..db52332e 100644 --- a/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json +++ b/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json @@ -2,6 +2,8 @@ @@ -364,7 +364,7 @@ index ab90032..db52332 100644 "bedrock_settings.viafabricplus.account": "Account for Bedrock Edition", "bedrock_settings.viafabricplus.account.login": "Click to log in", diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 66baeef..788e1b7 100644 +index 66baeef6..788e1b70 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -59,6 +59,9 @@ @@ -379,10 +379,10 @@ index 66baeef..788e1b7 100644 "features.misc.MixinInventoryScreen", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java new file mode 100644 -index 0000000..0c8028b +index 00000000..896cd6a0 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java -@@ -0,0 +1,158 @@ +@@ -0,0 +1,160 @@ +package com.viaversion.viafabricplus.bedrock.creative; + +import com.viaversion.nbt.tag.CompoundTag; @@ -412,6 +412,8 @@ index 0000000..0c8028b +import net.raphimc.viabedrock.api.model.NativeItemStackData; +import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.CreativeCatalog; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; +import net.raphimc.viabedrock.protocol.rewriter.ItemDataRewriter; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; @@ -476,7 +478,7 @@ index 0000000..0c8028b + bindMappings(data); + final var item = new StructuredItem(BuiltInRegistries.ITEM.getId(Items.PAPER), 1, data); + final var catalog = new CreativeCatalog(List.of(), List.of( -+ new CreativeCatalog.Entry(42, 0, nativeItem, item, true, false))); ++ new CreativeCatalog.Entry(42, 0, nativeItem, item, true, false)), new ItemRegistry(new ItemEntry[0])); + final var result = BedrockCreativeCatalog.toNative(catalog.entries().getFirst().javaItem(), + RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY)); + assertEquals(1, result.getMaxStackSize()); @@ -506,7 +508,7 @@ index 0000000..0c8028b + final var icon = mappedItem(3); + final var catalog = new CreativeCatalog(List.of( + new CreativeCatalog.Group(2, "flower", icon)), List.of( -+ entry(41, 0, 3), entry(42, 0, 7), entry(43, 5, 9))); ++ entry(41, 0, 3), entry(42, 0, 7), entry(43, 5, 9)), new ItemRegistry(new ItemEntry[0])); + assertEquals(List.of(41, 42), BedrockCreativeCatalog.entriesForDisplay(catalog, 3).stream() + .map(CreativeCatalog.Entry::netId).toList()); + assertTrue(BedrockCreativeCatalog.entriesForDisplay(catalog, 7).isEmpty()); From d82ebd016465467eb9e2dcfba47a61879b3027d1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:08:27 +0200 Subject: [PATCH 03/12] fix(inventory): preserve creative categories across catalog replacement Creative membership rewrites a shared definition category before search reads it. Keep the final category across catalog replacement, including variants that receive no new membership, so hidden entries cannot return to search through their initial registry defaults. Fold native group registration and ordered category membership into the owning catalog patch. Retain exact entry identities and separate palette, search and category sources for later player collection integration. All four projects build with 2,833 tests passed and 163 optional skips. The packaged core matches 186 complete native content controls and retains 11,146 category, search, eligibility and collection comparisons. Native row population, availability evaluation and the full player screen remain open. --- docs/native-player-inventory.md | 50 +++++- ...erver-authoritative-bedrock-requests.patch | 6 +- ...08-signal-sustained-bedrock-item-use.patch | 6 +- ...ain-bedrock-creative-catalog-entries.patch | 169 ++++++++++++++++-- ...rate-authored-native-ui-presentation.patch | 32 +++- ...rate-authored-native-ui-presentation.pr.md | 8 + ...rver-creative-items-in-a-bedrock-tab.patch | 24 ++- 7 files changed, 259 insertions(+), 36 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 3f6ad38b..9ceac1a1 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -124,20 +124,22 @@ Model builder `0x1434360d0` creates the item collections and four category recip It also creates a separate search collection. These recipe collections share constructor `0x143443e50`. The category counts use the source category's group references; search uses the complete source list. This model construction evidence comes from disassembly. The player search source now uses definition categories from the connection registry. -The four grouped category sources, recipe availability evaluation and mutation remain unfinished. +The connection catalog now retains the category membership sources. +Population into collection rows, recipe availability evaluation and mutation remain unfinished. The player screen must retain the complete catalog and recipes that need a crafting table. A fabricated empty recipe catalog cannot establish support for the default inventory asset. -## Player search catalog +## Player search and category catalogs -This section describes the unreleased player search catalog. +This section describes the unreleased player catalog changes. The model constructor receives the local player and stores it at offset `0x30`. The player owns the search vector at offsets `0x600` through `0x610`. The original rebuild routine is `0x14020ec60`. It copies creative registry entries in their existing order and admits definition categories one through four. An absent definition or any other category excludes the entry. -Group category, item count, Java fallback identity and crafting-grid size do not select this source. +The rebuild reads the current definition byte after creative membership updates. +Item count, Java fallback identity and crafting-grid size do not select this source. Text search, unlocks and craftability run later through separate evaluators. @@ -153,12 +155,48 @@ An absent or mistyped `item_properties` block preserves the previous byte. Unknown `None` registrations receive their fallback definition after component updates. Final runtime-ID replacement selects the surviving definition. +Creative content registers categories one through five in packet order. +The first named declaration within each category retains its icon. +Anonymous groups remain separate and use an empty icon. +Repeated named declarations and unsupported categories do not take a group slot. +Item group indices address the registered vector directly. + +Membership updates the shared definition category in entry order. +The final assignment controls search admission for every variant of that definition. +A later catalog preserves categories for definitions that receive no new membership. +The complete palette, search source and category sources remain separate snapshots. +Category sources visit groups in registration order, then members in entry order. + `CreativeCatalog` retains a separate search list when `ItemRewriter` receives the server catalog. This list uses the connection registry and preserves the admitted entries' identities, variants, duplicates and order. The complete creative catalog remains available independently. The search list does not discard recipes that require a crafting table. The authored player screen and its evaluator still need integration. +Complete original creative-registry construction, content replacement, category setup, group registration and member assignment execute 186 controls. +Definition rewrites, search rebuilding and registry destruction execute too, with no remaining native allocations. +Descriptor conversion, typed item backing, item copy/destruction, diagnostics and ordinary allocator/CRT calls remain supplied boundaries. +These controls cover replacement, duplicate names, anonymous groups, unsupported categories, direct registered indices and shared definition updates. +The prior committed catalog admits two search rows after hidden membership; the corrected packaged core and native reference both admit zero. + +### Population into recipe collections + +Original provider `0x143439c20` visits search first, then construction, nature, equipment and items. +Its budget derives from the total source count and persists across frames. +The executed guard consumes work only when that total exceeds seven; smaller controls retain their initial phase. +Full caller construction and publication remain outside this reference. + +Source assignment runs `0x1434463c0` and `0x143446410`. +It stores the first matching creative entry index, with native metadata wildcard matching and independent stack counts. +The collection then resolves that entry's group reference. +This index is not the selected entry's packet group index. +The future adapter must retain this distinction when it supplies group identity to `RecipeCollection`. + +The complete provider, source assignment and native item comparison execute 181 controls across 2,296 frames. +Collection-name lookup, receiver lookup and registry-reference delivery remain supplied interfaces. +The reference uses controlled ordinary definitions, nullable userdata, block state and child state. +Native packet decoding, recipe evaluation, publication, rendering and physical input remain unverified. + The complete original search rebuild and vector growth execute 906 controls. They cover all category bytes, absent definitions, duplicates, allocation boundaries and repeated replacement. Item copy/destruction, allocator and CRT calls use supplied boundaries. @@ -169,7 +207,7 @@ Built-in registration executes with the retained versioned block bootstrap and a This establishes constructor defaults rather than a server packet baseline. Physical inventory rendering, unlock policy, recipe solving and controller input remain unverified. -All four projects build with 2,830 tests passed, 163 optional skips and no failures or errors. +All four projects build with 2,833 tests passed, 163 optional skips and no failures or errors. The packaged core matches 1,949 factory defaults, 563 typed-update comparisons and 906 search rebuilds. It also retains all 7,728 original eligibility and collection/history comparisons. All 1,466 core classes and the factory table match the embedded add-on and proxy copies. @@ -181,7 +219,7 @@ Release 0.3.56 includes this shared collection model. The shared `RecipeCollection` model implements the target collection ordering and retained header expansion. Cooking uses the same partitioner for its flat visible recipe prefix. -The player controller still needs the four grouped category sources before it can use grouped collections. +The player controller still needs native row population and its availability evaluator before it can bind these category sources. Entries retain four stable evaluator buckets. A contiguous catalog group moves as one run: state zero wins, then state one; otherwise the first member selects state two or three. diff --git a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index a362bc58..d1451eac 100644 --- a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch +++ b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch @@ -1,4 +1,4 @@ -From 57fffe626f4870508cfaee599b64db4ca0a06c49 Mon Sep 17 00:00:00 2001 +From 085330c805917fad91969bbff2b90a8cbecc4eb5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(inventory): translate server-authoritative Bedrock @@ -7747,7 +7747,7 @@ index 000000000..19e0148c5 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -index 4a15ae8e0..2b41a4e79 100644 +index c20f5563f..d8bf335cc 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java @@ -13,6 +13,8 @@ import com.viaversion.nbt.tag.IntTag; @@ -7768,7 +7768,7 @@ index 4a15ae8e0..2b41a4e79 100644 import java.util.stream.Collectors; /** One connection's item IDs and the complete definitions that own those IDs. */ -@@ -120,6 +124,75 @@ public final class ItemRegistry { +@@ -131,6 +135,75 @@ public final class ItemRegistry { return Map.copyOf(result); } diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index f45febfe..cf11613e 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From ab2cc63124ce483a79ce1910370d199e8697abcd Mon Sep 17 00:00:00 2001 +From 9a17321aed4971c39a488016c6c6f8397090df30 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): signal sustained Bedrock item use @@ -99,10 +99,10 @@ index 000000000..c4e63834c + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -index 2b41a4e79..0f480838d 100644 +index d8bf335cc..d4de80071 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -@@ -265,6 +265,20 @@ public final class ItemRegistry { +@@ -276,6 +276,20 @@ public final class ItemRegistry { return "item." + Key.stripMinecraftNamespace(identifier); } diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index c0a124ce..59326187 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From 46baa49d1180225f6709a60620b55e0b8191f153 Mon Sep 17 00:00:00 2001 +From 554f9ab1225394eaa48033c5f3ab080d913dc477 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -39,26 +39,47 @@ The complete original rebuild and vector growth/cleanup execute 906 controls, including every category byte, absent definitions, duplicates, allocation boundaries and repeated replacement. Item copy/destruction, allocation and CRT boundaries are supplied. Text, unlock and craftability -evaluators, grouped category sources and authored player screen/input +evaluators, native collection population and authored player screen/input integration remain separate work. + +Construct creative groups with the native category/name rules before +assigning entries. Only categories one through five register. Named +groups retain their first declaration within a category; anonymous groups +remain distinct and ignore their supplied icon. Entry group indices address +the registered group vector directly rather than a remapped wire table. + +Membership rewrites the shared item definition category in entry order. +The final update controls search admission for every variant of that +definition, and survives subsequent catalog replacement without membership. +Retain immutable group and category sources in group/member order for the +player model while keeping the complete palette and search independent. + +Complete pinned registry construction, content replacement, native group +registration, member assignment, definition updates, search rebuild and +registry destruction execute 186 controls. Descriptor conversion, typed +item backing, item copies/destruction, diagnostics and ordinary allocator/ +CRT boundaries remain supplied. The frame population reference also +executes 181 original controls across 2,296 frames, including first-match +catalog identity; frame publication, evaluator and player UI integration +remain separate work. --- - .../protocol/model/CreativeCatalog.java | 152 ++++++++++ + .../protocol/model/CreativeCatalog.java | 201 +++++++++++++ .../protocol/packet/InventoryPackets.java | 198 ++++++++----- .../protocol/rewriter/ItemRewriter.java | 31 +- .../storage/InventoryRequestTracker.java | 24 ++ - .../protocol/model/CreativeCatalogTest.java | 145 ++++++++++ + .../protocol/model/CreativeCatalogTest.java | 222 +++++++++++++++ .../protocol/packet/InventoryPacketsTest.java | 266 ++++++++++++++---- .../storage/InventoryRequestTrackerTest.java | 83 ++++++ - 7 files changed, 761 insertions(+), 138 deletions(-) + 7 files changed, 887 insertions(+), 138 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java new file mode 100644 -index 000000000..afece9faa +index 000000000..8dc359350 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java -@@ -0,0 +1,152 @@ +@@ -0,0 +1,201 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -79,16 +100,23 @@ index 000000000..afece9faa +import com.viaversion.nbt.tag.IntTag; +import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; +import com.viaversion.viaversion.api.minecraft.item.Item; ++import com.viaversion.viaversion.api.minecraft.item.StructuredItem; +import com.viaversion.viaversion.api.minecraft.item.data.PotionContents; +import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectMap; +import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectOpenHashMap; +import net.raphimc.viabedrock.api.model.NativeItemStackData; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.CreativeItemCategory; +import net.raphimc.viabedrock.protocol.rewriter.ItemDataRewriter; + +import java.util.ArrayList; +import java.util.Arrays; ++import java.util.EnumMap; ++import java.util.HashMap; ++import java.util.HashSet; +import java.util.List; ++import java.util.Map; +import java.util.Objects; ++import java.util.Set; + +/** The creative inventory advertised by one Bedrock server connection. */ +public final class CreativeCatalog { @@ -99,11 +127,28 @@ index 000000000..afece9faa + private final List groups; + private final List entries; + private final List searchEntries; ++ private final List> groupEntries; ++ private final Map> categoryEntries; + private final Int2ObjectMap byEntryId = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap byJavaItemId = new Int2ObjectOpenHashMap<>(); + + public CreativeCatalog(final List groups, final List entries, final ItemRegistry registry) { -+ this.groups = List.copyOf(groups); ++ final List registeredGroups = new ArrayList<>(groups.size()); ++ final Map> names = new HashMap<>(); ++ for (final Group group : groups) { ++ if (group.category() < 1 || group.category() > 5) { ++ continue; ++ } ++ if (group.name().isEmpty() || names.computeIfAbsent(group.category(), ignored -> new HashSet<>()).add(group.name())) { ++ registeredGroups.add(new Group(group.category(), group.name(), ++ group.name().isEmpty() ? StructuredItem.empty() : group.icon().copy())); ++ } ++ } ++ this.groups = List.copyOf(registeredGroups); ++ final List> members = new ArrayList<>(this.groups.size()); ++ for (int index = 0; index < this.groups.size(); index++) { ++ members.add(new ArrayList<>()); ++ } + final List marked = new ArrayList<>(entries.size()); + for (final Entry entry : entries) { + final Item javaItem = entry.javaItem().copy(); @@ -112,9 +157,16 @@ index 000000000..afece9faa + markedData.putInt(ENTRY_KEY, entry.netId()); + javaItem.dataContainer().set(StructuredDataKey.CUSTOM_DATA, markedData); + -+ final Entry markedEntry = new Entry(entry.netId(), entry.groupIndex(), entry.bedrockItem().copy(), ++ final int groupIndex = entry.groupIndex(); ++ // Wire indices address the registered group vector directly, even when earlier declarations were omitted. ++ final boolean grouped = groupIndex >= 0 && groupIndex < this.groups.size() ++ && registry.assignCreativeCategory(entry.bedrockItem().identifier(), this.groups.get(groupIndex).category()); ++ final Entry markedEntry = new Entry(entry.netId(), grouped ? groupIndex : -1, entry.bedrockItem().copy(), + javaItem, entry.bedrockOnly(), entry.canonical()); + marked.add(markedEntry); ++ if (grouped) { ++ members.get(groupIndex).add(markedEntry); ++ } + this.byEntryId.put(entry.netId(), markedEntry); + final Entry previous = this.byJavaItemId.get(javaItem.identifier()); + if (previous == null || markedEntry.canonical()) { @@ -122,6 +174,15 @@ index 000000000..afece9faa + } + } + this.entries = List.copyOf(marked); ++ this.groupEntries = members.stream().map(List::copyOf).toList(); ++ final Map> categories = new EnumMap<>(CreativeItemCategory.class); ++ for (int index = 0; index < this.groups.size(); index++) { ++ categories.computeIfAbsent(CreativeItemCategory.getByValue(this.groups.get(index).category()), ignored -> new ArrayList<>()) ++ .addAll(this.groupEntries.get(index)); ++ } ++ categories.replaceAll((category, values) -> List.copyOf(values)); ++ this.categoryEntries = Map.copyOf(categories); ++ // Every membership update runs before search reads the final shared definition categories. + this.searchEntries = this.entries.stream().filter(entry -> { + final int category = registry.creativeCategory(entry.bedrockItem().identifier()); + return category >= 1 && category <= 4; @@ -145,6 +206,15 @@ index 000000000..afece9faa + return this.searchEntries; + } + ++ public List groupEntries(final int groupIndex) { ++ return this.groupEntries.get(groupIndex); ++ } ++ ++ /** Native category group order followed by each group's member order, before availability evaluation. */ ++ public List categoryEntries(final CreativeItemCategory category) { ++ return this.categoryEntries.getOrDefault(category, List.of()); ++ } ++ + public boolean containsJavaItem(final int identifier) { + return this.byJavaItemId.containsKey(identifier); + } @@ -592,14 +662,15 @@ index d08578907..8544e1ab3 100644 diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java new file mode 100644 -index 000000000..5a946d77c +index 000000000..8851b4569 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java -@@ -0,0 +1,145 @@ +@@ -0,0 +1,222 @@ +package net.raphimc.viabedrock.protocol.model; + +import com.viaversion.nbt.tag.CompoundTag; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.CreativeItemCategory; +import com.viaversion.viaversion.api.data.FullMappings; +import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer; +import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; @@ -617,6 +688,8 @@ index 000000000..5a946d77c +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; ++import static org.junit.jupiter.api.Assertions.assertNotSame; ++import static org.junit.jupiter.api.Assertions.assertTrue; + +class CreativeCatalogTest { + @@ -730,6 +803,80 @@ index 000000000..5a946d77c + assertEquals(5, catalog.searchEntries().size()); + } + ++ @Test ++ void groupMembershipUpdatesSharedCategoriesBeforeSearchAndAcrossCatalogReplacement() throws ReflectiveOperationException { ++ final var registry = registry(100, 101); ++ final var groups = List.of(new CreativeCatalog.Group(1, "visible", javaItem(1)), ++ new CreativeCatalog.Group(5, "hidden", javaItem(2))); ++ final var catalog = new CreativeCatalog(groups, List.of( ++ creativeEntry(1, 0, 100, 0), creativeEntry(2, 0, 101, 0), ++ creativeEntry(3, 1, 100, 5), creativeEntry(4, 999, 100, 17)), registry); ++ assertEquals(5, registry.creativeCategory(100)); ++ assertEquals(1, registry.creativeCategory(101)); ++ assertEquals(List.of(2), catalog.searchEntries().stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertEquals(List.of(1, 2), catalog.categoryEntries(CreativeItemCategory.Construction).stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertEquals(List.of(3), catalog.categoryEntries(CreativeItemCategory.ItemCommandOnly).stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertEquals(-1, catalog.entries().getLast().groupIndex()); ++ final var ungrouped = new CreativeCatalog(List.of(), List.of(creativeEntry(5, -1, 100, 0)), registry); ++ assertTrue(ungrouped.searchEntries().isEmpty()); ++ assertEquals(5, registry.creativeCategory(100)); ++ final var equipment = new CreativeCatalog(List.of(new CreativeCatalog.Group(3, "changed", javaItem(1))), ++ List.of(creativeEntry(6, 0, 100, 0)), registry); ++ assertEquals(3, registry.creativeCategory(100)); ++ assertEquals(1, equipment.searchEntries().size()); ++ assertSame(equipment.entries().getFirst(), equipment.categoryEntries(CreativeItemCategory.Equipment).getFirst()); ++ assertEquals(List.of(2), catalog.searchEntries().stream().map(CreativeCatalog.Entry::netId).toList()); ++ } ++ ++ @Test ++ void nativeGroupRegistrationUsesCategoryNamesAndRegisteredVectorIndices() throws ReflectiveOperationException { ++ final var groups = List.of(new CreativeCatalog.Group(0, "ignored", javaItem(1)), ++ new CreativeCatalog.Group(1, "same", javaItem(1)), new CreativeCatalog.Group(1, "same", javaItem(2)), ++ new CreativeCatalog.Group(2, "same", javaItem(2)), new CreativeCatalog.Group(1, "", javaItem(3)), ++ new CreativeCatalog.Group(1, "", javaItem(4))); ++ final var catalog = new CreativeCatalog(groups, List.of( ++ creativeEntry(1, 0, 100, 0), creativeEntry(2, 1, 101, 0), ++ creativeEntry(3, 2, 102, 0), creativeEntry(4, 3, 103, 0), ++ creativeEntry(5, 4, 104, 0), creativeEntry(6, 5, 105, 0)), registry(100, 101, 102, 103, 104, 105)); ++ assertEquals(4, catalog.groups().size()); ++ assertEquals(List.of(0, 1, 2, 3, -1, -1), catalog.entries().stream().map(CreativeCatalog.Entry::groupIndex).toList()); ++ assertEquals(List.of(1, 3, 4), catalog.categoryEntries(CreativeItemCategory.Construction).stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertEquals(List.of(2), catalog.categoryEntries(CreativeItemCategory.Nature).stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertTrue(catalog.groups().get(2).icon().isEmpty()); ++ assertTrue(catalog.groups().get(3).icon().isEmpty()); ++ assertNotSame(catalog.groups().get(2), catalog.groups().get(3)); ++ } ++ ++ @Test ++ void categorySourcesFollowGroupOrderAndRetainDuplicateMemberIdentity() throws ReflectiveOperationException { ++ final var catalog = new CreativeCatalog(List.of(new CreativeCatalog.Group(1, "a", javaItem(1)), ++ new CreativeCatalog.Group(1, "b", javaItem(2))), List.of( ++ creativeEntry(11, 1, 100, 0), creativeEntry(12, 0, 101, 0), ++ creativeEntry(13, 1, 100, 0), creativeEntry(14, 0, 102, 0), ++ creativeEntry(15, 1, 103, 0)), registry(100, 101, 102, 103)); ++ final var source = catalog.categoryEntries(CreativeItemCategory.Construction); ++ assertEquals(List.of(12, 14, 11, 13, 15), source.stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertEquals(List.of(11, 13, 15), catalog.groupEntries(1).stream().map(CreativeCatalog.Entry::netId).toList()); ++ assertSame(catalog.entries().getFirst(), source.get(2)); ++ assertSame(catalog.entries().get(2), source.get(3)); ++ assertTrue(catalog.categoryEntries(CreativeItemCategory.Undefined).isEmpty()); ++ assertThrows(UnsupportedOperationException.class, source::clear); ++ assertThrows(UnsupportedOperationException.class, () -> catalog.groupEntries(1).clear()); ++ } ++ ++ private static CreativeCatalog.Entry creativeEntry(final int netId, final int group, final int identifier, ++ final int metadata) throws ReflectiveOperationException { ++ return new CreativeCatalog.Entry(netId, group, new BedrockItem(identifier, (short) metadata, (byte) 1), javaItem(1), false, false); ++ } ++ ++ private static ItemRegistry registry(final int... identifiers) { ++ final var entries = new ArrayList(); ++ for (final int identifier : identifiers) { ++ entries.add(new ItemEntry("test:item" + identifier, identifier, false, ItemVersion.Legacy, null)); ++ } ++ return new ItemRegistry(entries.toArray(ItemEntry[]::new)); ++ } ++ + private static Item javaItem(final int identifier) throws ReflectiveOperationException { + final StructuredDataContainer data = new StructuredDataContainer(); + final FullMappings mappings = (FullMappings) Proxy.newProxyInstance(FullMappings.class.getClassLoader(), diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch index bc4bc9a5..d8c1d5c4 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch @@ -1,4 +1,4 @@ -From cc990851f6ce237c8b950a180188f4c85bd4552f Mon Sep 17 00:00:00 2001 +From 16414073231dc89dd317c44d4e744912ffdc6029 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): resolve authored Bedrock server UI scenes @@ -540,6 +540,15 @@ same inputs. Builtin initialization uses the retained native block bootstrap and an empty network vector, establishing constructor defaults rather than a server packet baseline. Recipe availability and player screen integration remain separate work. + +Creative membership later updates the same definition category byte. +Keep that state in the connection registry so content replacement cannot +reset categories that receive no new assignment. Each admitted member +updates its shared definition in entry order, independently of whether +the byte changes. All variants then use the final category for search. +Complete native creative registry construction, content replacement, +group/member assignment, search rebuild and destruction cover 186 +additional controls with the recorded typed/CRT boundaries. --- Data Asset Sources.md | 33 + .../chunk/blockstate/BuiltinBlockNames.java | 46 + @@ -564,7 +573,7 @@ screen integration remain separate work. .../ComponentItemStackingProperties.java | 40 + .../viabedrock/protocol/model/ItemEntry.java | 10 + .../protocol/model/ItemNameContext.java | 23 + - .../protocol/model/ItemRegistry.java | 273 + + .../protocol/model/ItemRegistry.java | 284 + .../model/ItemStackingProperties.java | 36 + .../model/LegacyItemStackingProperties.java | 54 + .../protocol/packet/HudPackets.java | 51 +- @@ -600,7 +609,7 @@ screen integration remain separate work. .../protocol/packet/NativeHudPacketTest.java | 160 + .../packet/NpcDialoguePacketTest.java | 301 +- .../storage/NativeContainerUiStateTest.java | 85 + - 59 files changed, 17474 insertions(+), 346 deletions(-) + 59 files changed, 17485 insertions(+), 346 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BuiltinBlockNames.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemStacking.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeContainerActionSlots.java @@ -2019,10 +2028,10 @@ index 000000000..20d57645c +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java new file mode 100644 -index 000000000..4a15ae8e0 +index 000000000..c20f5563f --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -@@ -0,0 +1,273 @@ +@@ -0,0 +1,284 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2081,7 +2090,7 @@ index 000000000..4a15ae8e0 + this.entries = List.copyOf(definitions.values()); + this.writtenBookNames = this.factories().get("minecraft:written_book") == ItemVersion.Legacy; + this.componentDescriptions = this.resolveComponentDescriptions(); -+ this.creativeCategories = this.resolveCreativeCategories(); ++ this.creativeCategories = new HashMap<>(this.resolveCreativeCategories()); + } + + public BiMap ids() { @@ -2113,6 +2122,17 @@ index 000000000..4a15ae8e0 + return identifier != null ? this.creativeCategories.get(identifier) : 0; + } + ++ /** Creative membership rewrites the shared definition even when its category is already equal. */ ++ boolean assignCreativeCategory(final int id, final int category) { ++ final short narrowed = (short) id; ++ final String identifier = narrowed == 0 || narrowed == -1 ? null : this.ids.inverse().get((int) narrowed); ++ if (identifier == null) { ++ return false; ++ } ++ this.creativeCategories.put(identifier, category & 0xFF); ++ return true; ++ } ++ + private Map resolveCreativeCategories() { + final Map categories = new HashMap<>(); + for (final ItemEntry entry : this.registrations) { diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md index d9a7224b..33853e35 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md @@ -735,3 +735,11 @@ The complete four-project build passes 2,830 tests with 163 optional skips and n The packaged core matches all 1,949 category defaults, 563 typed-update comparisons and 906 search rebuilds. All 1,466 core classes and the factory table match the add-on and proxy copies; all 1,191 client classes match the tested add-on. The previous 7,728 eligibility and collection/history comparisons still pass against the packaged core. + +Creative content can later rewrite the shared category byte through admitted group membership. +These updates remain in the connection registry across later catalog replacement. +The final membership assignment controls search for all variants of that definition. +Complete native creative-registry construction, content replacement, group/member assignment, search rebuilding and destruction provide 186 additional controls. +Typed descriptor conversion, item copies/destruction, diagnostics and ordinary allocator/CRT calls remain supplied boundaries. +The packaged core matches these controls, and all four builds pass 2,833 tests with 163 optional skips. +Raw group and category sources are retained; native collection population, evaluators and player screen/input integration remain open. diff --git a/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch b/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch index 702808be..872d2376 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch @@ -1,4 +1,4 @@ -From afb75aa2fa0e1984205efbfee203f69023d898b1 Mon Sep 17 00:00:00 2001 +From 43c912a003126bf571ef3fa3ec57ca836f280985 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(bedrock): show server creative items in a Bedrock tab @@ -14,8 +14,8 @@ Retain the exact native catalog entry and complete item snapshot through Java cr .../world/item/BedrockCreativeTab.java | 55 ++++++ .../viafabricplus-bedrock/lang/en_us.json | 2 + .../viafabricplus-bedrock.mixins.json | 3 + - .../creative/BedrockCreativeCatalogTest.java | 160 ++++++++++++++++++ - 8 files changed, 462 insertions(+) + .../creative/BedrockCreativeCatalogTest.java | 170 ++++++++++++++++++ + 8 files changed, 472 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalog.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeInventoryScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/creative/MixinCreativeModeTab_ItemDisplayBuilder.java @@ -379,10 +379,10 @@ index 66baeef6..788e1b70 100644 "features.misc.MixinInventoryScreen", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java new file mode 100644 -index 00000000..896cd6a0 +index 00000000..3edfa4c2 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java -@@ -0,0 +1,160 @@ +@@ -0,0 +1,170 @@ +package com.viaversion.viafabricplus.bedrock.creative; + +import com.viaversion.nbt.tag.CompoundTag; @@ -397,6 +397,7 @@ index 00000000..896cd6a0 +import com.viaversion.viaversion.api.type.types.item.StructuredDataType; +import io.netty.buffer.Unpooled; +import java.lang.reflect.Proxy; ++import java.util.ArrayList; +import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.core.RegistryAccess; @@ -414,6 +415,7 @@ index 00000000..896cd6a0 +import net.raphimc.viabedrock.protocol.model.CreativeCatalog; +import net.raphimc.viabedrock.protocol.model.ItemEntry; +import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; +import net.raphimc.viabedrock.protocol.rewriter.ItemDataRewriter; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; @@ -478,7 +480,7 @@ index 00000000..896cd6a0 + bindMappings(data); + final var item = new StructuredItem(BuiltInRegistries.ITEM.getId(Items.PAPER), 1, data); + final var catalog = new CreativeCatalog(List.of(), List.of( -+ new CreativeCatalog.Entry(42, 0, nativeItem, item, true, false)), new ItemRegistry(new ItemEntry[0])); ++ new CreativeCatalog.Entry(42, 0, nativeItem, item, true, false)), registry(5001)); + final var result = BedrockCreativeCatalog.toNative(catalog.entries().getFirst().javaItem(), + RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY)); + assertEquals(1, result.getMaxStackSize()); @@ -508,13 +510,21 @@ index 00000000..896cd6a0 + final var icon = mappedItem(3); + final var catalog = new CreativeCatalog(List.of( + new CreativeCatalog.Group(2, "flower", icon)), List.of( -+ entry(41, 0, 3), entry(42, 0, 7), entry(43, 5, 9)), new ItemRegistry(new ItemEntry[0])); ++ entry(41, 0, 3), entry(42, 0, 7), entry(43, 5, 9)), registry(41, 42, 43)); + assertEquals(List.of(41, 42), BedrockCreativeCatalog.entriesForDisplay(catalog, 3).stream() + .map(CreativeCatalog.Entry::netId).toList()); + assertTrue(BedrockCreativeCatalog.entriesForDisplay(catalog, 7).isEmpty()); + assertEquals(43, BedrockCreativeCatalog.entriesForDisplay(catalog, 9).getFirst().netId()); + } + ++ private static ItemRegistry registry(final int... identifiers) { ++ final var entries = new ArrayList(); ++ for (final int identifier : identifiers) { ++ entries.add(new ItemEntry("test:item" + identifier, identifier, false, ItemVersion.Legacy, null)); ++ } ++ return new ItemRegistry(entries.toArray(ItemEntry[]::new)); ++ } ++ + private static CreativeCatalog.Entry entry(final int netId, final int group, + final int javaId) throws Exception { + return new CreativeCatalog.Entry(netId, group, From cfd3207cd93c46fed9fe3921ebdfffd44407900b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:22:45 +0200 Subject: [PATCH 04/12] feat(inventory): bind native catalog rows to recipe collections Native player rows select their group through the first matching creative entry, which can differ from the original source group for duplicate or wildcard metadata. Populate owned stack snapshots with retained catalog references before availability evaluation so later refreshes keep identity. Retain original group icons beside their Java presentation. Admit headers through native member counts and definitions, including zero-quantity icons, and compare native icon identity and quantity for expansion history. Keep availability evaluation explicit until its native implementation and player controller are integrated. Fold the changes into their owning patches and replay both affected stacks. All four projects build with 2,838 tests passed and 163 optional skips. The packaged core matches 300 native collection cases and three retained flows, covering 309 steps, and retains all 11,332 prior native comparisons. Full frame delivery, availability evaluation and player screen parity remain open. --- docs/native-player-inventory.md | 36 ++- ...erver-authoritative-bedrock-requests.patch | 8 +- ...edrock-recipes-and-place-ingredients.patch | 6 +- ...ain-bedrock-creative-catalog-entries.patch | 230 +++++++++++++++--- ...rate-authored-native-ui-presentation.patch | 59 ++++- ...rate-authored-native-ui-presentation.pr.md | 12 + ...rver-creative-items-in-a-bedrock-tab.patch | 6 +- 7 files changed, 299 insertions(+), 58 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 9ceac1a1..4b2b6860 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -125,7 +125,7 @@ It also creates a separate search collection. These recipe collections share con The category counts use the source category's group references; search uses the complete source list. This model construction evidence comes from disassembly. The player search source now uses definition categories from the connection registry. The connection catalog now retains the category membership sources. -Population into collection rows, recipe availability evaluation and mutation remain unfinished. +Progressive frame delivery, recipe availability evaluation and model publication remain unfinished. The player screen must retain the complete catalog and recipes that need a crafting table. A fabricated empty recipe catalog cannot establish support for the default inventory asset. @@ -179,6 +179,32 @@ Descriptor conversion, typed item backing, item copy/destruction, diagnostics an These controls cover replacement, duplicate names, anonymous groups, unsupported categories, direct registered indices and shared definition updates. The prior committed catalog admits two search rows after hidden membership; the corrected packaged core and native reference both admit zero. +### Native collection adapter + +The unreleased catalog retains each native group icon beside its Java presentation. +`ItemRewriter.creativeCollectionSource` binds a catalog snapshot to the shared `RecipeCollection`. +The caller must supply current availability evaluation; the adapter does not invent an unlock or craftability policy. + +Population copies each source stack and retains its first matching creative entry before availability evaluation. +Refreshes use that stored reference when they select a group. +The comparison uses native metadata wildcards, userdata, directional block state and restrictions. +Root quantity and network ID do not select the matching entry. +The source entry's advertised group cannot override this first match. + +A header needs at least two registered members and a defined native icon. +The icon may have zero quantity. +Expansion history compares native icon identity and quantity, independently of the Java fallback item. +Anonymous groups retain empty icons and produce individual rows. + +Complete original creative registration, item comparison, source assignment, bucket ordering, header construction and projection execute together in 300 controls. +Three retained-state flows also execute native header toggles and refreshes with zero-count icons. +Parsed descriptor backing, stable ordinary definitions, descriptor conversion, item copy/destruction, availability results, change notifications and allocator/CRT calls remain supplied interfaces. +The packaged comparison checks stored catalog references, collection order, eligible counts, visible rows and retained expansion. +Full player-controller lifetime, actual availability evaluation, frame delivery and visible screen behavior remain unverified. + +The zero-count controls supply an icon with an existing definition. +They do not establish raw descriptor construction at zero count. + ### Population into recipe collections Original provider `0x143439c20` visits search first, then construction, nature, equipment and items. @@ -190,7 +216,7 @@ Source assignment runs `0x1434463c0` and `0x143446410`. It stores the first matching creative entry index, with native metadata wildcard matching and independent stack counts. The collection then resolves that entry's group reference. This index is not the selected entry's packet group index. -The future adapter must retain this distinction when it supplies group identity to `RecipeCollection`. +The catalog adapter retains this distinction when it supplies group identity to `RecipeCollection`. The complete provider, source assignment and native item comparison execute 181 controls across 2,296 frames. Collection-name lookup, receiver lookup and registry-reference delivery remain supplied interfaces. @@ -207,10 +233,10 @@ Built-in registration executes with the retained versioned block bootstrap and a This establishes constructor defaults rather than a server packet baseline. Physical inventory rendering, unlock policy, recipe solving and controller input remain unverified. -All four projects build with 2,833 tests passed, 163 optional skips and no failures or errors. +All four projects build with 2,838 tests passed, 163 optional skips and no failures or errors. The packaged core matches 1,949 factory defaults, 563 typed-update comparisons and 906 search rebuilds. It also retains all 7,728 original eligibility and collection/history comparisons. -All 1,466 core classes and the factory table match the embedded add-on and proxy copies. +All 1,468 core classes and the factory table match the embedded add-on and proxy copies. All 1,191 client classes match the tested add-on. ## Shared recipe collections @@ -219,7 +245,7 @@ Release 0.3.56 includes this shared collection model. The shared `RecipeCollection` model implements the target collection ordering and retained header expansion. Cooking uses the same partitioner for its flat visible recipe prefix. -The player controller still needs native row population and its availability evaluator before it can bind these category sources. +The player controller still needs progressive frame delivery and its availability evaluator before it can publish these bound collections. Entries retain four stable evaluator buckets. A contiguous catalog group moves as one run: state zero wins, then state one; otherwise the first member selects state two or three. diff --git a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index d1451eac..f9512e41 100644 --- a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch +++ b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch @@ -1,4 +1,4 @@ -From 085330c805917fad91969bbff2b90a8cbecc4eb5 Mon Sep 17 00:00:00 2001 +From 45e6b088e9c2a57d6c21301a8f1e508c2253f874 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(inventory): translate server-authoritative Bedrock @@ -10037,7 +10037,7 @@ index 85cdb3dee..a98ad8137 100644 final BlockEntity javaBlockEntity = BlockEntityRewriter.toJava(wrapper.user(), chunkTracker.getBlockState(bedrockBlockEntity.position()), bedrockBlockEntity); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 234767d2c..771a85f95 100644 +index ee57e8c94..26915529d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -36,6 +36,7 @@ import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectMap; @@ -10423,7 +10423,7 @@ index 234767d2c..771a85f95 100644 } /** Match network-loaded stack queries while their connection registry remains current. */ -@@ -428,6 +556,18 @@ public class ItemRewriter extends StoredObject { +@@ -438,6 +566,18 @@ public class ItemRewriter extends StoredObject { return properties == null ? 0 : properties.maximum(); } @@ -10442,7 +10442,7 @@ index 234767d2c..771a85f95 100644 public Type itemType() { return this.itemType; } -@@ -452,10 +592,13 @@ public class ItemRewriter extends StoredObject { +@@ -462,10 +602,13 @@ public class ItemRewriter extends StoredObject { return this.newItemArrayType; } diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 94c171a8..f075e40a 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From 23f47fe2115f7de0bf38ab3d59cd6ac2ce4948c0 Mon Sep 17 00:00:00 2001 +From 3366790b262bc67de3947f57e19bf90aac50a657 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -3030,7 +3030,7 @@ index 0b8c600e9..4fef2c18d 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 771a85f95..9eb3fb9fe 100644 +index 26915529d..e3943150f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -40,9 +40,9 @@ import com.viaversion.viaversion.util.Key; @@ -3092,7 +3092,7 @@ index 771a85f95..9eb3fb9fe 100644 public ItemRegistry getItemRegistry() { return this.registry; } -@@ -572,6 +592,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -582,6 +602,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.itemType; } diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index 59326187..c59823fa 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From 554f9ab1225394eaa48033c5f3ab080d913dc477 Mon Sep 17 00:00:00 2001 +From 649fbe7cca0763bd94ac5839681583252756e511 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -62,24 +62,41 @@ CRT boundaries remain supplied. The frame population reference also executes 181 original controls across 2,296 frames, including first-match catalog identity; frame publication, evaluator and player UI integration remain separate work. + +Retain original native group icons beside their Java presentation. Bind +connection catalog snapshots to RecipeCollection with the actual native +identity comparator and an explicit availability evaluator. Resolve each +source stack through the first matching catalog entry before selecting its +group. Each populated row owns a stack snapshot and keeps that catalog +reference through availability refreshes. Require at least two native members and a defined icon, including +icons whose root quantity is zero; compare header identity and quantity +independently of the translated Java fallback. + +Complete original content registration, native item equality, first-match +source assignment, collection partition, header construction and projection +execute together in 300 controls and three retained toggle/refresh flows. +Typed packet/descriptor backing, item +copy/destruction, allocator/CRT and injected availability remain supplied +boundaries. Progressive frame delivery, actual availability evaluation and +full player controller/screen integration remain separate work. --- - .../protocol/model/CreativeCatalog.java | 201 +++++++++++++ - .../protocol/packet/InventoryPackets.java | 198 ++++++++----- - .../protocol/rewriter/ItemRewriter.java | 31 +- + .../protocol/model/CreativeCatalog.java | 266 +++++++++++++++ + .../protocol/packet/InventoryPackets.java | 198 ++++++++---- + .../protocol/rewriter/ItemRewriter.java | 33 +- .../storage/InventoryRequestTracker.java | 24 ++ - .../protocol/model/CreativeCatalogTest.java | 222 +++++++++++++++ - .../protocol/packet/InventoryPacketsTest.java | 266 ++++++++++++++---- - .../storage/InventoryRequestTrackerTest.java | 83 ++++++ - 7 files changed, 887 insertions(+), 138 deletions(-) + .../protocol/model/CreativeCatalogTest.java | 303 ++++++++++++++++++ + .../protocol/packet/InventoryPacketsTest.java | 266 ++++++++++++--- + .../storage/InventoryRequestTrackerTest.java | 83 +++++ + 7 files changed, 1037 insertions(+), 136 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java new file mode 100644 -index 000000000..8dc359350 +index 000000000..128248459 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java -@@ -0,0 +1,201 @@ +@@ -0,0 +1,266 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -105,6 +122,7 @@ index 000000000..8dc359350 +import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectMap; +import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectOpenHashMap; +import net.raphimc.viabedrock.api.model.NativeItemStackData; ++import net.raphimc.viabedrock.api.resourcepack.ui.RecipeCollection; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.CreativeItemCategory; +import net.raphimc.viabedrock.protocol.rewriter.ItemDataRewriter; + @@ -117,6 +135,8 @@ index 000000000..8dc359350 +import java.util.Map; +import java.util.Objects; +import java.util.Set; ++import java.util.function.BiPredicate; ++import java.util.function.ToIntFunction; + +/** The creative inventory advertised by one Bedrock server connection. */ +public final class CreativeCatalog { @@ -125,6 +145,7 @@ index 000000000..8dc359350 + private static final CreativeCatalog EMPTY = new CreativeCatalog(List.of(), List.of(), new ItemRegistry(new ItemEntry[0])); + + private final List groups; ++ private final ItemRegistry registry; + private final List entries; + private final List searchEntries; + private final List> groupEntries; @@ -133,6 +154,7 @@ index 000000000..8dc359350 + private final Int2ObjectMap byJavaItemId = new Int2ObjectOpenHashMap<>(); + + public CreativeCatalog(final List groups, final List entries, final ItemRegistry registry) { ++ this.registry = Objects.requireNonNull(registry); + final List registeredGroups = new ArrayList<>(groups.size()); + final Map> names = new HashMap<>(); + for (final Group group : groups) { @@ -141,6 +163,7 @@ index 000000000..8dc359350 + } + if (group.name().isEmpty() || names.computeIfAbsent(group.category(), ignored -> new HashSet<>()).add(group.name())) { + registeredGroups.add(new Group(group.category(), group.name(), ++ group.name().isEmpty() ? BedrockItem.empty() : group.bedrockIcon().copy(), + group.name().isEmpty() ? StructuredItem.empty() : group.icon().copy())); + } + } @@ -215,6 +238,12 @@ index 000000000..8dc359350 + return this.categoryEntries.getOrDefault(category, List.of()); + } + ++ /** Bind native row population to this catalog without substituting an availability policy. */ ++ public CollectionSource collectionSource(final ToIntFunction evaluation, ++ final BiPredicate identity) { ++ return new CollectionSource(evaluation, identity); ++ } ++ + public boolean containsJavaItem(final int identifier) { + return this.byJavaItemId.containsKey(identifier); + } @@ -273,7 +302,60 @@ index 000000000..8dc359350 + && Arrays.equals(left.customEffects(), right.customEffects()); + } + -+ public record Group(int category, String name, Item icon) { ++ public final class CollectionSource implements RecipeCollection.Source { ++ private final ToIntFunction evaluation; ++ private final BiPredicate identity; ++ ++ private CollectionSource(final ToIntFunction evaluation, ++ final BiPredicate identity) { ++ this.evaluation = Objects.requireNonNull(evaluation); ++ this.identity = Objects.requireNonNull(identity); ++ } ++ ++ /** Copy the source stack and retain the first matching catalog entry once, before evaluation. */ ++ public CollectionEntry populate(final BedrockItem item) { ++ final BedrockItem snapshot = Objects.requireNonNull(item).copy(); ++ for (final Entry entry : CreativeCatalog.this.entries) { ++ if (this.identity.test(entry.bedrockItem(), snapshot)) { ++ return new CollectionEntry(snapshot, entry); ++ } ++ } ++ return new CollectionEntry(snapshot, null); ++ } ++ ++ @Override ++ public int evaluate(final CollectionEntry entry) { ++ return this.evaluation.applyAsInt(entry.item()); ++ } ++ ++ @Override ++ public boolean valid(final CollectionEntry entry) { ++ return !entry.item().isEmpty() && CreativeCatalog.this.registry.definition(entry.item().identifier()) != null; ++ } ++ ++ @Override ++ public Group group(final CollectionEntry row) { ++ final Entry entry = row.catalogEntry(); ++ final int index = entry != null ? entry.groupIndex() : -1; ++ if (index < 0 || CreativeCatalog.this.groupEntries.get(index).size() < 2) { ++ return null; ++ } ++ final Group group = CreativeCatalog.this.groups.get(index); ++ // Native header admission tests the definition, not the icon quantity. ++ return CreativeCatalog.this.registry.definition(group.bedrockIcon().identifier()) != null ? group : null; ++ } ++ ++ @Override ++ public boolean sameHeader(final Group first, final Group second) { ++ return first.bedrockIcon().amount() == second.bedrockIcon().amount() ++ && this.identity.test(first.bedrockIcon(), second.bedrockIcon()); ++ } ++ } ++ ++ public record CollectionEntry(BedrockItem item, Entry catalogEntry) { ++ } ++ ++ public record Group(int category, String name, BedrockItem bedrockIcon, Item icon) { + } + + public record Entry(int netId, int groupIndex, BedrockItem bedrockItem, Item javaItem, @@ -282,7 +364,7 @@ index 000000000..8dc359350 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index a1a76a4d9..4cec1d2cb 100644 +index a1a76a4d9..1f9574928 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -92,14 +92,19 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.ComplexInventoryTransa @@ -325,7 +407,7 @@ index a1a76a4d9..4cec1d2cb 100644 + final int category = wrapper.read(Types.UNSIGNED_BYTE); + final String name = wrapper.read(BedrockTypes.STRING); + final BedrockItem icon = readCreativeItem(wrapper, itemRewriter); -+ creativeGroups.add(new CreativeCatalog.Group(category, name, itemRewriter.javaItem(icon))); ++ creativeGroups.add(new CreativeCatalog.Group(category, name, icon, itemRewriter.javaItem(icon))); } - itemRewriter.clearCreativeItems(); @@ -546,7 +628,7 @@ index a1a76a4d9..4cec1d2cb 100644 public static boolean handleLegacyOffhandClick(final UserConnection user, final Container container, diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index ef32aca46..28475b5d9 100644 +index d1e8b3c38..4892086ab 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -50,6 +50,7 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; @@ -557,7 +639,7 @@ index ef32aca46..28475b5d9 100644 import net.raphimc.viabedrock.protocol.model.ItemEntry; import net.raphimc.viabedrock.protocol.model.ItemRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; -@@ -78,8 +79,8 @@ import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; +@@ -78,9 +79,10 @@ import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; import java.util.HashMap; import java.util.HashSet; @@ -565,9 +647,11 @@ index ef32aca46..28475b5d9 100644 import java.util.Map; -import java.util.Objects; import java.util.Set; ++import java.util.function.ToIntFunction; import java.util.logging.Level; -@@ -113,7 +114,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex + public class ItemRewriter extends StoredObject implements NetworkItemStackContext { +@@ -113,7 +115,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Type optionalNewItemType; private final Type newItemArrayType; private final Type stackRequestResultType; @@ -576,7 +660,7 @@ index ef32aca46..28475b5d9 100644 static { // TODO: Add missing item nbt rewriters -@@ -450,29 +451,13 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -450,29 +452,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex "Translating Java items to Bedrock is not yet supported"); } @@ -595,8 +679,11 @@ index ef32aca46..28475b5d9 100644 - this.creativeItemStacks.put(javaItem.identifier(), item.copy()); - } - } -- } -- ++ public CreativeCatalog.CollectionSource creativeCollectionSource( ++ final ToIntFunction evaluation) { ++ return this.creativeCatalog.collectionSource(evaluation, this::matchesStackIdentity); + } + - public BedrockItem creativeItem(final Item item) { - final BedrockItem creativeItem = this.creativeItemStacks.get(item.identifier()); - if (creativeItem == null) { @@ -662,10 +749,10 @@ index d08578907..8544e1ab3 100644 diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java new file mode 100644 -index 000000000..8851b4569 +index 000000000..af059ce23 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java -@@ -0,0 +1,222 @@ +@@ -0,0 +1,303 @@ +package net.raphimc.viabedrock.protocol.model; + +import com.viaversion.nbt.tag.CompoundTag; @@ -682,6 +769,9 @@ index 000000000..8851b4569 +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; ++import java.util.Set; ++import net.raphimc.viabedrock.api.model.container.NativeItemStackMatcher; ++import net.raphimc.viabedrock.api.resourcepack.ui.RecipeCollection; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; @@ -790,7 +880,7 @@ index 000000000..8851b4569 + entries.add(new CreativeCatalog.Entry(entries.size() + 1, 999, + new BedrockItem(identifier, (short) 7, (byte) (identifier == 102 ? 0 : 64)), javaItem(1), true, false)); + } -+ final var catalog = new CreativeCatalog(List.of(new CreativeCatalog.Group(5, "", javaItem(1))), entries, registry); ++ final var catalog = new CreativeCatalog(List.of(new CreativeCatalog.Group(5, "", new BedrockItem(1), javaItem(1))), entries, registry); + assertEquals(List.of(1, 3, 4, 5, 7), catalog.searchEntries().stream().map(CreativeCatalog.Entry::netId).toList()); + assertEquals(9, catalog.entries().size()); + assertEquals(0, catalog.searchEntries().getLast().bedrockItem().amount()); @@ -806,8 +896,8 @@ index 000000000..8851b4569 + @Test + void groupMembershipUpdatesSharedCategoriesBeforeSearchAndAcrossCatalogReplacement() throws ReflectiveOperationException { + final var registry = registry(100, 101); -+ final var groups = List.of(new CreativeCatalog.Group(1, "visible", javaItem(1)), -+ new CreativeCatalog.Group(5, "hidden", javaItem(2))); ++ final var groups = List.of(new CreativeCatalog.Group(1, "visible", new BedrockItem(1), javaItem(1)), ++ new CreativeCatalog.Group(5, "hidden", new BedrockItem(2), javaItem(2))); + final var catalog = new CreativeCatalog(groups, List.of( + creativeEntry(1, 0, 100, 0), creativeEntry(2, 0, 101, 0), + creativeEntry(3, 1, 100, 5), creativeEntry(4, 999, 100, 17)), registry); @@ -820,7 +910,7 @@ index 000000000..8851b4569 + final var ungrouped = new CreativeCatalog(List.of(), List.of(creativeEntry(5, -1, 100, 0)), registry); + assertTrue(ungrouped.searchEntries().isEmpty()); + assertEquals(5, registry.creativeCategory(100)); -+ final var equipment = new CreativeCatalog(List.of(new CreativeCatalog.Group(3, "changed", javaItem(1))), ++ final var equipment = new CreativeCatalog(List.of(new CreativeCatalog.Group(3, "changed", new BedrockItem(1), javaItem(1))), + List.of(creativeEntry(6, 0, 100, 0)), registry); + assertEquals(3, registry.creativeCategory(100)); + assertEquals(1, equipment.searchEntries().size()); @@ -830,10 +920,10 @@ index 000000000..8851b4569 + + @Test + void nativeGroupRegistrationUsesCategoryNamesAndRegisteredVectorIndices() throws ReflectiveOperationException { -+ final var groups = List.of(new CreativeCatalog.Group(0, "ignored", javaItem(1)), -+ new CreativeCatalog.Group(1, "same", javaItem(1)), new CreativeCatalog.Group(1, "same", javaItem(2)), -+ new CreativeCatalog.Group(2, "same", javaItem(2)), new CreativeCatalog.Group(1, "", javaItem(3)), -+ new CreativeCatalog.Group(1, "", javaItem(4))); ++ final var groups = List.of(new CreativeCatalog.Group(0, "ignored", new BedrockItem(1), javaItem(1)), ++ new CreativeCatalog.Group(1, "same", new BedrockItem(1), javaItem(1)), new CreativeCatalog.Group(1, "same", new BedrockItem(2), javaItem(2)), ++ new CreativeCatalog.Group(2, "same", new BedrockItem(2), javaItem(2)), new CreativeCatalog.Group(1, "", new BedrockItem(3), javaItem(3)), ++ new CreativeCatalog.Group(1, "", new BedrockItem(4), javaItem(4))); + final var catalog = new CreativeCatalog(groups, List.of( + creativeEntry(1, 0, 100, 0), creativeEntry(2, 1, 101, 0), + creativeEntry(3, 2, 102, 0), creativeEntry(4, 3, 103, 0), @@ -849,8 +939,8 @@ index 000000000..8851b4569 + + @Test + void categorySourcesFollowGroupOrderAndRetainDuplicateMemberIdentity() throws ReflectiveOperationException { -+ final var catalog = new CreativeCatalog(List.of(new CreativeCatalog.Group(1, "a", javaItem(1)), -+ new CreativeCatalog.Group(1, "b", javaItem(2))), List.of( ++ final var catalog = new CreativeCatalog(List.of(new CreativeCatalog.Group(1, "a", new BedrockItem(1), javaItem(1)), ++ new CreativeCatalog.Group(1, "b", new BedrockItem(2), javaItem(2))), List.of( + creativeEntry(11, 1, 100, 0), creativeEntry(12, 0, 101, 0), + creativeEntry(13, 1, 100, 0), creativeEntry(14, 0, 102, 0), + creativeEntry(15, 1, 103, 0)), registry(100, 101, 102, 103)); @@ -864,6 +954,84 @@ index 000000000..8851b4569 + assertThrows(UnsupportedOperationException.class, () -> catalog.groupEntries(1).clear()); + } + ++ @Test ++ void collectionResolvesTheFirstWildcardCatalogEntryBeforeItsSourceGroup() throws ReflectiveOperationException { ++ final var catalog = new CreativeCatalog(List.of( ++ new CreativeCatalog.Group(1, "first", new BedrockItem(50), javaItem(1)), ++ new CreativeCatalog.Group(2, "second", new BedrockItem(51), javaItem(1))), List.of( ++ creativeEntry(1, 0, 100, Short.MAX_VALUE), creativeEntry(2, 1, 100, 7), ++ creativeEntry(3, 1, 101, 0)), registry(50, 51, 100, 101)); ++ final var source = catalog.categoryEntries(CreativeItemCategory.Nature).stream() ++ .map(CreativeCatalog.Entry::bedrockItem).toList(); ++ final var population = collectionSource(catalog); ++ final var snapshot = new RecipeCollection<>(population).refresh(source.stream().map(population::populate).toList(), true, false, true); ++ assertEquals(2, snapshot.rows().size()); ++ assertTrue(snapshot.rows().getFirst() instanceof RecipeCollection.Item); ++ assertSame(catalog.entries().getFirst(), ((RecipeCollection.Item) snapshot.rows().getFirst()).entry().catalogEntry()); ++ assertSame(catalog.groups().get(1), ((RecipeCollection.Header) snapshot.rows().get(1)).group()); ++ } ++ ++ @Test ++ void zeroQuantityNativeIconsRetainExpansionAcrossEquivalentGroups() throws ReflectiveOperationException { ++ final var icon = new BedrockItem(50, Short.MAX_VALUE, (byte) 0); ++ final var catalog = new CreativeCatalog(List.of( ++ new CreativeCatalog.Group(1, "first", icon, javaItem(1)), ++ new CreativeCatalog.Group(2, "second", icon, javaItem(1))), List.of( ++ creativeEntry(1, 0, 100, 0), creativeEntry(2, 0, 101, 0), ++ creativeEntry(3, 1, 102, 0), creativeEntry(4, 1, 103, 0)), registry(50, 100, 101, 102, 103)); ++ icon.setIdentifier(999); ++ final var population = collectionSource(catalog); ++ final var source = catalog.entries().stream().map(entry -> population.populate(entry.bedrockItem())).toList(); ++ final var collection = new RecipeCollection<>(population); ++ assertEquals(2, collection.refresh(source, true, false, true).rows().size()); ++ assertEquals(2, collection.toggleHeader(catalog.groups().getFirst()).memberCount()); ++ assertEquals(6, collection.refresh(source, true, false, false).rows().size()); ++ assertEquals(50, catalog.groups().getFirst().bedrockIcon().identifier()); ++ assertEquals(0, catalog.groups().getFirst().bedrockIcon().amount()); ++ } ++ ++ @Test ++ void expansionUsesNativeIconAndQuantityRatherThanJavaFallbackIdentity() throws ReflectiveOperationException { ++ final var catalog = new CreativeCatalog(List.of( ++ new CreativeCatalog.Group(1, "first", new BedrockItem(50), javaItem(1)), ++ new CreativeCatalog.Group(2, "other item", new BedrockItem(51), javaItem(1)), ++ new CreativeCatalog.Group(3, "other count", new BedrockItem(50, (short) 0, (byte) 2), javaItem(1))), List.of( ++ creativeEntry(1, 0, 100, 0), creativeEntry(2, 0, 101, 0), ++ creativeEntry(3, 1, 102, 0), creativeEntry(4, 1, 103, 0), ++ creativeEntry(5, 2, 104, 0), creativeEntry(6, 2, 105, 0)), registry(50, 51, 100, 101, 102, 103, 104, 105)); ++ final var population = collectionSource(catalog); ++ final var source = catalog.entries().stream().map(entry -> population.populate(entry.bedrockItem())).toList(); ++ final var collection = new RecipeCollection<>(population); ++ collection.refresh(source, true, false, true); ++ collection.toggleHeader(catalog.groups().getFirst()); ++ final var snapshot = collection.refresh(source, true, false, false); ++ assertEquals(5, snapshot.rows().size()); ++ final var headers = snapshot.rows().stream().filter(row -> row instanceof RecipeCollection.Header) ++ .map(row -> ((RecipeCollection.Header) row).expanded()).toList(); ++ assertEquals(List.of(true, false, false), headers); ++ } ++ ++ @Test ++ void populationCopiesTheStackAndKeepsItsFirstCatalogReferenceThroughRefresh() throws ReflectiveOperationException { ++ final var catalog = new CreativeCatalog(List.of(), List.of(creativeEntry(1, -1, 100, 7)), registry(100)); ++ final var population = collectionSource(catalog); ++ final var item = new BedrockItem(100, (short) 7, (byte) 2); ++ final var row = population.populate(item); ++ item.setData(8); ++ item.setAmount(64); ++ assertEquals(7, row.item().data()); ++ assertEquals(2, row.item().amount()); ++ assertSame(catalog.entries().getFirst(), row.catalogEntry()); ++ final var collection = new RecipeCollection<>(population); ++ assertSame(row, collection.refresh(List.of(row), false, true, true).ordered().getFirst()); ++ assertSame(row, collection.refresh(List.of(row), true, false, false).ordered().getFirst()); ++ } ++ ++ private static CreativeCatalog.CollectionSource collectionSource(final CreativeCatalog catalog) { ++ return catalog.collectionSource(ignored -> 1, ++ (first, second) -> NativeItemStackMatcher.matchesIdentity(first, second, ignored -> null, ignored -> Set.of())); ++ } ++ + private static CreativeCatalog.Entry creativeEntry(final int netId, final int group, final int identifier, + final int metadata) throws ReflectiveOperationException { + return new CreativeCatalog.Entry(netId, group, new BedrockItem(identifier, (short) metadata, (byte) 1), javaItem(1), false, false); diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch index d8c1d5c4..cf39951b 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch @@ -1,4 +1,4 @@ -From 16414073231dc89dd317c44d4e744912ffdc6029 Mon Sep 17 00:00:00 2001 +From ddf80fb86693f5664bb5c615fbca1fec4bde5562 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): resolve authored Bedrock server UI scenes @@ -549,6 +549,12 @@ the byte changes. All variants then use the final category for search. Complete native creative registry construction, content replacement, group/member assignment, search rebuild and destruction cover 186 additional controls with the recorded typed/CRT boundaries. + +Separate admitted stack queries from native value identity. Catalog icons +can retain a definition at zero quantity; identity still compares metadata, +userdata, block state and restrictions while query admission rejects an +empty quantity. Native catalog/collection controls execute the complete +original item comparator and confirm zero-quantity icon headers. --- Data Asset Sources.md | 33 + .../chunk/blockstate/BuiltinBlockNames.java | 46 + @@ -558,7 +564,7 @@ additional controls with the recorded typed/CRT boundaries. .../container/NativeContainerActionSlots.java | 75 + .../NativeContainerLegacyActions.java | 130 + .../container/NativeContainerTransfer.java | 73 + - .../container/NativeItemStackMatcher.java | 41 + + .../container/NativeItemStackMatcher.java | 48 + .../container/NativeStorageContainers.java | 49 + .../container/block/Generic3x3Container.java | 47 + .../container/block/HopperContainer.java | 47 + @@ -583,7 +589,7 @@ additional controls with the recorded typed/CRT boundaries. .../protocol/packet/NpcDialoguePackets.java | 417 +- .../protocol/rewriter/BlockStateRewriter.java | 7 + .../protocol/rewriter/ItemDataRewriter.java | 1 - - .../protocol/rewriter/ItemRewriter.java | 100 +- + .../protocol/rewriter/ItemRewriter.java | 110 +- .../protocol/storage/EntityTracker.java | 4 +- .../protocol/storage/GameSessionStorage.java | 10 + .../protocol/storage/InventoryTracker.java | 25 +- @@ -597,7 +603,7 @@ additional controls with the recorded typed/CRT boundaries. .../api/model/NativeItemStackingTest.java | 149 + .../NativeContainerLegacyActionsTest.java | 290 + .../NativeContainerTransferTest.java | 90 + - .../container/NativeItemStackMatcherTest.java | 179 + + .../container/NativeItemStackMatcherTest.java | 191 + .../resourcepack/ui/HudActionbarUiTest.java | 108 + .../api/resourcepack/ui/HudTitleUiTest.java | 176 + .../protocol/model/BuiltinItemNameTest.java | 159 + @@ -609,7 +615,7 @@ additional controls with the recorded typed/CRT boundaries. .../protocol/packet/NativeHudPacketTest.java | 160 + .../packet/NpcDialoguePacketTest.java | 301 +- .../storage/NativeContainerUiStateTest.java | 85 + - 59 files changed, 17485 insertions(+), 346 deletions(-) + 59 files changed, 17514 insertions(+), 346 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BuiltinBlockNames.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemStacking.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeContainerActionSlots.java @@ -1162,10 +1168,10 @@ index 000000000..6c6f6c7fe +} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcher.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcher.java new file mode 100644 -index 000000000..02f107b88 +index 000000000..6a74ce38d --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcher.java -@@ -0,0 +1,41 @@ +@@ -0,0 +1,48 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1184,7 +1190,14 @@ index 000000000..02f107b88 + + public static boolean matches(final BedrockItem query, final BedrockItem candidate, + final Function blocks, final Function> restrictions) { -+ if (query == null || candidate == null || query.isEmpty() || candidate.isEmpty() ++ return query != null && candidate != null && !query.isEmpty() && !candidate.isEmpty() ++ && matchesIdentity(query, candidate, blocks, restrictions); ++ } ++ ++ /** Compare defined stack values, including zero-quantity catalog icons, independently of root quantity. */ ++ public static boolean matchesIdentity(final BedrockItem query, final BedrockItem candidate, ++ final Function blocks, final Function> restrictions) { ++ if (query == null || candidate == null || query.identifier() == 0 || query.identifier() == -1 + || query.identifier() != candidate.identifier() + || query.data() != candidate.data() && query.data() != Short.MAX_VALUE && candidate.data() != Short.MAX_VALUE) { + return false; @@ -3902,7 +3915,7 @@ index f98365142..1baab2219 100644 && bedrockTag.get("customColor") instanceof IntTag color) { javaItem.dataContainer().set(StructuredDataKey.DYED_COLOR1_21_5, diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 353a67ede..234767d2c 100644 +index 353a67ede..ee57e8c94 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -18,7 +18,6 @@ @@ -4073,7 +4086,7 @@ index 353a67ede..234767d2c 100644 } } -@@ -372,6 +391,43 @@ public class ItemRewriter extends StoredObject { +@@ -372,6 +391,53 @@ public class ItemRewriter extends StoredObject { return this.componentItems; } @@ -4087,6 +4100,16 @@ index 353a67ede..234767d2c 100644 + return NativeItemStackMatcher.matches(query, candidate, blocks::blockState, blocks::restrictionBlocks); + } + ++ /** Catalog identity ignores root quantity; zero-quantity native group icons still have a definition. */ ++ public boolean matchesStackIdentity(final BedrockItem query, final BedrockItem candidate) { ++ if (query == null || candidate == null || this.registry.definition(query.identifier()) == null ++ || this.registry.definition(candidate.identifier()) == null) { ++ return false; ++ } ++ final var blocks = this.user().get(BlockStateRewriter.class); ++ return NativeItemStackMatcher.matchesIdentity(query, candidate, blocks::blockState, blocks::restrictionBlocks); ++ } ++ + public boolean canMerge(final BedrockItem source, final BedrockItem destination) { + if (destination == null || destination.isEmpty()) { + return false; @@ -16941,10 +16964,10 @@ index 000000000..53926519c +} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcherTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcherTest.java new file mode 100644 -index 000000000..eb0ed4345 +index 000000000..3882b97b4 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcherTest.java -@@ -0,0 +1,179 @@ +@@ -0,0 +1,191 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -16979,6 +17002,18 @@ index 000000000..eb0ed4345 +class NativeItemStackMatcherTest { + + @Test ++ void catalogIdentityRetainsZeroQuantityWithoutAdmittingAnEmptyQuery() { ++ final var icon = new BedrockItem(321, Short.MAX_VALUE, (byte) 0); ++ final var entry = new BedrockItem(321, (short) 3, (byte) 64); ++ assertTrue(NativeItemStackMatcher.matchesIdentity(icon, entry, ignored -> null, ignored -> Set.of())); ++ assertTrue(NativeItemStackMatcher.matchesIdentity(entry, icon, ignored -> null, ignored -> Set.of())); ++ assertFalse(NativeItemStackMatcher.matches(icon, entry, ignored -> null, ignored -> Set.of())); ++ icon.setTag(new CompoundTag()); ++ icon.tag().putInt("value", 1); ++ assertFalse(NativeItemStackMatcher.matchesIdentity(icon, entry, ignored -> null, ignored -> Set.of())); ++ } ++ ++ @Test + void queriesIgnoreRootQuantityAndNetworkIdButRetainNativeItemState() { + final Object block = new Object(); + final var blocks = Map.of(0, block, 1, new Object()); diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md index 33853e35..f8cbe298 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md @@ -743,3 +743,15 @@ Complete native creative-registry construction, content replacement, group/membe Typed descriptor conversion, item copies/destruction, diagnostics and ordinary allocator/CRT calls remain supplied boundaries. The packaged core matches these controls, and all four builds pass 2,833 tests with 163 optional skips. Raw group and category sources are retained; native collection population, evaluators and player screen/input integration remain open. + +Native value identity now permits defined zero-quantity catalog icons while ordinary stack-query admission still rejects an empty quantity. +Creative groups retain their native icon beside the Java presentation. +The catalog adapter supplies owned row snapshots, retained first-matching entry references, member-count admission and native icon/count header history to the shared collection model. +Complete original creative registration, item comparison, source assignment, collection ordering and projection execute together in 300 controls. +Three retained flows also execute native header toggles and refreshes. +Availability results, typed item backing, item copies/destruction, change notifications and allocator/CRT remain supplied interfaces. +Progressive frame delivery, actual availability evaluation and the full player controller/screen remain open. + +The final four-project build passes 2,838 tests with 163 optional skips and no failures or errors. +All 1,468 core classes and the factory table match the embedded add-on and proxy copies; all 1,191 client classes match the tested add-on. +The final packaged core retains all 11,332 prior comparisons and matches the 309 new catalog/collection case and flow steps. diff --git a/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch b/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch index 872d2376..bab0b34a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0010-show-server-creative-items-in-a-bedrock-tab.patch @@ -1,4 +1,4 @@ -From 43c912a003126bf571ef3fa3ec57ca836f280985 Mon Sep 17 00:00:00 2001 +From 20582a9d5464ccbb984f1b92c90557430556c3ca Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(bedrock): show server creative items in a Bedrock tab @@ -379,7 +379,7 @@ index 66baeef6..788e1b70 100644 "features.misc.MixinInventoryScreen", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java new file mode 100644 -index 00000000..3edfa4c2 +index 00000000..852bf812 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/creative/BedrockCreativeCatalogTest.java @@ -0,0 +1,170 @@ @@ -509,7 +509,7 @@ index 00000000..3edfa4c2 + void anchorsNativeGroupsToTheirIconsAndKeepsUnknownGroupsInTheirJavaLocation() throws Exception { + final var icon = mappedItem(3); + final var catalog = new CreativeCatalog(List.of( -+ new CreativeCatalog.Group(2, "flower", icon)), List.of( ++ new CreativeCatalog.Group(2, "flower", new BedrockItem(41), icon)), List.of( + entry(41, 0, 3), entry(42, 0, 7), entry(43, 5, 9)), registry(41, 42, 43)); + assertEquals(List.of(41, 42), BedrockCreativeCatalog.entriesForDisplay(catalog, 3).stream() + .map(CreativeCatalog.Entry::netId).toList()); From e909a2f9518a62499568f8cd27aff9682c4bdfba Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:39:26 +0200 Subject: [PATCH 05/12] feat(inventory): populate native player collections across frames Native collection construction reserves source rows before the progressive provider fills them. Share one budget across search and the four categories, retain group/member/destination cursors, and preserve the native guard for combined totals below eight rather than forcing a minimum budget. Report completion on the following tick and dirty only the active tab. Keep inactive dirty tabs and expanded headers, and separate left-tab mapping from population order. Require an explicit per-view availability evaluator; installed-screen delivery and opening ownership remain separate integration. Fold the model into the owning creative catalog patch and replay the stack. All four projects build with 2,841 tests passed and 163 optional skips. The packaged model matches 508 complete native controls across 9,438 frames and retains all 11,641 previous comparisons. Actual availability policy, full screen construction, input and pixel parity remain unverified. --- docs/native-player-inventory.md | 47 ++- ...ain-bedrock-creative-catalog-entries.patch | 336 +++++++++++++++++- 2 files changed, 357 insertions(+), 26 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 4b2b6860..8582655b 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -125,7 +125,7 @@ It also creates a separate search collection. These recipe collections share con The category counts use the source category's group references; search uses the complete source list. This model construction evidence comes from disassembly. The player search source now uses definition categories from the connection registry. The connection catalog now retains the category membership sources. -Progressive frame delivery, recipe availability evaluation and model publication remain unfinished. +Installed-screen tick delivery, recipe availability evaluation and model publication remain unfinished. The player screen must retain the complete catalog and recipes that need a crafting table. A fabricated empty recipe catalog cannot establish support for the default inventory asset. @@ -207,10 +207,26 @@ They do not establish raw descriptor construction at zero count. ### Population into recipe collections +The unreleased `CreativeInventoryModel` owns the five creative collection buffers and their retained refresh state. +`ItemRewriter.creativeInventoryModel` binds a connection catalog with explicit per-view availability evaluation. +The installed player screen still needs to own this model and deliver its ticks. + Original provider `0x143439c20` visits search first, then construction, nature, equipment and items. -Its budget derives from the total source count and persists across frames. -The executed guard consumes work only when that total exceeds seven; smaller controls retain their initial phase. -Full caller construction and publication remain outside this reference. +One budget covers all five sources and derives from their combined count divided by eight. +Group, member and destination cursors persist between ticks. +The model retains fixed-capacity empty rows until population replaces them with owned snapshots and catalog references. +The original collection constructor stores `-1` for an unpopulated source reference. + +The executed guard consumes work only when the combined count exceeds seven. +Smaller controls retain their initial phase; the Java model preserves that behavior. +This controlled result does not establish the behavior of a fully constructed screen with a small catalog. + +The call after the final write phase reports completion once. +Controller tick `0x144e168b0` then dirties the active left tab. +Dispatcher `0x144e1f430` refreshes that collection and retains the other dirty tabs. +An initial dirty tab can refresh before population completes. +Inventory tab six clears its bit without selecting a recipe collection. +Population order remains separate from the left-tab mapping. Source assignment runs `0x1434463c0` and `0x143446410`. It stores the first matching creative entry index, with native metadata wildcard matching and independent stack counts. @@ -218,10 +234,19 @@ The collection then resolves that entry's group reference. This index is not the selected entry's packet group index. The catalog adapter retains this distinction when it supplies group identity to `RecipeCollection`. -The complete provider, source assignment and native item comparison execute 181 controls across 2,296 frames. -Collection-name lookup, receiver lookup and registry-reference delivery remain supplied interfaces. -The reference uses controlled ordinary definitions, nullable userdata, block state and child state. -Native packet decoding, recipe evaluation, publication, rendering and physical input remain unverified. +Complete original creative setup, five collection constructors and destructors, progressive population, controller tick, refresh dispatch, ordering and projection execute together in 508 controls. +The packaged model is compared across all 9,438 frames, including row references, phases, cursors, dirty masks, visible rows and eligible counts. +The reference supplies parsed descriptors, stable ordinary definitions, descriptor conversion, item copies/destruction, labels, name/receiver lookup, availability functions, unrelated base-controller ticking and allocator/CRT calls. +Gesture, focus and delayed-action flags remain inactive during these complete tick controls. +Native allocation cleanup completes with no remaining allocations. + +Three additional weak-owner flows execute 68 native frames. +They establish that a missing strong owner prevents population while ordinary dirty-tab dispatch can still run. +The Java screen's opening and retirement ownership is not integrated by these controls. +Full screen construction, raw packet/item construction, actual availability policy, rendering and physical input remain unverified. + +The earlier provider-only reference retains 181 controls across 2,296 frames with independently supplied category/group/member sources. +Those broader input controls remain separate from the coherent creative-content and controller comparisons above. The complete original search rebuild and vector growth execute 906 controls. They cover all category bytes, absent definitions, duplicates, allocation boundaries and repeated replacement. @@ -233,10 +258,10 @@ Built-in registration executes with the retained versioned block bootstrap and a This establishes constructor defaults rather than a server packet baseline. Physical inventory rendering, unlock policy, recipe solving and controller input remain unverified. -All four projects build with 2,838 tests passed, 163 optional skips and no failures or errors. +All four projects build with 2,841 tests passed, 163 optional skips and no failures or errors. The packaged core matches 1,949 factory defaults, 563 typed-update comparisons and 906 search rebuilds. It also retains all 7,728 original eligibility and collection/history comparisons. -All 1,468 core classes and the factory table match the embedded add-on and proxy copies. +All 1,471 core classes and the factory table match the embedded add-on and proxy copies. All 1,191 client classes match the tested add-on. ## Shared recipe collections @@ -245,7 +270,7 @@ Release 0.3.56 includes this shared collection model. The shared `RecipeCollection` model implements the target collection ordering and retained header expansion. Cooking uses the same partitioner for its flat visible recipe prefix. -The player controller still needs progressive frame delivery and its availability evaluator before it can publish these bound collections. +The player controller still needs installed-screen tick delivery, opening ownership and its availability evaluator before it can publish these bound collections. Entries retain four stable evaluator buckets. A contiguous catalog group moves as one run: state zero wins, then state one; otherwise the first member selects state two or three. diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index c59823fa..6072fe37 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From 649fbe7cca0763bd94ac5839681583252756e511 Mon Sep 17 00:00:00 2001 +From ed6445070fa141da6d14d4778cfc1cbb4dd4d269 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -79,16 +79,38 @@ Typed packet/descriptor backing, item copy/destruction, allocator/CRT and injected availability remain supplied boundaries. Progressive frame delivery, actual availability evaluation and full player controller/screen integration remain separate work. + +Populate the five creative collections through one native frame budget, +visiting search, construction, nature, equipment and items in that order. +Retain group/member/destination cursors and initially invalid source rows. +The original provider leaves combined totals below eight pending; preserve +that guard rather than forcing a minimum budget. + +Completion reports once on the call after the final write phase and dirties +only the active tab. Keep inactive dirty tabs and header expansion, and use +the native left-tab mapping separately from population order. Bind the +model through ItemRewriter with explicit per-view availability evaluation. + +Complete original creative setup, five collection constructors/destructors, +progressive population, controller tick, weak-owner operations, dirty-tab +refresh dispatch and collection ordering/projection execute in 508 controls +across 9,438 frames. Three additional weak-owner flows remain reference-only. +Typed descriptor/definition backing, item conversion/copies/destruction, +label and receiver interfaces, injected availability, unrelated base tick +and allocator/CRT boundaries remain supplied. Actual availability policy, +full screen ownership and installed-screen frame delivery remain open. --- - .../protocol/model/CreativeCatalog.java | 266 +++++++++++++++ - .../protocol/packet/InventoryPackets.java | 198 ++++++++---- - .../protocol/rewriter/ItemRewriter.java | 33 +- + .../protocol/model/CreativeCatalog.java | 266 ++++++++++++ + .../model/CreativeInventoryModel.java | 193 +++++++++ + .../protocol/packet/InventoryPackets.java | 198 ++++++--- + .../protocol/rewriter/ItemRewriter.java | 39 +- .../storage/InventoryRequestTracker.java | 24 ++ - .../protocol/model/CreativeCatalogTest.java | 303 ++++++++++++++++++ - .../protocol/packet/InventoryPacketsTest.java | 266 ++++++++++++--- - .../storage/InventoryRequestTrackerTest.java | 83 +++++ - 7 files changed, 1037 insertions(+), 136 deletions(-) + .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++++++++ + .../protocol/packet/InventoryPacketsTest.java | 266 +++++++++--- + .../storage/InventoryRequestTrackerTest.java | 83 ++++ + 8 files changed, 1315 insertions(+), 136 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java + create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java @@ -363,6 +385,205 @@ index 000000000..128248459 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java +new file mode 100644 +index 000000000..c52a8436f +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java +@@ -0,0 +1,193 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model; ++ ++import net.raphimc.viabedrock.api.resourcepack.ui.RecipeCollection; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.CreativeItemCategory; ++ ++import java.util.ArrayList; ++import java.util.EnumMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.function.BiPredicate; ++import java.util.function.ToIntBiFunction; ++ ++/** One player model's progressive creative collections and retained active-tab refresh state. */ ++public final class CreativeInventoryModel { ++ ++ private static final List POPULATION_ORDER = List.of(View.SEARCH, View.CONSTRUCTION, View.NATURE, View.EQUIPMENT, View.ITEMS); ++ ++ private final Map collections = new EnumMap<>(View.class); ++ private final long totalEntries; ++ private int phase; ++ private int groupIndex; ++ private int memberIndex; ++ private int rowIndex; ++ private int dirty = 0x3F; ++ ++ public CreativeInventoryModel(final CreativeCatalog catalog, final ToIntBiFunction evaluation, ++ final BiPredicate identity) { ++ Objects.requireNonNull(catalog); ++ Objects.requireNonNull(evaluation); ++ Objects.requireNonNull(identity); ++ long total = 0; ++ for (final View view : POPULATION_ORDER) { ++ final List> groups = new ArrayList<>(); ++ if (view == View.SEARCH) { ++ groups.add(catalog.searchEntries()); ++ } else { ++ for (int index = 0; index < catalog.groups().size(); index++) { ++ if (catalog.groups().get(index).category() == view.category.getValue()) { ++ groups.add(catalog.groupEntries(index)); ++ } ++ } ++ } ++ final var source = catalog.collectionSource(item -> evaluation.applyAsInt(view, item), identity); ++ final var collection = new Collection(groups, source); ++ this.collections.put(view, collection); ++ total += collection.entries.size(); ++ } ++ this.totalEntries = total; ++ } ++ ++ /** Population completion dirties the active tab once, on the call after the final write phase. */ ++ public boolean tick(final int leftTab, final boolean grouping, final boolean filtering) { ++ final int bit = 1 << (leftTab - 1 & 31); ++ if (this.populate()) { ++ this.dirty |= bit; ++ } ++ if ((this.dirty & bit) <= 0) { ++ return false; ++ } ++ final View selected = View.fromLeftTab(leftTab); ++ if (selected != null) { ++ final Collection collection = this.collections.get(selected); ++ collection.view.refresh(collection.entries, grouping, filtering, false); ++ this.dirty &= ~bit; ++ } else if (leftTab == 6) { ++ this.dirty &= ~bit; ++ } ++ return true; ++ } ++ ++ private boolean populate() { ++ if (this.phase == POPULATION_ORDER.size()) { ++ this.phase++; ++ return true; ++ } ++ // The original provider leaves totals below eight pending, rather than forcing a minimum budget. ++ if (this.ready() || this.totalEntries < 8) { ++ return false; ++ } ++ int remaining = (int) (this.totalEntries >>> 3); ++ while (this.phase < POPULATION_ORDER.size() && remaining > 0) { ++ final Collection collection = this.collections.get(POPULATION_ORDER.get(this.phase)); ++ if (this.phase == 0) { ++ final List search = collection.groups.getFirst(); ++ final int count = Math.min(remaining, search.size() - this.rowIndex); ++ for (int index = 0; index < count; index++) { ++ collection.entries.set(this.rowIndex, collection.source.populate(search.get(this.rowIndex).bedrockItem())); ++ this.rowIndex++; ++ } ++ remaining -= count; ++ } else { ++ while (remaining > 0 && this.groupIndex < collection.groups.size()) { ++ final List group = collection.groups.get(this.groupIndex); ++ while (remaining > 0 && this.memberIndex < group.size()) { ++ collection.entries.set(this.rowIndex, collection.source.populate(group.get(this.memberIndex).bedrockItem())); ++ this.memberIndex++; ++ this.rowIndex++; ++ remaining--; ++ } ++ if (this.memberIndex < group.size()) { ++ break; ++ } ++ this.memberIndex = 0; ++ this.groupIndex++; ++ if (this.rowIndex == collection.entries.size()) { ++ break; ++ } ++ } ++ } ++ if (this.rowIndex == collection.entries.size()) { ++ this.rowIndex = 0; ++ this.groupIndex = 0; ++ this.phase++; ++ } ++ } ++ return false; ++ } ++ ++ public boolean ready() { ++ return this.phase > POPULATION_ORDER.size(); ++ } ++ ++ public void invalidate(final View view) { ++ this.dirty |= 1 << (view.leftTab - 1); ++ } ++ ++ public void invalidateAll() { ++ this.dirty |= 0x3F; ++ } ++ ++ public List entries(final View view) { ++ return List.copyOf(this.collections.get(view).entries); ++ } ++ ++ public RecipeCollection collection(final View view) { ++ return this.collections.get(view).view; ++ } ++ ++ public enum View { ++ CONSTRUCTION(1, CreativeItemCategory.Construction), ++ EQUIPMENT(2, CreativeItemCategory.Equipment), ++ ITEMS(3, CreativeItemCategory.Items), ++ NATURE(4, CreativeItemCategory.Nature), ++ SEARCH(5, null); ++ ++ private final int leftTab; ++ private final CreativeItemCategory category; ++ ++ View(final int leftTab, final CreativeItemCategory category) { ++ this.leftTab = leftTab; ++ this.category = category; ++ } ++ ++ public int leftTab() { ++ return this.leftTab; ++ } ++ ++ private static View fromLeftTab(final int tab) { ++ for (final View view : values()) { ++ if (view.leftTab == tab) { ++ return view; ++ } ++ } ++ return null; ++ } ++ } ++ ++ private static final class Collection { ++ private final List> groups; ++ private final CreativeCatalog.CollectionSource source; ++ private final List entries; ++ private final RecipeCollection view; ++ ++ private Collection(final List> groups, final CreativeCatalog.CollectionSource source) { ++ this.groups = List.copyOf(groups); ++ this.source = source; ++ this.entries = new ArrayList<>(); ++ for (final List group : groups) { ++ for (int index = 0; index < group.size(); index++) { ++ this.entries.add(new CreativeCatalog.CollectionEntry(BedrockItem.empty(), null)); ++ } ++ } ++ this.view = new RecipeCollection<>(source); ++ } ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java index a1a76a4d9..1f9574928 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -628,18 +849,19 @@ index a1a76a4d9..1f9574928 100644 public static boolean handleLegacyOffhandClick(final UserConnection user, final Container container, diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index d1e8b3c38..4892086ab 100644 +index d1e8b3c38..0176f672c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -50,6 +50,7 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; +@@ -50,6 +50,8 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; import net.raphimc.viabedrock.protocol.data.ProtocolConstants; import net.raphimc.viabedrock.protocol.data.generated.bedrock.CustomItemTags; import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.CreativeCatalog; ++import net.raphimc.viabedrock.protocol.model.CreativeInventoryModel; import net.raphimc.viabedrock.protocol.model.ItemEntry; import net.raphimc.viabedrock.protocol.model.ItemRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; -@@ -78,9 +79,10 @@ import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; +@@ -78,9 +80,11 @@ import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; import java.util.HashMap; import java.util.HashSet; @@ -648,10 +870,11 @@ index d1e8b3c38..4892086ab 100644 -import java.util.Objects; import java.util.Set; +import java.util.function.ToIntFunction; ++import java.util.function.ToIntBiFunction; import java.util.logging.Level; public class ItemRewriter extends StoredObject implements NetworkItemStackContext { -@@ -113,7 +115,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -113,7 +117,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Type optionalNewItemType; private final Type newItemArrayType; private final Type stackRequestResultType; @@ -660,7 +883,7 @@ index d1e8b3c38..4892086ab 100644 static { // TODO: Add missing item nbt rewriters -@@ -450,29 +452,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -450,29 +454,22 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex "Translating Java items to Bedrock is not yet supported"); } @@ -692,6 +915,10 @@ index d1e8b3c38..4892086ab 100644 - final BedrockItem result = creativeItem.copy(); - result.setAmount(item.amount()); - return result; ++ public CreativeInventoryModel creativeInventoryModel(final ToIntBiFunction evaluation) { ++ return new CreativeInventoryModel(this.creativeCatalog, evaluation, this::matchesStackIdentity); ++ } ++ + public void setCreativeCatalog(final List groups, + final List entries) { + this.creativeCatalog = new CreativeCatalog(groups, entries, this.registry); @@ -749,10 +976,10 @@ index d08578907..8544e1ab3 100644 diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java new file mode 100644 -index 000000000..af059ce23 +index 000000000..b560e6c03 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java -@@ -0,0 +1,303 @@ +@@ -0,0 +1,382 @@ +package net.raphimc.viabedrock.protocol.model; + +import com.viaversion.nbt.tag.CompoundTag; @@ -775,6 +1002,7 @@ index 000000000..af059ce23 +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; @@ -1027,6 +1255,84 @@ index 000000000..af059ce23 + assertSame(row, collection.refresh(List.of(row), true, false, false).ordered().getFirst()); + } + ++ @Test ++ void playerModelPopulatesSearchFirstAndRefreshesAfterTheCompletionPulse() throws ReflectiveOperationException { ++ final var model = playerModel(playerCatalog(16)); ++ assertTrue(model.tick(1, true, false)); ++ assertEquals(4, model.entries(CreativeInventoryModel.View.SEARCH).stream().filter(row -> !row.item().isEmpty()).count()); ++ assertTrue(model.collection(CreativeInventoryModel.View.CONSTRUCTION).snapshot().rows().isEmpty()); ++ for (int frame = 2; frame <= 8; frame++) { ++ assertFalse(model.tick(1, true, false)); ++ assertFalse(model.ready()); ++ } ++ assertTrue(model.collection(CreativeInventoryModel.View.CONSTRUCTION).snapshot().rows().isEmpty()); ++ assertTrue(model.tick(1, true, false)); ++ assertTrue(model.ready()); ++ assertEquals(1, model.collection(CreativeInventoryModel.View.CONSTRUCTION).snapshot().rows().size()); ++ assertFalse(model.tick(1, true, false)); ++ } ++ ++ @Test ++ void playerModelKeepsSmallNativeSourcesPendingAndSharesBudgetsAcrossCollections() throws ReflectiveOperationException { ++ final var small = playerModel(playerCatalog(3)); ++ for (int frame = 0; frame < 20; frame++) { ++ assertEquals(frame == 0, small.tick(5, true, false)); ++ } ++ assertFalse(small.ready()); ++ assertEquals(3, small.entries(CreativeInventoryModel.View.SEARCH).size()); ++ assertTrue(small.entries(CreativeInventoryModel.View.SEARCH).stream().allMatch(row -> row.item().isEmpty())); ++ final var model = playerModel(playerCatalog(11)); ++ for (int frame = 0; frame < 6; frame++) { ++ model.tick(6, true, false); ++ } ++ assertEquals(11, model.entries(CreativeInventoryModel.View.SEARCH).stream().filter(row -> !row.item().isEmpty()).count()); ++ assertEquals(1, model.entries(CreativeInventoryModel.View.CONSTRUCTION).stream().filter(row -> !row.item().isEmpty()).count()); ++ assertTrue(model.entries(CreativeInventoryModel.View.NATURE).stream().allMatch(row -> row.item().isEmpty())); ++ assertThrows(UnsupportedOperationException.class, () -> model.entries(CreativeInventoryModel.View.SEARCH).clear()); ++ } ++ ++ @Test ++ void playerModelRetainsInactiveDirtyTabsAndExpandedHeaders() throws ReflectiveOperationException { ++ final var catalog = playerCatalog(16); ++ final var model = playerModel(catalog); ++ for (int frame = 0; frame < 9; frame++) { ++ model.tick(6, true, false); ++ } ++ assertTrue(model.ready()); ++ assertTrue(model.collection(CreativeInventoryModel.View.CONSTRUCTION).snapshot().rows().isEmpty()); ++ assertTrue(model.tick(1, true, false)); ++ final var collection = model.collection(CreativeInventoryModel.View.CONSTRUCTION); ++ assertEquals(4, collection.toggleHeader(catalog.groups().getFirst()).memberCount()); ++ model.invalidate(CreativeInventoryModel.View.CONSTRUCTION); ++ assertTrue(model.tick(5, true, false)); ++ assertTrue(model.tick(1, true, false)); ++ assertEquals(5, collection.snapshot().rows().size()); ++ assertFalse(model.tick(1, true, false)); ++ model.invalidateAll(); ++ assertTrue(model.tick(1, true, false)); ++ assertEquals(5, collection.snapshot().rows().size()); ++ } ++ ++ private static CreativeInventoryModel playerModel(final CreativeCatalog catalog) { ++ return new CreativeInventoryModel(catalog, (view, item) -> 1, ++ (first, second) -> NativeItemStackMatcher.matchesIdentity(first, second, ignored -> null, ignored -> Set.of())); ++ } ++ ++ private static CreativeCatalog playerCatalog(final int count) throws ReflectiveOperationException { ++ final var groups = new ArrayList(); ++ for (int category = 1; category <= 4; category++) { ++ groups.add(new CreativeCatalog.Group(category, "g" + category, new BedrockItem(50), javaItem(1))); ++ } ++ final var entries = new ArrayList(); ++ final int[] identifiers = new int[count + 1]; ++ identifiers[0] = 50; ++ for (int index = 0; index < count; index++) { ++ identifiers[index + 1] = 100 + index; ++ entries.add(creativeEntry(index + 1, index % 4, 100 + index, index)); ++ } ++ return new CreativeCatalog(groups, entries, registry(identifiers)); ++ } ++ + private static CreativeCatalog.CollectionSource collectionSource(final CreativeCatalog catalog) { + return catalog.collectionSource(ignored -> 1, + (first, second) -> NativeItemStackMatcher.matchesIdentity(first, second, ignored -> null, ignored -> Set.of())); From 97645f4f470cc921b2b00f41918d1444f454f30c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:52:43 +0200 Subject: [PATCH 06/12] feat(inventory): share native player recipe search policies Player search strips formatting from both name sources and can require a word-prefix match in addition to the complete literal query. Generalize the cooking matcher in its owning UI patch so the policies share normalization and lazy name acquisition without changing cooking behavior. Record complete native search comparisons and their supplied interfaces. Recipe unlocking, ingredient counts, authoritative name producers and installed-screen integration remain separate work. --- docs/native-cooking-actions.md | 6 +- docs/native-player-inventory.md | 42 ++ ...are-native-ui-programs-and-resources.patch | 431 +++++++++++------- ...are-native-ui-programs-and-resources.pr.md | 23 +- 4 files changed, 331 insertions(+), 171 deletions(-) diff --git a/docs/native-cooking-actions.md b/docs/native-cooking-actions.md index aba5c6d6..6e9f00c0 100644 --- a/docs/native-cooking-actions.md +++ b/docs/native-cooking-actions.md @@ -884,10 +884,12 @@ The shared UI patch now exports the query matcher, target normalizer, and raw ed These APIs do not register a channel or replace the default cooking screen. The production menu still publishes an empty query until name producers, transport, and widget integration are complete. -`CookingRecipeSearch` normalizes the query once and requests primary names lazily. +`NativeRecipeSearch` in `COOKING` mode normalizes the query once and requests primary names lazily. A primary match skips the secondary-label producer. An empty query skips both producers. The secondary branch removes native formatting codes before normalization. -The matcher does not trim spaces, split words, or substitute Java preview names. +This mode does not trim spaces, split words, or substitute Java preview names. +The same API now provides explicit player substring and word-prefix policies. +See [the player search reference](native-player-inventory.md#player-search-policies) for their rules and evidence. `CookingSearchText` retains the latest pending raw string until a ready tick. Equal edits still apply. An empty string remains a pending edit until that tick. diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 8582655b..80a16e90 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -449,3 +449,45 @@ The installed-screen bridge has sixteen packaged lifecycle and codec regressions The full player-inventory controller and physical input delivery remain unfinished. Direct joins, creative item actions, reconfiguration and recovery under packet delay still need live verification. All five implementation areas and the four original bug reports remain open. + +## Player search policies + +`NativeRecipeSearch` shares normalized query bytes between cooking and player search. +Callers select an explicit mode. Cooking preserves primary-name formatting. +Both player modes remove native formatting from primary names and secondary display labels. +A primary match skips the secondary producer. An empty normalized query skips both producers. +Search does not remove query formatting, trim spaces or substitute Java preview names. + +`PLAYER_SUBSTRING` requires the complete normalized query to occur in either normalized name. +`PLAYER_WORD_PREFIX` requires that literal match plus a prefix match between any query token and any name token. +It does not require every query token to match. Only ASCII space separates tokens. +Leading and repeated spaces produce empty tokens. A trailing space does not produce a final empty token. +Tabs, newlines and Unicode spaces remain inside tokens after target normalization. + +For example, `on` matches `stone` only in substring mode. +It matches `stone onyx` in both player modes. +The query `en sto` matches `wooden stone bricks` in word-prefix mode because `sto` prefixes `stone`. +The complete query still occurs literally in that name. The query `wood brick` fails that literal check. + +The pinned client is Bedrock 1.26.51.1, build 51061372, protocol 2193. +Complete original functions `0x143438780` and `0x143438da0` supply 1,136 player-search cases and 1,220 normalized word controls. +Comparisons include malformed UTF-8, embedded NULs, formatting, Unicode normalization, repeated spaces and lazy name acquisition. +Native formatting, normalization, empty-item checks, matching, vector allocation and cleanup execute. +The harness supplies name and unlock providers, raw/fallback game types, stable item definitions and standard byte/stream interfaces. +Thirty invalid-stack controls confirm that an empty query returns state one before native validity checks. +Eighteen secondary-label controls confirm the raw/fallback creative flag delivered to the name provider. +These additional controls describe native dependencies; the shared matcher does not own item admission or game mode. + +The shared API returns a text match. The full player availability evaluator remains unfinished. +Native function `0x14341c4c0` installs separate search and category evaluators after collection construction. +Search combines text matching, recipe unlocking and craftability. Category views also depend on filtering and an actor ability predicate. +The ability predicate is not established as creative mode. +The player screen still needs authoritative name producers, unlock/query inputs, native ingredient counting and installed-screen callbacks. +The four original reports and all five goal areas remain unresolved. + +The packaged core matches all 2,660 search comparisons, including 304 retained cooking compositions. +Four deliberate regressions fail: omitting the literal check, accepting trailing empty tokens, preserving player-name formatting and requiring every query token. +The retained progressive player model still matches 508 controls across 9,438 frames. +All four projects build with 2,842 passing tests, 163 optional skips and no failures or errors. +Checkstyle passes after full replay. All 1,472 core classes match the embedded add-on and proxy copies. +All 1,191 client classes match the tested add-on. Captures and verifiers remain private. diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 152ef86d..8302da12 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From 2e70057804a0a5a7e2cefaf7d0b8e96ce310804b Mon Sep 17 00:00:00 2001 +From 07d653a4e82cf7b19d51e4d58ae91629fd04eca7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): share bounded native UI programs and resource @@ -239,6 +239,22 @@ Original getter 1434461c0 supplies 3,584 cases with fresh evaluator results after construction. Three regressions retain snapshot identity, current availability and header/member distinctions. Full catalog acquisition, controller ownership, action admission, input and pixels remain unfinished. + +Generalize cooking search into NativeRecipeSearch with explicit cooking and +player policies. Player names remove formatting on both branches. Its word +policy requires the full literal query plus any query-token/name-token prefix +match, using ASCII spaces and getline's omitted trailing empty token. Keep +Unicode normalization, raw query bytes and lazy name acquisition shared. +Do not reuse Java preview labels or assume that an ability predicate means +creative mode. Recipe unlock/count inputs and installed player-screen search +remain separate integration work. + +Complete original player search 143438780 and word matcher 143438da0 match +1,136 search cases and 1,220 normalized word controls in the pinned +1.26.51.1 build51061372 executable. These controls supply name, unlock, +game-type and standard-stream interfaces. Native formatting, normalization, +validity, matching, vector allocation and cleanup execute. They do not prove +production name acquisition, the full availability evaluator or pixels. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -261,7 +277,6 @@ controller ownership, action admission, input and pixels remain unfinished. .../api/resourcepack/ui/ContainerUi.java | 511 ++++++ .../resourcepack/ui/CookingNavigation.java | 83 + .../resourcepack/ui/CookingRecipeOrder.java | 206 +++ - .../resourcepack/ui/CookingRecipeSearch.java | 74 + .../resourcepack/ui/CookingSearchText.java | 45 + .../api/resourcepack/ui/CookingUi.java | 433 +++++ .../api/resourcepack/ui/FormBindings.java | 290 ++++ @@ -270,6 +285,7 @@ controller ownership, action admission, input and pixels remain unfinished. .../api/resourcepack/ui/HudRootUi.java | 35 + .../api/resourcepack/ui/HudTitleUi.java | 28 + .../api/resourcepack/ui/JsonUiProgram.java | 1498 +++++++++++++++++ + .../resourcepack/ui/NativeRecipeSearch.java | 120 ++ .../ui/NativeSearchNormalization.java | 189 +++ .../api/resourcepack/ui/NpcDialogueUi.java | 316 ++++ .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ @@ -304,12 +320,12 @@ controller ownership, action admission, input and pixels remain unfinished. .../ui/CookingNavigationTest.java | 87 + .../resourcepack/ui/CookingProgressTest.java | 47 + .../ui/CookingRecipeOrderTest.java | 82 + - .../ui/CookingRecipeSearchTest.java | 75 + .../ui/CookingSearchTextTest.java | 90 + .../api/resourcepack/ui/CookingUiTest.java | 646 +++++++ .../ui/FormCollectionActivationTest.java | 97 ++ .../api/resourcepack/ui/HudRootUiTest.java | 90 + .../resourcepack/ui/JsonUiProgramTest.java | 556 ++++++ + .../ui/NativeRecipeSearchTest.java | 110 ++ .../ui/NativeSearchNormalizationTest.java | 46 + .../resourcepack/ui/NpcDialogueUiTest.java | 108 ++ .../resourcepack/ui/NpcEditorStateTest.java | 209 +++ @@ -322,7 +338,7 @@ controller ownership, action admission, input and pixels remain unfinished. .../api/resourcepack/ui/UiFontsTest.java | 192 +++ .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 82 files changed, 12851 insertions(+), 2 deletions(-) + 82 files changed, 12932 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -342,7 +358,6 @@ controller ownership, action admission, input and pixels remain unfinished. create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingNavigation.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeOrder.java - create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearch.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingSearchText.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/FormBindings.java @@ -351,6 +366,7 @@ controller ownership, action admission, input and pixels remain unfinished. create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/HudRootUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/HudTitleUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearch.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeSearchNormalization.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NpcDialogueUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NpcEditorBindings.java @@ -383,12 +399,12 @@ controller ownership, action admission, input and pixels remain unfinished. create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingNavigationTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingProgressTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeOrderTest.java - create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearchTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingSearchTextTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/FormCollectionActivationTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/HudRootUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearchTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeSearchNormalizationTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NpcDialogueUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NpcEditorStateTest.java @@ -2706,86 +2722,6 @@ index 000000000..5d8e463b5 + private record RegistryEntry(T entry, byte[] key) { } + +} -diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearch.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearch.java -new file mode 100644 -index 000000000..94c1b0728 ---- /dev/null -+++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearch.java -@@ -0,0 +1,74 @@ -+/* -+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock -+ * Copyright (C) 2026 RK_01/RaphiMC and contributors -+ * SPDX-License-Identifier: GPL-3.0-or-later -+ */ -+package net.raphimc.viabedrock.api.resourcepack.ui; -+ -+import java.io.ByteArrayOutputStream; -+import java.nio.charset.StandardCharsets; -+import java.util.Arrays; -+import java.util.Objects; -+import java.util.function.Supplier; -+ -+/** Searches native item names first, then formatting-free display labels on a miss. */ -+public final class CookingRecipeSearch { -+ -+ private final byte[] query; -+ -+ public CookingRecipeSearch(final String query) { -+ this(query.getBytes(StandardCharsets.UTF_8)); -+ } -+ -+ public CookingRecipeSearch(final byte[] query) { -+ this.query = NativeSearchNormalization.normalize(Objects.requireNonNull(query)); -+ } -+ -+ /** Name producers remain lazy. Empty queries need neither producer; a primary match skips the label. */ -+ public boolean matches(final Supplier primaryName, final Supplier secondaryLabel) { -+ return this.query.length == 0 || this.contains(NativeSearchNormalization.normalize(primaryName.get())) -+ || this.contains(NativeSearchNormalization.normalize(stripFormatting(secondaryLabel.get()))); -+ } -+ -+ /** Native search drops incomplete section signs and recognizes only digits and lowercase a through w. */ -+ public static byte[] stripFormatting(final byte[] input) { -+ final var output = new ByteArrayOutputStream(input.length); -+ int state = 0; -+ for (final byte value : input) { -+ final int next = Byte.toUnsignedInt(value); -+ if (state == 0) { -+ if (next == 0xC2) { -+ state = 1; -+ } else { -+ output.write(next); -+ } -+ } else if (state == 1) { -+ if (next == 0xA7) { -+ state = 2; -+ } else { -+ output.write(0xC2); -+ output.write(next); -+ state = 0; -+ } -+ } else { -+ if (!(next >= '0' && next <= '9' || next >= 'a' && next <= 'w')) { -+ output.write(0xC2); -+ output.write(0xA7); -+ output.write(next); -+ } -+ state = 0; -+ } -+ } -+ return output.toByteArray(); -+ } -+ -+ private boolean contains(final byte[] text) { -+ for (int offset = 0; offset <= text.length - this.query.length; offset++) { -+ if (Arrays.equals(this.query, 0, this.query.length, text, offset, offset + this.query.length)) { -+ return true; -+ } -+ } -+ return false; -+ } -+ -+} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingSearchText.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingSearchText.java new file mode 100644 index 000000000..00df0b136 @@ -5315,6 +5251,132 @@ index 000000000..33a6a77f3 + private record ManualGrid(String collection, int columns) { } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearch.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearch.java +new file mode 100644 +index 000000000..2728fcac3 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearch.java +@@ -0,0 +1,120 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.resourcepack.ui; ++ ++import java.io.ByteArrayOutputStream; ++import java.nio.charset.StandardCharsets; ++import java.util.Arrays; ++import java.util.Objects; ++import java.util.function.Supplier; ++ ++/** Searches native item names and display labels with the owning screen's matching policy. */ ++public final class NativeRecipeSearch { ++ ++ private final byte[] query; ++ private final Mode mode; ++ ++ public NativeRecipeSearch(final String query, final Mode mode) { ++ this(query.getBytes(StandardCharsets.UTF_8), mode); ++ } ++ ++ public NativeRecipeSearch(final byte[] query, final Mode mode) { ++ this.query = NativeSearchNormalization.normalize(Objects.requireNonNull(query)); ++ this.mode = Objects.requireNonNull(mode); ++ } ++ ++ /** Name producers remain lazy. Empty queries need neither producer; a primary match skips the label. */ ++ public boolean matches(final Supplier primaryName, final Supplier secondaryLabel) { ++ return this.query.length == 0 ++ || this.matchesName(this.mode == Mode.COOKING ? primaryName.get() : stripFormatting(primaryName.get())) ++ || this.matchesName(stripFormatting(secondaryLabel.get())); ++ } ++ ++ /** Native search drops incomplete section signs and recognizes only digits and lowercase a through w. */ ++ public static byte[] stripFormatting(final byte[] input) { ++ final var output = new ByteArrayOutputStream(input.length); ++ int state = 0; ++ for (final byte value : input) { ++ final int next = Byte.toUnsignedInt(value); ++ if (state == 0) { ++ if (next == 0xC2) { ++ state = 1; ++ } else { ++ output.write(next); ++ } ++ } else if (state == 1) { ++ if (next == 0xA7) { ++ state = 2; ++ } else { ++ output.write(0xC2); ++ output.write(next); ++ state = 0; ++ } ++ } else { ++ if (!(next >= '0' && next <= '9' || next >= 'a' && next <= 'w')) { ++ output.write(0xC2); ++ output.write(0xA7); ++ output.write(next); ++ } ++ state = 0; ++ } ++ } ++ return output.toByteArray(); ++ } ++ ++ private boolean matchesName(final byte[] name) { ++ final byte[] text = NativeSearchNormalization.normalize(name); ++ if (!this.contains(text)) { ++ return false; ++ } ++ if (this.mode != Mode.PLAYER_WORD_PREFIX) { ++ return true; ++ } ++ // The native stream loop accepts the first matching query token. Only ASCII space separates tokens. ++ // getline does not produce a final empty token after a trailing delimiter. ++ for (int queryStart = 0; queryStart < this.query.length; ) { ++ final int queryEnd = tokenEnd(this.query, queryStart); ++ final int length = queryEnd - queryStart; ++ for (int textStart = 0; textStart < text.length; ) { ++ final int textEnd = tokenEnd(text, textStart); ++ if (length <= textEnd - textStart ++ && Arrays.equals(this.query, queryStart, queryEnd, text, textStart, textStart + length)) { ++ return true; ++ } ++ textStart = textEnd + 1; ++ } ++ queryStart = queryEnd + 1; ++ } ++ return false; ++ } ++ ++ private static int tokenEnd(final byte[] text, final int start) { ++ int end = start; ++ while (end < text.length && text[end] != ' ') { ++ end++; ++ } ++ return end; ++ } ++ ++ private boolean contains(final byte[] text) { ++ for (int offset = 0; offset <= text.length - this.query.length; offset++) { ++ if (Arrays.equals(this.query, 0, this.query.length, text, offset, offset + this.query.length)) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public enum Mode { ++ /** Cooking preserves primary-name formatting and uses a literal substring. */ ++ COOKING, ++ /** Player search removes formatting from both name sources and uses a literal substring. */ ++ PLAYER_SUBSTRING, ++ /** Player search also requires any space-delimited query token to prefix a name token. */ ++ PLAYER_WORD_PREFIX ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeSearchNormalization.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeSearchNormalization.java new file mode 100644 index 000000000..67ec101a4 @@ -10971,87 +11033,6 @@ index 000000000..5c5140c88 + private record KeyedEntry(int identity, String key) { } + +} -diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearchTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearchTest.java -new file mode 100644 -index 000000000..eb308ff8c ---- /dev/null -+++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeSearchTest.java -@@ -0,0 +1,75 @@ -+/* -+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock -+ * Copyright (C) 2026 RK_01/RaphiMC and contributors -+ * SPDX-License-Identifier: GPL-3.0-or-later -+ */ -+package net.raphimc.viabedrock.api.resourcepack.ui; -+ -+import com.viaversion.viaversion.libs.gson.JsonParser; -+import java.nio.charset.StandardCharsets; -+import java.nio.file.Files; -+import java.nio.file.Path; -+import java.util.concurrent.atomic.AtomicInteger; -+import org.junit.jupiter.api.Test; -+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -+import static org.junit.jupiter.api.Assertions.*; -+ -+class CookingRecipeSearchTest { -+ -+ @Test -+ void normalizesTheQueryAndKeepsNameProductionLazy() { -+ assertTrue(new CookingRecipeSearch("").matches(() -> fail("Unexpected primary lookup"), () -> fail("Unexpected label lookup"))); -+ assertTrue(new CookingRecipeSearch("STRASSE").matches(() -> bytes("Straße"), () -> fail("Unexpected label lookup"))); -+ assertTrue(new CookingRecipeSearch("CAFÉ").matches(() -> bytes("other"), () -> bytes("§aCafé"))); -+ assertFalse(new CookingRecipeSearch("§acafé").matches(() -> bytes("other"), () -> bytes("§aCafé"))); -+ assertFalse(new CookingRecipeSearch(" beef ").matches(() -> bytes("Beef"), () -> bytes(""))); -+ assertFalse(new CookingRecipeSearch("beef raw").matches(() -> bytes("Raw Beef"), () -> bytes(""))); -+ assertFalse(new CookingRecipeSearch("be" + Character.toString(0x200B) + "ef").matches(() -> bytes("Beef"), () -> bytes(""))); -+ final byte[] query = bytes("BEEF"); -+ final var search = new CookingRecipeSearch(query); -+ query[0] = 'x'; -+ assertTrue(search.matches(() -> bytes("Beef"), () -> fail("Unexpected label lookup"))); -+ } -+ -+ @Test -+ void strippingUsesTheNativeByteAutomatonIncludingIncompleteAndUnknownCodes() { -+ assertArrayEquals(bytes("A§ZB§xC"), CookingRecipeSearch.stripFormatting(bytes("§aA§ZB§xC§w"))); -+ assertArrayEquals(bytes("A"), CookingRecipeSearch.stripFormatting(bytes("A§"))); -+ assertArrayEquals(bytes("A"), CookingRecipeSearch.stripFormatting(new byte[]{'A', (byte) 0xC2})); -+ assertArrayEquals(new byte[]{(byte) 0xC2, (byte) 0xC2, (byte) 0xA7, 'a'}, -+ CookingRecipeSearch.stripFormatting(new byte[]{(byte) 0xC2, (byte) 0xC2, (byte) 0xA7, 'a'})); -+ } -+ -+ @Test -+ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_COOKING_SEARCH", matches = ".+") -+ void matchesTheCapturedPredicateCompositionAndLazyNameBoundaries() throws Exception { -+ final var data = JsonParser.parseString(Files.readString(Path.of(System.getenv("STACKANVIL_NATIVE_COOKING_SEARCH")))).getAsJsonObject(); -+ int cases = 0; -+ for (final var element : data.getAsJsonArray("rows")) { -+ final var row = element.getAsJsonObject(); -+ final var primary = new AtomicInteger(); -+ final var secondary = new AtomicInteger(); -+ final var search = new CookingRecipeSearch(row.get("query").getAsString()); -+ final boolean admitted = row.get("filtering").getAsBoolean() || row.get("eligible").getAsBoolean(); -+ final boolean match = admitted && search.matches( -+ () -> { -+ primary.incrementAndGet(); -+ return bytes(row.get("primary").getAsString()); -+ }, -+ () -> { -+ secondary.incrementAndGet(); -+ return bytes(row.get("secondary").getAsString()); -+ }); -+ assertEquals(row.get("result").getAsInt(), match ? row.get("availability").getAsInt() : 3); -+ assertEquals(row.get("primaryCalls").getAsInt(), primary.get()); -+ assertEquals(row.get("secondaryCalls").getAsInt(), secondary.get()); -+ cases++; -+ } -+ assertEquals(304, cases); -+ } -+ -+ private static byte[] bytes(final String value) { -+ return value.getBytes(StandardCharsets.UTF_8); -+ } -+ -+} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingSearchTextTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingSearchTextTest.java new file mode 100644 index 000000000..b60776001 @@ -12561,6 +12542,122 @@ index 000000000..86db18f09 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearchTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearchTest.java +new file mode 100644 +index 000000000..97641080e +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeRecipeSearchTest.java +@@ -0,0 +1,110 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.resourcepack.ui; ++ ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.Base64; ++import java.util.concurrent.atomic.AtomicInteger; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++import static org.junit.jupiter.api.Assertions.*; ++ ++class NativeRecipeSearchTest { ++ ++ @Test ++ void normalizesTheQueryAndKeepsNameProductionLazy() { ++ assertTrue(new NativeRecipeSearch("", NativeRecipeSearch.Mode.COOKING).matches(() -> fail("Unexpected primary lookup"), () -> fail("Unexpected label lookup"))); ++ assertTrue(new NativeRecipeSearch("STRASSE", NativeRecipeSearch.Mode.COOKING).matches(() -> bytes("Straße"), () -> fail("Unexpected label lookup"))); ++ assertTrue(new NativeRecipeSearch("CAFÉ", NativeRecipeSearch.Mode.COOKING).matches(() -> bytes("other"), () -> bytes("§aCafé"))); ++ assertFalse(new NativeRecipeSearch("§acafé", NativeRecipeSearch.Mode.COOKING).matches(() -> bytes("other"), () -> bytes("§aCafé"))); ++ assertFalse(new NativeRecipeSearch(" beef ", NativeRecipeSearch.Mode.COOKING).matches(() -> bytes("Beef"), () -> bytes(""))); ++ assertFalse(new NativeRecipeSearch("beef raw", NativeRecipeSearch.Mode.COOKING).matches(() -> bytes("Raw Beef"), () -> bytes(""))); ++ assertFalse(new NativeRecipeSearch("be" + Character.toString(0x200B) + "ef", NativeRecipeSearch.Mode.COOKING).matches(() -> bytes("Beef"), () -> bytes(""))); ++ final byte[] query = bytes("BEEF"); ++ final var search = new NativeRecipeSearch(query, NativeRecipeSearch.Mode.COOKING); ++ query[0] = 'x'; ++ assertTrue(search.matches(() -> bytes("Beef"), () -> fail("Unexpected label lookup"))); ++ } ++ ++ @Test ++ void strippingUsesTheNativeByteAutomatonIncludingIncompleteAndUnknownCodes() { ++ assertArrayEquals(bytes("A§ZB§xC"), NativeRecipeSearch.stripFormatting(bytes("§aA§ZB§xC§w"))); ++ assertArrayEquals(bytes("A"), NativeRecipeSearch.stripFormatting(bytes("A§"))); ++ assertArrayEquals(bytes("A"), NativeRecipeSearch.stripFormatting(new byte[]{'A', (byte) 0xC2})); ++ assertArrayEquals(new byte[]{(byte) 0xC2, (byte) 0xC2, (byte) 0xA7, 'a'}, ++ NativeRecipeSearch.stripFormatting(new byte[]{(byte) 0xC2, (byte) 0xC2, (byte) 0xA7, 'a'})); ++ } ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_COOKING_SEARCH", matches = ".+") ++ void matchesTheCapturedPredicateCompositionAndLazyNameBoundaries() throws Exception { ++ final var data = JsonParser.parseString(Files.readString(Path.of(System.getenv("STACKANVIL_NATIVE_COOKING_SEARCH")))).getAsJsonObject(); ++ int cases = 0; ++ for (final var element : data.getAsJsonArray("rows")) { ++ final var row = element.getAsJsonObject(); ++ final var primary = new AtomicInteger(); ++ final var secondary = new AtomicInteger(); ++ final var search = new NativeRecipeSearch(row.get("query").getAsString(), NativeRecipeSearch.Mode.COOKING); ++ final boolean admitted = row.get("filtering").getAsBoolean() || row.get("eligible").getAsBoolean(); ++ final boolean match = admitted && search.matches( ++ () -> { ++ primary.incrementAndGet(); ++ return bytes(row.get("primary").getAsString()); ++ }, ++ () -> { ++ secondary.incrementAndGet(); ++ return bytes(row.get("secondary").getAsString()); ++ }); ++ assertEquals(row.get("result").getAsInt(), match ? row.get("availability").getAsInt() : 3); ++ assertEquals(row.get("primaryCalls").getAsInt(), primary.get()); ++ assertEquals(row.get("secondaryCalls").getAsInt(), secondary.get()); ++ cases++; ++ } ++ assertEquals(304, cases); ++ } ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_PLAYER_SEARCH", matches = ".+") ++ void matchesTheCompletePlayerSearchAndItsLazyNameProviders() throws Exception { ++ final var data = JsonParser.parseString(Files.readString(Path.of(System.getenv("STACKANVIL_NATIVE_PLAYER_SEARCH")))).getAsJsonObject(); ++ for (final var element : data.getAsJsonArray("rows")) { ++ final var row = element.getAsJsonObject(); ++ final var primary = new AtomicInteger(); ++ final var secondary = new AtomicInteger(); ++ final var mode = row.get("mode").getAsInt() == 0 ++ ? NativeRecipeSearch.Mode.PLAYER_SUBSTRING : NativeRecipeSearch.Mode.PLAYER_WORD_PREFIX; ++ final var search = new NativeRecipeSearch(Base64.getDecoder().decode(row.get("query").getAsString()), mode); ++ final boolean matched = search.matches( ++ () -> { ++ primary.incrementAndGet(); ++ return Base64.getDecoder().decode(row.get("primary").getAsString()); ++ }, ++ () -> { ++ secondary.incrementAndGet(); ++ return Base64.getDecoder().decode(row.get("secondary").getAsString()); ++ }); ++ assertEquals(row.get("result").getAsInt() != 3, matched, row.toString()); ++ assertEquals(row.get("primaryCalls").getAsInt(), primary.get()); ++ assertEquals(row.get("secondaryCalls").getAsInt(), secondary.get()); ++ } ++ for (final var element : data.getAsJsonArray("wordRows")) { ++ final var row = element.getAsJsonObject(); ++ final byte[] query = Base64.getDecoder().decode(row.get("query").getAsString()); ++ final byte[] text = Base64.getDecoder().decode(row.get("text").getAsString()); ++ final var search = new NativeRecipeSearch(query, NativeRecipeSearch.Mode.PLAYER_WORD_PREFIX); ++ assertEquals(query.length == 0 || row.get("result").getAsBoolean(), ++ search.matches(() -> text, () -> new byte[0]), row.toString()); ++ } ++ } ++ ++ private static byte[] bytes(final String value) { ++ return value.getBytes(StandardCharsets.UTF_8); ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeSearchNormalizationTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/NativeSearchNormalizationTest.java new file mode 100644 index 000000000..b79b89724 diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index b379a98c..7e3b5645 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -376,10 +376,12 @@ The shared UI patch now exports the query matcher, target normalizer, and raw ed These APIs do not register a channel or replace the default cooking screen. The production menu still publishes an empty query until name producers, transport, and widget integration are complete. -`CookingRecipeSearch` normalizes the query once and requests primary names lazily. +`NativeRecipeSearch` in `COOKING` mode normalizes the query once and requests primary names lazily. A primary match skips the secondary-label producer. An empty query skips both producers. The secondary branch removes native formatting codes before normalization. -The matcher does not trim spaces, split words, or substitute Java preview names. +This mode does not trim spaces, split words, or substitute Java preview names. +The same API now provides explicit player substring and word-prefix policies. +See [the player search reference](../../../docs/native-player-inventory.md#player-search-policies) for their rules and evidence. `CookingSearchText` retains the latest pending raw string until a ready tick. Equal edits still apply. An empty string remains a pending edit until that tick. @@ -514,3 +516,20 @@ Full catalog acquisition, controller ownership, action admission, physical input The full four-project build passes 2,826 tests with 163 optional skips and no failures or errors. Checkstyle passes after replay. All 1,466 core classes match the embedded copies, and all 1,191 client classes match the tested add-on. The packaged core matches all 3,584 eligibility cases and retains all 4,144 collection/history comparisons, with production origins verified. + +## Shared player search policy + +`NativeRecipeSearch` replaces the cooking-only API with explicit cooking, player substring and player word-prefix modes. +The cooking policy preserves its primary-name behavior. Player policies remove formatting from both name sources. +All modes share target normalization and lazy primary/secondary producers. +The player word policy requires the complete literal query and any query-token/name-token prefix match. +It splits only at ASCII spaces and preserves the native treatment of leading, repeated and trailing delimiters. + +The packaged core matches 1,136 complete native player searches, 1,220 normalized word controls and 304 retained cooking compositions. +Four deliberate semantic regressions fail. Native formatting, normalization, validity, matching, vector allocation and cleanup execute. +Name/unlock providers, raw/fallback game types, stable item definitions and standard byte/stream interfaces remain supplied boundaries. +The [player search reference](../../../docs/native-player-inventory.md#player-search-policies) records these limits and the evaluator dependencies. + +All four builds pass with 2,842 tests, 163 optional skips and no failures or errors. +Checkstyle passes after full replay. All 1,472 core classes match both embedded copies; all 1,191 client classes match the tested add-on. +The actual player availability evaluator, authoritative name acquisition, installed-screen search and pixels remain unfinished. From c20c993984f0746c07f0491a959ccc69f509c916 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:19:52 +0200 Subject: [PATCH 07/12] feat(crafting): query native recipe output unlocks Resolve output unlocking from current connection rules and accepted recipes. Preserve reconstructed count-one keys, exact metadata buckets and replacement by recipe ID before context or stack filtering, so a replaced recipe cannot remain visible through an older entry. Keep the private index tied to catalog revisions and connection rewriters, while unlock membership remains current. Record original executable comparisons, retained-state regressions and full packaged validation. Ingredient counting, context acquisition and player-screen integration remain unfinished. --- docs/native-player-inventory.md | 45 +++ ...edrock-recipes-and-place-ingredients.patch | 350 ++++++++++++++++-- 2 files changed, 373 insertions(+), 22 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 80a16e90..49f5ca87 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -491,3 +491,48 @@ The retained progressive player model still matches 508 controls across 9,438 fr All four projects build with 2,842 passing tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. All 1,472 core classes match the embedded add-on and proxy copies. All 1,191 client classes match the tested add-on. Captures and verifiers remain private. + +## Output unlocking + +`CraftingDataTracker.isOutputUnlocked` reads current connection game rules, registered recipes and unlocked recipe IDs. +Disabled or unavailable `recipesUnlock` rules admit the output before lookup, as the native predicate does. +With unlocking enabled, the query uses the native output index independently of displayed recipes and ingredient placement capacity. + +Registration indexes only the first fixed result. It reconstructs a count-one Item key before selecting the exact definition/auxiliary bucket. +This construction preserves associated-block resolution, metadata normalization and the native wildcard value. +Ingredient wildcard comparison does not select output-index buckets. +The query applies full native stack identity after it selects the bucket. +Root quantity does not determine that identity. + +Each output bucket retains the latest recipe with a given ID, including registrations under different station tags. +Context, stack and unlock filtering happen after that replacement. +An earlier unlocked recipe cannot remain visible when its replacement belongs to another context or has different stack data. +Different output buckets retain their own replacements. + +The tracker caches the private index against the catalog revision and connection rewriters. +Recipe additions, catalog clears and unlock updates invalidate that snapshot. +Each query reads current rules and unlock membership. It does not cache the unlocked result or reuse Java recipe-book visibility. +The tests also compose actual associated-block construction with the stripped-oak-wood auxiliary-eight alias and wildcard metadata. + +The pinned client remains Bedrock 1.26.51.1, build 51061372, protocol 2193. +The original registration region `0x14324d3bf` through `0x14324e422` executes key construction, tree/hash insertion, replacement and rehash. +Complete output queries `0x143249b10` and `0x143249870` execute context hashing, per-query deduplication and full stack matching. +Complete functions `0x143439200`, `0x14344d180` and `0x142895850` execute rule checks, actor-component lookup and unlock membership. +Negative input auxiliaries use the original count-one constructor before the harness sets controlled quantities. +Query temporaries return to the persistent index's allocation baseline. + +The reference has 589 controls. The packaged tracker and real Item rewriter match 553 ordinary controls across 1,659 unlock-state comparisons. +Four retained catalog flows add 12 update/clear comparisons. +Thirty-six missing-component controls remain native references for later screen-ownership integration. +Six deliberate regressions fail: first-registration retention, early context filtering, early stack filtering, stale index reuse, ignored rules and collapsed metadata buckets. + +The harness supplies admitted static recipe objects, result getters, context/Level/actor backing, stable ordinary definitions and connection definition lookup. +Item copies/destruction, empty userdata loading, derived item refresh, clocks and allocator/CRT remain supplied interfaces. +The complete wire registration, concrete recipe-class admission, actual recipe-context production and actor-component construction remain unverified. +Nonordinary block canonicalization retains its earlier constructor evidence; these new full-query controls use ordinary definitions and controlled block pointers. + +All four projects build with 2,843 passing tests, 163 optional skips and no failures or errors. +Checkstyle passes. All 1,475 core classes match both embedded copies; all 1,191 client classes match the tested add-on. + +The player availability evaluator still needs ingredient counts, actual context inputs, evaluator installation and screen lifecycle integration. +The four original reports and all five goal areas remain active. diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index f075e40a..89a03c87 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From 3366790b262bc67de3947f57e19bf90aac50a657 Mon Sep 17 00:00:00 2001 +From c2d0ab7fc7b323f18abb62d2e112d27810e0bf4e Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -403,6 +403,29 @@ recipe-book-only layouts, unknown values, independent furnace settings, workbench admission and pending-close restoration. This adds native option state and Java projection, not the unfinished authored inventory screen, player portrait, native tab/search controls or full physical UI parity. + +Add the native player output-unlock query to the authoritative recipe tracker. +Index only the first fixed result by its reconstructed count-one Item key. +Require exact definition/auxiliary bucket identity before full stack matching, +and retain the last recipe pointer for an ID within each output bucket, even +across station tags. Do not filter by contexts or unlocks before replacement. +Keep displayed recipes and placement capacity outside this query. + +Rebuild the private immutable index after catalog revisions or connection +rewriter changes. Read current game rules and unlocked IDs on every query. +Reuse verified stack construction for associated-block ownership and metadata +normalization; ingredient wildcard matching is not an output-index key. + +Complete original output queries, unlock membership and their registration +region execute on 589 controls in pinned 1.26.51.1 build51061372. The production +tracker and real Item rewriter match 553 ordinary controls over 1,659 unlock-state +steps plus 12 catalog update/clear steps. Typed static recipes, context/Level/ +actor backing, definition lookup, empty userdata/derived refresh, item copies, +clocks and allocator/CRT remain supplied boundaries. Original key construction, +index replacement/rehash, full stack identity and query allocation cleanup +execute. Missing actor-component controls remain native reference-only. +Full context acquisition, ingredient counting, screen ownership and pixels +remain separate requirements. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + @@ -425,15 +448,16 @@ player portrait, native tab/search controls or full physical UI parity. .../protocol/model/recipe/ItemDescriptor.java | 13 +- .../protocol/model/recipe/MolangTagQuery.java | 64 ++ .../protocol/model/recipe/MultiRecipe.java | 46 +- + .../model/recipe/RecipeOutputKey.java | 9 + .../protocol/model/recipe/ShapedRecipe.java | 65 +- .../model/recipe/ShapelessRecipe.java | 6 +- .../protocol/model/recipe/SmithingRecipe.java | 4 +- .../protocol/packet/InventoryPackets.java | 146 +++- .../protocol/packet/JoinPackets.java | 1 + .../protocol/packet/MultiStatePackets.java | 55 ++ - .../protocol/rewriter/ItemRewriter.java | 30 +- + .../protocol/rewriter/ItemRewriter.java | 46 +- .../protocol/storage/CraftingDataStorage.java | 3 +- - .../protocol/storage/CraftingDataTracker.java | 573 +++++++++++-- + .../protocol/storage/CraftingDataTracker.java | 626 ++++++++++++-- .../storage/InventoryOptionsStorage.java | 187 ++++ .../storage/InventoryRequestTracker.java | 7 + .../protocol/storage/InventoryTracker.java | 16 + @@ -458,15 +482,16 @@ player portrait, native tab/search controls or full physical UI parity. .../NativeFurnaceOptionsPacketTest.java | 214 +++++ .../storage/CookingRecipeBookTest.java | 154 ++++ .../protocol/storage/CraftingCursorTest.java | 158 ++++ - .../storage/CraftingDataTrackerTest.java | 400 +++++++++ + .../storage/CraftingDataTrackerTest.java | 409 +++++++++ .../storage/FurnaceRecipeRequestTest.java | 797 ++++++++++++++++++ .../storage/NativeCookingUiStateTest.java | 619 ++++++++++++++ + .../storage/NativeRecipeOutputUnlockTest.java | 166 ++++ ...NativeRecipeRegistrationReferenceTest.java | 104 +++ .../item/BedrockRecipeResultTypeTest.java | 106 +++ .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 81 +- - 62 files changed, 7753 insertions(+), 161 deletions(-) + 64 files changed, 8006 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -479,6 +504,7 @@ player portrait, native tab/search controls or full physical UI parity. create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQuery.java + create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOutputKey.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryOptionsStorage.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/NativeCookingUiState.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/types/item/BedrockRecipeResultType.java @@ -502,6 +528,7 @@ player portrait, native tab/search controls or full physical UI parity. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTrackerTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/FurnaceRecipeRequestTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeCookingUiStateTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeOutputUnlockTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeRegistrationReferenceTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/item/BedrockRecipeResultTypeTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/model/FurnaceOptionsTypeTest.java @@ -2541,6 +2568,21 @@ index 7ad3eb6d6..0610c6432 100644 + private record Identity(String name, String station) { } + } +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOutputKey.java b/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOutputKey.java +new file mode 100644 +index 000000000..d8d709d57 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOutputKey.java +@@ -0,0 +1,9 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model.recipe; ++ ++/** Exact Item-definition and auxiliary bucket used before full output-stack matching. */ ++public record RecipeOutputKey(int identifier, short metadata) { } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ShapedRecipe.java b/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ShapedRecipe.java index 0554290f9..8178fbee1 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ShapedRecipe.java @@ -3030,7 +3072,7 @@ index 0b8c600e9..4fef2c18d 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 26915529d..e3943150f 100644 +index 26915529d..b9b61e10a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -40,9 +40,9 @@ import com.viaversion.viaversion.util.Key; @@ -3044,7 +3086,15 @@ index 26915529d..e3943150f 100644 import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -71,6 +71,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; +@@ -55,6 +55,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; + import net.raphimc.viabedrock.protocol.model.ItemNameContext; + import net.raphimc.viabedrock.protocol.model.ItemStackHeader; + import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; ++import net.raphimc.viabedrock.protocol.model.recipe.RecipeOutputKey; + import net.raphimc.viabedrock.protocol.model.ItemStackingProperties; + import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; + import net.raphimc.viabedrock.protocol.model.ItemCooldown; +@@ -71,6 +72,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; import net.raphimc.viabedrock.protocol.types.array.ArrayType; import net.raphimc.viabedrock.protocol.types.inventory.ItemStackRequestResultType; import net.raphimc.viabedrock.protocol.types.item.BedrockItemType; @@ -3052,7 +3102,7 @@ index 26915529d..e3943150f 100644 import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackDescriptorType; import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; -@@ -102,6 +103,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -102,6 +104,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Set componentItems; private final Int2ObjectMap blockItemValidBlockStates; private final Type itemType; @@ -3061,7 +3111,7 @@ index 26915529d..e3943150f 100644 private final Type optionalItemType; private final Type itemArrayType; private final Type newItemType; -@@ -189,6 +192,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -189,6 +193,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex this.items.getOrDefault("minecraft:shield", 0), false, this); @@ -3070,7 +3120,7 @@ index 26915529d..e3943150f 100644 this.optionalItemType = new OptionalType<>(this.itemType); this.itemArrayType = new ArrayType<>(this.itemType, BedrockTypes.UNSIGNED_VAR_INT); this.newItemType = -@@ -515,6 +520,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -515,6 +521,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? blocks.blockItemState(queryAssociation, (short) auxiliary) : null); } @@ -3092,7 +3142,29 @@ index 26915529d..e3943150f 100644 public ItemRegistry getItemRegistry() { return this.registry; } -@@ -582,6 +602,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -539,6 +560,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex + return NativeItemStackMatcher.matchesIdentity(query, candidate, blocks::blockState, blocks::restrictionBlocks); + } + ++ /** Output lookup compares the exact defined Item and selected auxiliary before stack identity. */ ++ public RecipeOutputKey recipeOutputKey(final BedrockItem item) { ++ if (item == null || this.registry.definition(item.identifier()) == null) { ++ return null; ++ } ++ final Short auxiliary = this.stackingAuxiliary(item); ++ return auxiliary != null ? new RecipeOutputKey(item.identifier(), auxiliary) : null; ++ } ++ ++ /** Registration reconstructs a count-one key, including associated-block resolution and metadata normalization. */ ++ public RecipeOutputKey recipeOutputRegistrationKey(final BedrockItem output) { ++ final RecipeOutputKey key = this.recipeOutputKey(output); ++ return key != null ? this.recipeOutputKey(this.createNetworkItem(output.identifier(), key.metadata(), (byte) 1, 0)) : null; ++ } ++ + public boolean canMerge(final BedrockItem source, final BedrockItem destination) { + if (destination == null || destination.isEmpty()) { + return false; +@@ -582,6 +618,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.itemType; } @@ -3122,10 +3194,10 @@ index 4506fe070..2b1c6b934 100644 @Override public @NonNull String toString() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java -index 73165b006..669157039 100644 +index 73165b006..7be67b71a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java -@@ -19,17 +19,34 @@ import com.viaversion.viaversion.api.type.types.version.VersionedTypes; +@@ -19,17 +19,36 @@ import com.viaversion.viaversion.api.type.types.version.VersionedTypes; import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; import net.raphimc.viabedrock.api.model.container.Container; @@ -3134,6 +3206,7 @@ index 73165b006..669157039 100644 import net.raphimc.viabedrock.protocol.model.BedrockItem; import net.raphimc.viabedrock.protocol.model.recipe.*; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; import java.util.ArrayList; +import java.util.HashMap; @@ -3157,10 +3230,11 @@ index 73165b006..669157039 100644 + private final Set unlockedRecipeIds = new HashSet<>(); + private Map displayedRecipes = Map.of(); + private int nextDisplayId; ++ private OutputCatalog outputCatalog; public CraftingDataTracker(final UserConnection user) { super(user); -@@ -39,35 +56,140 @@ public final class CraftingDataTracker extends StoredObject { +@@ -39,35 +58,186 @@ public final class CraftingDataTracker extends StoredObject { return this.recipes; } @@ -3179,6 +3253,52 @@ index 73165b006..669157039 100644 + return display == null || this.displayedCatalog == null ? null : this.displayedCatalog.recipe(displayId); + } + ++ /** Query the current native output index without depending on displayed recipes or placement capacity. */ ++ public boolean isOutputUnlocked(final BedrockItem query, final List contexts) { ++ final var rules = this.user().get(GameRulesStorage.class); ++ if (rules == null || !Boolean.TRUE.equals(rules.getGameRule("recipesUnlock"))) { ++ return true; ++ } ++ final var items = this.user().get(ItemRewriter.class); ++ final RecipeOutputKey key = items.recipeOutputKey(query); ++ if (key == null) { ++ return false; ++ } ++ final var blocks = this.user().get(BlockStateRewriter.class); ++ if (this.outputCatalog == null || this.outputCatalog.revision != this.catalogRevision ++ || this.outputCatalog.items != items || this.outputCatalog.blocks != blocks) { ++ final Map> outputs = new HashMap<>(); ++ for (final CraftingDataStorage data : this.recipes) { ++ final BedrockItem output = firstOutput(data.recipe()); ++ final RecipeOutputKey registered = items.recipeOutputRegistrationKey(output); ++ if (registered != null) { ++ // The native per-output map replaces the recipe pointer by ID, even across station tags. ++ outputs.computeIfAbsent(registered, ignored -> new HashMap<>()) ++ .put(data.recipe().getUniqueId(), new OutputRecipe(data, output)); ++ } ++ } ++ outputs.replaceAll((ignored, recipes) -> Map.copyOf(recipes)); ++ this.outputCatalog = new OutputCatalog(this.catalogRevision, items, blocks, Map.copyOf(outputs)); ++ } ++ for (final OutputRecipe recipe : this.outputCatalog.outputs.getOrDefault(key, Map.of()).values()) { ++ if (contexts.contains(recipe.data.recipe().getRecipeTag()) ++ && items.matchesStackIdentity(query, recipe.output) && this.isRecipeUnlocked(recipe.data)) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ private static BedrockItem firstOutput(final Recipe recipe) { ++ final List outputs = switch (recipe) { ++ case ShapedRecipe shaped -> shaped.getResults(); ++ case ShapelessRecipe shapeless -> shapeless.getResults(); ++ case SmithingRecipe smithing -> List.of(smithing.getResult()); ++ default -> List.of(); ++ }; ++ return outputs.isEmpty() ? null : outputs.getFirst(); ++ } ++ + public void sendJavaGhostRecipe(final UserConnection user, + final int containerId, final int displayId) { + final RecipeDisplay display = this.displayedRecipes.get(displayId); @@ -3209,7 +3329,7 @@ index 73165b006..669157039 100644 + identities.add(registrationKey(data)); + networkIds.add(data.networkId()); + } - } ++ } + for (final CraftingDataStorage data : recipes) { + final RecipeRegistrationKey identity = registrationKey(data); + // Native network registration rejects either conflict before inserting @@ -3246,7 +3366,7 @@ index 73165b006..669157039 100644 + case 3 -> this.unlockedRecipeIds.removeAll(ids); + case 4 -> this.unlockedRecipeIds.clear(); + default -> { } -+ } + } + this.catalogRevision = revision; + } + @@ -3319,7 +3439,7 @@ index 73165b006..669157039 100644 } private boolean matchesSmithing(final Container container, final SmithingRecipe recipe) { -@@ -76,79 +198,110 @@ public final class CraftingDataTracker extends StoredObject { +@@ -76,79 +246,110 @@ public final class CraftingDataTracker extends StoredObject { && this.matches(recipe.getAdditionIngredient(), container.getItem(52)); } @@ -3474,7 +3594,7 @@ index 73165b006..669157039 100644 return container.getItem(container.bedrockSlot(index + 1)); } -@@ -168,19 +321,17 @@ public final class CraftingDataTracker extends StoredObject { +@@ -168,19 +369,17 @@ public final class CraftingDataTracker extends StoredObject { continue; } final ItemDescriptor descriptor = recipe.getIngredients().get(0); @@ -3500,7 +3620,7 @@ index 73165b006..669157039 100644 } final PacketWrapper packet = -@@ -192,6 +343,209 @@ public final class CraftingDataTracker extends StoredObject { +@@ -192,6 +391,209 @@ public final class CraftingDataTracker extends StoredObject { BedrockProtocol.MAPPINGS.getJavaSlotDisplayId("minecraft:item_stack"), entry.output()); } packet.send(BedrockProtocol.class); @@ -3710,7 +3830,7 @@ index 73165b006..669157039 100644 } static void writeStonecutterRecipe(final PacketWrapper packet, final int inputId, -@@ -203,4 +557,83 @@ public final class CraftingDataTracker extends StoredObject { +@@ -203,4 +605,88 @@ public final class CraftingDataTracker extends StoredObject { private record StonecutterEntry(int inputId, Item output) { } @@ -3747,6 +3867,11 @@ index 73165b006..669157039 100644 + } + } + ++ private record OutputRecipe(CraftingDataStorage data, BedrockItem output) { } ++ ++ private record OutputCatalog(long revision, ItemRewriter items, BlockStateRewriter blocks, ++ Map> outputs) { } ++ + public record CatalogEntry(int id, CraftingDataStorage data, boolean unlocked) { } + + /** An action can use this snapshot only on its original connection and catalog revision. */ @@ -7150,10 +7275,10 @@ index 000000000..bb2efb764 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTrackerTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTrackerTest.java new file mode 100644 -index 000000000..6da0ed14c +index 000000000..3f9bd4418 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTrackerTest.java -@@ -0,0 +1,400 @@ +@@ -0,0 +1,409 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7183,6 +7308,7 @@ index 000000000..6da0ed14c +import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; +import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptorType; +import net.raphimc.viabedrock.protocol.model.recipe.RecipeType; ++import net.raphimc.viabedrock.protocol.model.recipe.RecipeOutputKey; +import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; +import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; +import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; @@ -7236,6 +7362,8 @@ index 000000000..6da0ed14c + Mockito.when(blocks.blockState(124)).thenReturn(variantBlock); + Mockito.when(blocks.blockItemNumericId(123)).thenReturn(itemId & 0xFFFF); + Mockito.when(blocks.blockItemNumericId(124)).thenReturn(itemId & 0xFFFF); ++ Mockito.when(blocks.registeredRuntimeId(defaultBlock)).thenReturn(123); ++ Mockito.when(blocks.registeredRuntimeId(variantBlock)).thenReturn(124); + Mockito.doReturn(user).when(rewriter).user(); + Mockito.when(user.get(ItemRewriter.class)).thenReturn(rewriter); + Mockito.when(user.get(BlockStateRewriter.class)).thenReturn(blocks); @@ -7247,6 +7375,12 @@ index 000000000..6da0ed14c + new BlockItemDescriptor(1074, (short) 1)); + Mockito.when(blocks.blockItemRecipeDescriptor(Mockito.eq(identifier), Mockito.anyShort())).thenReturn( + new BlockItemDescriptor(1074, (short) 0)); ++ assertEquals(new RecipeOutputKey(itemId, (short) 8), ++ rewriter.recipeOutputKey(new BedrockItem(itemId, (short) 8, (byte) 1))); ++ assertEquals(new RecipeOutputKey(itemId, (short) 0), ++ rewriter.recipeOutputRegistrationKey(new BedrockItem(itemId, (short) 8, (byte) 1))); ++ assertEquals(new RecipeOutputKey(itemId, Short.MAX_VALUE), ++ rewriter.recipeOutputRegistrationKey(new BedrockItem(itemId, Short.MAX_VALUE, (byte) 1))); + final var ingredient = new ItemDescriptor.ItemNameDescriptor("minecraft:stripped_oak_wood", 8); + for (final var codec : List.of(new BedrockItemType(0, false, rewriter), + new NetworkItemStackDescriptorType(0, true, rewriter))) { @@ -8982,6 +9116,178 @@ index 000000000..e4b19b36e + private record Fixture(UserConnection user, InventoryTracker inventory, FurnaceContainer station, NativeCookingUiState state, ItemRewriter items) { } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeOutputUnlockTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeOutputUnlockTest.java +new file mode 100644 +index 000000000..062cbc593 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeOutputUnlockTest.java +@@ -0,0 +1,166 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.storage; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.libs.gson.JsonArray; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import net.raphimc.viabedrock.api.model.BlockState; ++import net.raphimc.viabedrock.api.chunk.blockstate.BlockItemDescriptor; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; ++import net.raphimc.viabedrock.protocol.model.recipe.Recipe; ++import net.raphimc.viabedrock.protocol.model.recipe.RecipeType; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; ++import net.raphimc.viabedrock.protocol.model.recipe.SmithingRecipe; ++import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++import org.mockito.Mockito; ++ ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.UUID; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertFalse; ++ ++class NativeRecipeOutputUnlockTest { ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_OUTPUT_UNLOCKS", matches = ".+") ++ void matchesNativeOutputRegistrationQueriesAndLiveUnlockChanges() throws Exception { ++ verify(JsonParser.parseString(Files.readString(Path.of(System.getenv("STACKANVIL_NATIVE_OUTPUT_UNLOCKS")))).getAsJsonObject()); ++ } ++ ++ static int verify(final JsonObject fixture) throws Exception { ++ assertFalse(fixture.getAsJsonArray("rows").isEmpty()); ++ int controls = 0; ++ for (final var element : fixture.getAsJsonArray("rows")) { ++ final var row = element.getAsJsonObject(); ++ if (!row.get("component").getAsString().equals("present")) { ++ continue; // Missing native actor components belong to screen ownership, not this tracker instance. ++ } ++ final var tracker = tracker(row.get("ruleCount").getAsInt() > 4 ? row.get("enabled").getAsBoolean() : null); ++ final List recipes = recipes(row.getAsJsonArray("recipes"), 1); ++ final List allIds = recipes.stream().map(data -> data.recipe().getUniqueId()).toList(); ++ tracker.updateCraftingDataList(recipes, true); ++ tracker.updateUnlockedRecipes(1, strings(row.getAsJsonArray("unlockedIds"))); ++ final BedrockItem query = item(row.getAsJsonObject("query")); ++ final List contexts = strings(row.getAsJsonArray("tags")); ++ assertEquals(row.get("unlocked").getAsBoolean(), tracker.isOutputUnlocked(query, contexts), row::toString); ++ assertEquals(row.get("unlocked").getAsBoolean(), tracker.isOutputUnlocked(query, contexts), row::toString); ++ tracker.updateUnlockedRecipes(4, List.of()); ++ assertEquals(row.getAsJsonObject("cleared").get("unlocked").getAsBoolean(), tracker.isOutputUnlocked(query, contexts), row::toString); ++ tracker.updateUnlockedRecipes(1, allIds); ++ assertEquals(row.getAsJsonObject("granted").get("unlocked").getAsBoolean(), tracker.isOutputUnlocked(query, contexts), row::toString); ++ controls += 3; ++ } ++ for (final var flow : fixture.getAsJsonArray("flows")) { ++ final var tracker = tracker(true); ++ int nextNetworkId = 1; ++ for (final var element : flow.getAsJsonArray()) { ++ final var step = element.getAsJsonObject(); ++ final var recipes = recipes(step.getAsJsonArray("recipes"), nextNetworkId); ++ nextNetworkId += recipes.size(); ++ tracker.updateCraftingDataList(recipes, step.get("clear").getAsBoolean()); ++ tracker.updateUnlockedRecipes(1, strings(step.getAsJsonArray("unlockedIds"))); ++ assertEquals(step.get("unlocked").getAsBoolean(), tracker.isOutputUnlocked( ++ item(step.getAsJsonObject("query")), strings(step.getAsJsonArray("tags"))), step::toString); ++ controls++; ++ } ++ } ++ return controls; ++ } ++ ++ private static CraftingDataTracker tracker(final Boolean enabled) throws Exception { ++ final var user = Mockito.mock(UserConnection.class); ++ final var items = Mockito.mock(ItemRewriter.class, Mockito.CALLS_REAL_METHODS); ++ final var blocks = Mockito.mock(BlockStateRewriter.class); ++ final var rules = Mockito.mock(GameRulesStorage.class); ++ final var registry = new ItemRegistry(new ItemEntry[]{ ++ new ItemEntry("test:ordinary_1", 1, false, ItemVersion.Legacy, null), ++ new ItemEntry("test:ordinary_2", 2, false, ItemVersion.Legacy, null), ++ new ItemEntry("test:ordinary_3", 3, false, ItemVersion.Legacy, null), ++ new ItemEntry("test:ordinary_4", 4, false, ItemVersion.Legacy, null) ++ }); ++ for (final var fieldValue : Map.of("registry", registry, "items", registry.ids(), "cookingProperties", Map.of()).entrySet()) { ++ final var field = ItemRewriter.class.getDeclaredField(fieldValue.getKey()); ++ field.setAccessible(true); ++ field.set(items, fieldValue.getValue()); ++ } ++ Mockito.doReturn(user).when(items).user(); ++ Mockito.when(user.get(ItemRewriter.class)).thenReturn(items); ++ Mockito.when(user.get(BlockStateRewriter.class)).thenReturn(blocks); ++ Mockito.when(user.get(GameRulesStorage.class)).thenReturn(rules); ++ Mockito.when(rules.getGameRule("recipesUnlock")).thenReturn(enabled); ++ final Map states = new HashMap<>(); ++ Mockito.when(blocks.blockState(Mockito.anyInt())).thenAnswer(call -> states.computeIfAbsent(call.getArgument(0), ignored -> Mockito.mock(BlockState.class))); ++ Mockito.when(blocks.blockItemRecipeDescriptor(Mockito.anyInt())).thenAnswer(call -> new BlockItemDescriptor(1, (short) ((int) call.getArgument(0) - 1000))); ++ Mockito.when(blocks.restrictionBlocks(Mockito.any(String[].class))).thenReturn(Set.of()); ++ return new CraftingDataTracker(user); ++ } ++ ++ private static List recipes(final JsonArray data, final int firstNetworkId) { ++ final List recipes = new ArrayList<>(); ++ for (final var recipeElement : data) { ++ final var entry = recipeElement.getAsJsonObject(); ++ final String name = entry.get("name").getAsString(); ++ final String tag = entry.get("tag").getAsString(); ++ final List outputs = new ArrayList<>(); ++ entry.getAsJsonArray("outputs").forEach(output -> outputs.add(item(output.getAsJsonObject()))); ++ final int id = firstNetworkId + recipes.size(); ++ final UUID uuid = new UUID(0, id); ++ final Recipe recipe; ++ final RecipeType type; ++ if (outputs.size() == 1 && id % 3 == 0) { ++ recipe = new SmithingRecipe(name, uuid, tag, 0, new ItemDescriptor.InvalidDescriptor(), ++ new ItemDescriptor.InvalidDescriptor(), new ItemDescriptor.InvalidDescriptor(), outputs.getFirst()); ++ type = RecipeType.SMITHING_TRANSFORM; ++ } else if (id % 3 == 1) { ++ recipe = new ShapedRecipe(name, uuid, tag, 0, ++ new ItemDescriptor[][]{{new ItemDescriptor.InvalidDescriptor()}}, outputs, false); ++ type = RecipeType.SHAPED; ++ } else { ++ recipe = new ShapelessRecipe(name, uuid, tag, 0, List.of(), outputs); ++ type = RecipeType.SHAPELESS; ++ } ++ recipes.add(new CraftingDataStorage(type, id, recipe, entry.get("always").getAsBoolean())); ++ } ++ return recipes; ++ } ++ ++ private static BedrockItem item(final JsonObject value) { ++ final var item = new BedrockItem(value.has("id") ? value.get("id").getAsInt() : 1, ++ value.has("meta") ? value.get("meta").getAsShort() : 0, ++ value.has("count") ? value.get("count").getAsByte() : 1); ++ if (value.has("block")) { ++ item.setBlockRuntimeId(1000 + value.get("block").getAsInt()); ++ } ++ if (value.has("blockingTicks")) { ++ item.setBlockingTicks(value.get("blockingTicks").getAsLong()); ++ } ++ return item; ++ } ++ ++ private static List strings(final JsonArray array) { ++ final List strings = new ArrayList<>(); ++ array.forEach(value -> strings.add(value.getAsString())); ++ return strings; ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeRegistrationReferenceTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeRegistrationReferenceTest.java new file mode 100644 index 000000000..427aa9a1c From d70a6187d99f0f7ea2eeea075dfe8dcd787a18cd Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:47:30 +0200 Subject: [PATCH 08/12] feat(crafting): count native ingredient availability Keep craftability inputs separate from Java placement capacity and item request eligibility. Preserve native descriptor grouping, first-stack matching, sort tie order, shared allocations and final partial rounds. Bind the counter to copied current-owner grid, hotbar, inventory and optional cursor collections through the real connection descriptor converter. Share the pinned STL sort with cooking and retain its license. Refresh merchant patch context for the new imports without changing its additions or removals. All four builds pass with 2850 tests and 163 optional skips. Packaged execution matches 755 original counter controls and rejects six deliberate regressions. Concrete recipe matching/admission, full availability evaluation and installed screen integration remain unfinished; the original reports remain unresolved. --- docs/native-player-inventory.md | 67 +- ...edrock-recipes-and-place-ingredients.patch | 668 +++++++++++++++++- ...edrock-recipes-and-place-ingredients.pr.md | 27 + ...bedrock-merchant-inventories-in-core.patch | 16 +- ...are-native-ui-programs-and-resources.patch | 345 +++++---- ...are-native-ui-programs-and-resources.pr.md | 10 + 6 files changed, 951 insertions(+), 182 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 49f5ca87..7e114b28 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -482,7 +482,7 @@ The shared API returns a text match. The full player availability evaluator rema Native function `0x14341c4c0` installs separate search and category evaluators after collection construction. Search combines text matching, recipe unlocking and craftability. Category views also depend on filtering and an actor ability predicate. The ability predicate is not established as creative mode. -The player screen still needs authoritative name producers, unlock/query inputs, native ingredient counting and installed-screen callbacks. +The player screen still needs authoritative name producers, concrete recipe matching, actual context inputs and installed-screen callbacks. The four original reports and all five goal areas remain unresolved. The packaged core matches all 2,660 search comparisons, including 304 retained cooking compositions. @@ -534,5 +534,68 @@ Nonordinary block canonicalization retains its earlier constructor evidence; the All four projects build with 2,843 passing tests, 163 optional skips and no failures or errors. Checkstyle passes. All 1,475 core classes match both embedded copies; all 1,191 client classes match the tested add-on. -The player availability evaluator still needs ingredient counts, actual context inputs, evaluator installation and screen lifecycle integration. +The player availability evaluator still needs concrete ingredient matching, actual context inputs, evaluator installation and screen lifecycle integration. +The four original reports and all five goal areas remain active. + +## Ingredient availability rounds + +`NativeRecipeCounter` counts available ingredients independently of Java placement capacity and item request authority. +The concrete recipe supplies ingredient emptiness and matching. It must contain at least one nonempty ingredient. +The original counter does not terminate for empty or all-empty ingredient vectors within the execution bounds. +The Java boundary rejects those inputs. Concrete registration and admission remain unfinished. + +The counter groups source stacks by their concrete descriptor hash. +Only the first stack with a nonzero quantity for each hash determines its compatible ingredient slots. +Later stacks retain that decision, including an empty match set. Stack request IDs do not determine count eligibility. +The counter sorts groups once by compatible-slot count, then by descending aggregate quantity. +The shared `NativeSort` retains the target STL ordering of equivalent groups above the insertion-sort threshold. +Cooking priorities use the same extracted algorithm, with the original license notice intact. + +Each round distributes a group's remaining quantity evenly among its unfilled compatible slots. +Earlier slots receive the remainder. Each nonzero assignment retains all contributing collection and slot references. +The smallest allocation among nonempty ingredients adds to the total. +Each active ingredient then consumes that quantity from its chosen group, including repeated consumption from a shared group. +The result retains every round, including the final partial or zero round. +Ingredient descriptor quantities do not divide allocations inside this counter. The concrete matcher can still depend on those quantities. + +`InventoryTracker.craftingSources` captures the current owner's native collections in this order: + +| Collection | Player inventory | Crafting table | +| --- | --- | --- | +| `CraftingInputContainer` | HUD slots 28 through 31 | Table slots 32 through 40 | +| `HotbarContainer` | Inventory slots 0 through 8 | Inventory slots 0 through 8 | +| `InventoryContainer` | Inventory slots 9 through 35 | Inventory slots 9 through 35 | +| `CursorContainer` | Optional HUD slot 0 | Optional HUD slot 0 | + +The optional flag controls cursor inclusion. Both paths always include the crafting grid. +Armor, offhand and preview stacks do not enter these collections. +The producer copies stack data and rejects a missing, stale or unsupported owner. +Result slots remain collection-relative. They require translation before any future item request uses them. +`ItemRewriter.countRecipeInputs` binds the snapshots to the connection's concrete descriptor conversion and hashing. +This API does not publish a second item authority or replace placement and request admission. + +The pinned client remains Bedrock 1.26.51.1, build 51061372, protocol 2193. +Complete original counter `0x14297b140` executes source grouping, matching dispatch, allocation rounds and nested cleanup. +Source descriptor construction `0x142486a70` and `0x1424d0b30`, hashing `0x1424e54b0`, and pair sorting `0x142982a00` also execute. +Source-reference construction and copying preserve actual collection names and relative slot indices. +Constructor `0x1434360d0` establishes collection order and the four-input or nine-input grid capacity. + +The reference covers 755 cases, 7,282 allocation rounds and 55,549 matcher calls. +Fifty controls exercise large sorts, including comparator-equivalent groups above 32 entries. +The Java comparison checks total counts, every allocation, every source reference and exact matcher-call order. +Separate tests compose the real connection rewriter with player and table snapshots, cursor inclusion, immutable copies and owner rejection. + +The harness supplies immutable source models, collection names, vectors, stable ordinary definitions and connection definition lookup. +It supplies the concrete recipe's matching interface through typed per-stack matching sets. +Ingredient descriptor types, emptiness and quantities are controlled inputs. No count or availability decision uses a supplied result. +Native temporary allocation and nested cleanup return to the ingredient baseline; final descriptor cleanup leaves no allocations. +Actual actor/provider capture, concrete recipe admission and matching, full availability evaluation, installed-screen delivery, actions and pixels remain unverified. + +The packaged core matches all 755 counter controls and the six focused counter/source regressions. +Six deliberate changes fail value comparisons: ascending quantity, stable ties, repeated empty-hash matching, zero-quantity inclusion, omitted partial rounds and reversed remainder order. +The retained packaged comparisons pass all 2,660 searches, 1,671 output-unlock/update checks and 508 progressive controls across 9,438 frames. + +All four projects build with 2,850 passing tests, 163 optional skips and no failures or errors. +Checkstyle passes after full replay. All 1,483 core classes match both embedded copies; all 1,191 client classes match the tested add-on. + The four original reports and all five goal areas remain active. diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 89a03c87..6660f83e 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From c2d0ab7fc7b323f18abb62d2e112d27810e0bf4e Mon Sep 17 00:00:00 2001 +From 2fe27fb22c92a866412938d26c2dfdfa0fd3847d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -426,6 +426,31 @@ index replacement/rehash, full stack identity and query allocation cleanup execute. Missing actor-component controls remain native reference-only. Full context acquisition, ingredient counting, screen ownership and pixels remain separate requirements. + +Count native ingredient availability independently of Java placement capacity and +network IDs. Group by the concrete descriptor hash, retain the first matching +policy even for an empty match set, and sort once by compatible-slot count then +descending quantity through the shared pinned STL sort. Distribute quantities +across unfilled slots, preserve every source reference and the final partial +round, and consume shared groups once per active ingredient between rounds. +The concrete recipe remains responsible for matching and empty descriptors. +Reject recipes without active ingredients at this boundary because the original +counter does not terminate for those inputs; registration admission is unresolved. + +Capture the current owner's grid, hotbar, inventory and optional cursor in native +collection order. Preserve collection-relative slots and snapshot all stack data; +exclude armor, offhand and preview cells. ItemRewriter binds these collections to +its real concrete descriptor conversion. This adds count inputs, not an installed +player availability evaluator or a replacement for request admission/placement. + +Complete original 1.26.51.1 counter14297b140 executions supply 755 controls, +7,282 allocation rounds and 55,549 matcher calls, including 50 large-sort cases. +Native source descriptor construction, hashing, pair sorting, source-reference +copies and nested cleanup execute. Immutable model backing, ordinary definitions, +connection definition lookup and concrete recipe matching remain supplied seams. +Actor capture, concrete recipe admission/matching, full evaluator installation, +screen delivery, actions and pixels remain unfinished. Targeted Java comparisons +retain count, every round and source reference, and exact matcher-call order. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + @@ -436,6 +461,7 @@ remain separate requirements. .../model/container/CraftingPlacement.java | 229 +++++ .../container/FurnaceRecipePlacement.java | 219 +++++ .../NativeCookingRecipeSelection.java | 206 +++++ + .../model/container/NativeRecipeCounter.java | 168 ++++ .../block/CraftingTableContainer.java | 100 ++- .../container/block/FurnaceContainer.java | 209 +++++ .../model/container/player/HudContainer.java | 3 + @@ -455,12 +481,12 @@ remain separate requirements. .../protocol/packet/InventoryPackets.java | 146 +++- .../protocol/packet/JoinPackets.java | 1 + .../protocol/packet/MultiStatePackets.java | 55 ++ - .../protocol/rewriter/ItemRewriter.java | 46 +- + .../protocol/rewriter/ItemRewriter.java | 59 +- .../protocol/storage/CraftingDataStorage.java | 3 +- .../protocol/storage/CraftingDataTracker.java | 626 ++++++++++++-- .../storage/InventoryOptionsStorage.java | 187 ++++ .../storage/InventoryRequestTracker.java | 7 + - .../protocol/storage/InventoryTracker.java | 16 + + .../protocol/storage/InventoryTracker.java | 47 ++ .../storage/NativeCookingUiState.java | 502 +++++++++++ .../protocol/types/BedrockTypes.java | 2 + .../types/item/BedrockRecipeResultType.java | 23 + @@ -474,6 +500,8 @@ remain separate requirements. .../container/CraftingOutputPlanTest.java | 84 ++ .../container/CraftingPlacementTest.java | 161 ++++ .../container/FurnaceRecipePlacementTest.java | 418 +++++++++ + .../NativeRecipeCounterFixtureTest.java | 132 +++ + .../container/NativeRecipeCounterTest.java | 66 ++ .../player/InventoryContainerTest.java | 131 +++ .../model/recipe/MolangTagQueryTest.java | 53 ++ .../model/recipe/RecipeOwnershipTest.java | 52 ++ @@ -483,6 +511,7 @@ remain separate requirements. .../storage/CookingRecipeBookTest.java | 154 ++++ .../protocol/storage/CraftingCursorTest.java | 158 ++++ .../storage/CraftingDataTrackerTest.java | 409 +++++++++ + .../storage/CraftingRecipeSourcesTest.java | 128 +++ .../storage/FurnaceRecipeRequestTest.java | 797 ++++++++++++++++++ .../storage/NativeCookingUiStateTest.java | 619 ++++++++++++++ .../storage/NativeRecipeOutputUnlockTest.java | 166 ++++ @@ -491,7 +520,7 @@ remain separate requirements. .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 81 +- - 64 files changed, 8006 insertions(+), 161 deletions(-) + 68 files changed, 8544 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -501,6 +530,7 @@ remain separate requirements. create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CraftingPlacement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeCookingRecipeSelection.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQuery.java @@ -517,6 +547,8 @@ remain separate requirements. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlanTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CraftingPlacementTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacementTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterFixtureTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOwnershipTest.java @@ -526,6 +558,7 @@ remain separate requirements. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CookingRecipeBookTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingCursorTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTrackerTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingRecipeSourcesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/FurnaceRecipeRequestTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeCookingUiStateTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeOutputUnlockTest.java @@ -1646,6 +1679,180 @@ index 000000000..50b7fdf1d + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java +new file mode 100644 +index 000000000..27d44cefc +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java +@@ -0,0 +1,168 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import net.raphimc.viabedrock.api.util.NativeSort; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++ ++import java.util.ArrayList; ++import java.util.Comparator; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.function.ToIntFunction; ++ ++/** Native ingredient availability rounds, independent of placement and stack request authority. */ ++public final class NativeRecipeCounter { ++ ++ private NativeRecipeCounter() { } ++ ++ /** ++ * Recipes must have at least one nonempty ingredient. The original counter does not ++ * terminate for an empty recipe; admission belongs to the concrete recipe owner. ++ */ ++ public static Result count(final Recipe recipe, final List sources, ++ final ToIntFunction descriptorHash) { ++ final int ingredientCount = recipe.ingredientCount(); ++ final boolean[] active = new boolean[ingredientCount]; ++ boolean anyActive = false; ++ for (int index = 0; index < ingredientCount; index++) { ++ active[index] = !recipe.emptyIngredient(index); ++ anyActive |= active[index]; ++ } ++ if (!anyActive) { ++ throw new IllegalArgumentException("Native recipe counting requires a nonempty ingredient"); ++ } ++ ++ final Map byHash = new HashMap<>(); ++ final List groups = new ArrayList<>(); ++ for (final Source source : sources) { ++ for (int slot = 0; slot < source.items.size(); slot++) { ++ final BedrockItem item = source.items.get(slot); ++ if (item.amount() == 0) { ++ continue; ++ } ++ final int hash = descriptorHash.applyAsInt(item); ++ Group group = byHash.get(hash); ++ if (group == null) { ++ group = new Group(); ++ for (int index = 0; index < ingredientCount; index++) { ++ if (recipe.matchesIngredient(index, item)) { ++ group.ingredients.add(index); ++ } ++ } ++ byHash.put(hash, group); ++ groups.add(group); ++ } ++ // Even an empty match set is cached for all later stacks with this hash. ++ if (!group.ingredients.isEmpty()) { ++ group.amount += item.amount(); ++ group.sources.add(new Location(source.collection, slot)); ++ } ++ } ++ } ++ final List ordered = NativeSort.sorted(groups, Comparator ++ .comparingInt((Group group) -> group.ingredients.size()) ++ .thenComparing((first, second) -> Integer.compare(second.amount, first.amount))); ++ ++ int count = 0; ++ final List> rounds = new ArrayList<>(); ++ while (true) { ++ final int[] amounts = new int[ingredientCount]; ++ final Group[] choices = new Group[ingredientCount]; ++ final List> locations = new ArrayList<>(ingredientCount); ++ for (int index = 0; index < ingredientCount; index++) { ++ locations.add(new ArrayList<>()); ++ } ++ for (final Group group : ordered) { ++ if (group.amount <= 0) { ++ continue; ++ } ++ final List open = group.ingredients.stream() ++ .filter(index -> amounts[index] == 0).toList(); ++ if (open.isEmpty()) { ++ continue; ++ } ++ final int quotient = group.amount / open.size(); ++ int remainder = group.amount % open.size(); ++ for (final int index : open) { ++ final int assigned = quotient + (remainder > 0 ? 1 : 0); ++ if (remainder > 0) { ++ remainder--; ++ } ++ choices[index] = group; ++ amounts[index] += assigned; ++ if (assigned != 0) { ++ locations.get(index).addAll(group.sources); ++ } ++ } ++ } ++ int minimum = Integer.MAX_VALUE; ++ final List round = new ArrayList<>(ingredientCount); ++ for (int index = 0; index < ingredientCount; index++) { ++ round.add(new IngredientCount(amounts[index], locations.get(index))); ++ if (active[index]) { ++ minimum = Math.min(minimum, amounts[index]); ++ } ++ } ++ rounds.add(round); ++ if (minimum <= 0) { ++ return new Result(count, rounds); ++ } ++ count += minimum; ++ for (int index = 0; index < ingredientCount; index++) { ++ if (active[index]) { ++ choices[index].amount -= minimum; ++ } ++ } ++ } ++ } ++ ++ /** The concrete recipe owns descriptor emptiness and ingredient matching. */ ++ public interface Recipe { ++ int ingredientCount(); ++ ++ boolean emptyIngredient(int index); ++ ++ boolean matchesIngredient(int index, BedrockItem item); ++ } ++ ++ /** A captured native collection. Slots in results are relative to this collection. */ ++ public record Source(ContainerEnumName collection, List items) { ++ public Source { ++ Objects.requireNonNull(collection); ++ items = items.stream().map(BedrockItem::copy).toList(); ++ } ++ ++ @Override ++ public List items() { ++ return this.items.stream().map(BedrockItem::copy).toList(); ++ } ++ } ++ ++ public record Location(ContainerEnumName collection, int slot) { } ++ ++ public record IngredientCount(int amount, List sources) { ++ public IngredientCount { ++ sources = List.copyOf(sources); ++ } ++ } ++ ++ public record Result(int count, List> rounds) { ++ public Result { ++ rounds = rounds.stream().map(List::copyOf).toList(); ++ } ++ } ++ ++ private static final class Group { ++ private final List ingredients = new ArrayList<>(); ++ private final List sources = new ArrayList<>(); ++ private int amount; ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java index fec81fe6c..178d1ef15 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java @@ -3072,10 +3279,10 @@ index 0b8c600e9..4fef2c18d 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 26915529d..b9b61e10a 100644 +index 26915529d..f83bb23aa 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -40,9 +40,9 @@ import com.viaversion.viaversion.util.Key; +@@ -40,9 +40,11 @@ import com.viaversion.viaversion.util.Key; import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.api.model.BlockState; import net.raphimc.viabedrock.api.model.NativeItemNames; @@ -3083,10 +3290,12 @@ index 26915529d..b9b61e10a 100644 import net.raphimc.viabedrock.api.model.NativeItemStacking; import net.raphimc.viabedrock.api.model.NativeItemStackData; +import net.raphimc.viabedrock.api.model.container.NativeItemStackMatcher; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.Container; import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -55,6 +55,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; +@@ -55,6 +57,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; import net.raphimc.viabedrock.protocol.model.ItemStackHeader; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; @@ -3094,7 +3303,14 @@ index 26915529d..b9b61e10a 100644 import net.raphimc.viabedrock.protocol.model.ItemStackingProperties; import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; import net.raphimc.viabedrock.protocol.model.ItemCooldown; -@@ -71,6 +72,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; +@@ -65,12 +68,14 @@ import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; + import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomAttachableResourceRewriter; + import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomItemTextureResourceRewriter; + import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; ++import net.raphimc.viabedrock.protocol.storage.InventoryTracker; + import net.raphimc.viabedrock.protocol.storage.MapTracker; + import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; + import net.raphimc.viabedrock.protocol.types.BedrockTypes; import net.raphimc.viabedrock.protocol.types.array.ArrayType; import net.raphimc.viabedrock.protocol.types.inventory.ItemStackRequestResultType; import net.raphimc.viabedrock.protocol.types.item.BedrockItemType; @@ -3102,7 +3318,7 @@ index 26915529d..b9b61e10a 100644 import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackDescriptorType; import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; -@@ -102,6 +104,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -102,6 +107,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Set componentItems; private final Int2ObjectMap blockItemValidBlockStates; private final Type itemType; @@ -3111,7 +3327,7 @@ index 26915529d..b9b61e10a 100644 private final Type optionalItemType; private final Type itemArrayType; private final Type newItemType; -@@ -189,6 +193,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -189,6 +196,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex this.items.getOrDefault("minecraft:shield", 0), false, this); @@ -3120,7 +3336,24 @@ index 26915529d..b9b61e10a 100644 this.optionalItemType = new OptionalType<>(this.itemType); this.itemArrayType = new ArrayType<>(this.itemType, BedrockTypes.UNSIGNED_VAR_INT); this.newItemType = -@@ -515,6 +521,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -500,6 +509,16 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex + ? this.user().get(BlockStateRewriter.class).blockItemRecipeDescriptor(item.blockRuntimeId()) : null); + } + ++ public NativeRecipeCounter.Result countRecipeInputs(final NativeRecipeCounter.Recipe recipe, ++ final Container owner, final boolean includeCursor) { ++ return NativeRecipeCounter.count(recipe, ++ this.user().get(InventoryTracker.class).craftingSources(owner, includeCursor), ++ item -> { ++ final ItemRecipeKey key = this.recipeKey(item); ++ return key != null ? key.descriptorHash() : 0; ++ }); ++ } ++ + public boolean matchesRecipeItem(final String identifier, final int metadata, final BedrockItem item) { + final ItemRecipeKey query = this.registry.namedRecipeKey(identifier, metadata); + if (query == null || item == null || item.isEmpty()) { +@@ -515,6 +534,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? blocks.blockItemState(queryAssociation, (short) auxiliary) : null); } @@ -3142,7 +3375,7 @@ index 26915529d..b9b61e10a 100644 public ItemRegistry getItemRegistry() { return this.registry; } -@@ -539,6 +560,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -539,6 +573,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return NativeItemStackMatcher.matchesIdentity(query, candidate, blocks::blockState, blocks::restrictionBlocks); } @@ -3164,7 +3397,7 @@ index 26915529d..b9b61e10a 100644 public boolean canMerge(final BedrockItem source, final BedrockItem destination) { if (destination == null || destination.isEmpty()) { return false; -@@ -582,6 +618,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -582,6 +631,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.itemType; } @@ -4152,10 +4385,30 @@ index 208c07858..0f3889de3 100644 this.drainQueuedInteractions(); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java -index 99d26d410..6d498eac1 100644 +index 99d26d410..6ea6cdaa1 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java -@@ -197,6 +197,7 @@ public class InventoryTracker extends StoredObject { +@@ -28,6 +28,8 @@ import net.raphimc.viabedrock.ViaBedrock; + import net.raphimc.viabedrock.api.chunk.section.BedrockChunkSection; + import net.raphimc.viabedrock.api.model.container.Container; + import net.raphimc.viabedrock.api.model.container.ContainerOpenContext; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; + import net.raphimc.viabedrock.api.model.container.dynamic.BundleContainer; + import net.raphimc.viabedrock.api.model.container.player.ArmorContainer; + import net.raphimc.viabedrock.api.model.container.player.HudContainer; +@@ -48,7 +50,10 @@ import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; + import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; + import net.raphimc.viabedrock.protocol.types.BedrockTypes; + ++import java.util.ArrayList; ++import java.util.Arrays; + import java.util.HashMap; ++import java.util.List; + import java.util.Map; + import java.util.Objects; + import java.util.logging.Level; +@@ -197,6 +202,7 @@ public class InventoryTracker extends StoredObject { this.currentContainerOpenContext = null; } this.pendingCloseContainer = container; @@ -4163,7 +4416,7 @@ index 99d26d410..6d498eac1 100644 } public void setCurrentContainerClosed(final boolean serverInitiated) { -@@ -212,6 +213,7 @@ public class InventoryTracker extends StoredObject { +@@ -212,6 +218,7 @@ public class InventoryTracker extends StoredObject { this.pendingCloseContainerGeneration = 0; this.currentContainerOpenContext = null; this.pendingCloseContainerOpenContext = null; @@ -4171,7 +4424,7 @@ index 99d26d410..6d498eac1 100644 final InventoryRequestTracker requests = this.user().get(InventoryRequestTracker.class); if (requests != null) { requests.drainQueuedInteractions(); -@@ -243,6 +245,7 @@ public class InventoryTracker extends StoredObject { +@@ -243,6 +250,7 @@ public class InventoryTracker extends StoredObject { this.pendingCloseContainerGeneration = 0; this.currentContainerOpenContext = null; this.pendingCloseContainerOpenContext = null; @@ -4179,7 +4432,40 @@ index 99d26d410..6d498eac1 100644 } private void clearQueuedScreenInteractions() { -@@ -401,6 +404,15 @@ public class InventoryTracker extends StoredObject { +@@ -386,6 +394,32 @@ public class InventoryTracker extends StoredObject { + return this.currentContainer; + } + ++ /** Capture the native grid, hotbar, inventory and optional cursor collections in order. */ ++ public List craftingSources(final Container owner, final boolean includeCursor) { ++ if (owner == null || owner != this.currentContainer) { ++ throw new IllegalStateException("Recipe sources require the current container owner"); ++ } ++ final List grid = new ArrayList<>(); ++ if (owner instanceof CraftingTableContainer) { ++ for (int slot = 32; slot <= 40; slot++) { ++ grid.add(owner.getItem(slot)); ++ } ++ } else if (owner instanceof InventoryContainer) { ++ for (int slot = 28; slot <= 31; slot++) { ++ grid.add(this.hudContainer.getItem(slot)); ++ } ++ } else { ++ throw new IllegalArgumentException("Container has no native crafting input collection"); ++ } ++ final List inventory = Arrays.asList(this.inventoryContainer.getItems()); ++ return List.of( ++ new NativeRecipeCounter.Source(ContainerEnumName.CraftingInputContainer, grid), ++ new NativeRecipeCounter.Source(ContainerEnumName.HotbarContainer, inventory.subList(0, 9)), ++ new NativeRecipeCounter.Source(ContainerEnumName.InventoryContainer, inventory.subList(9, inventory.size())), ++ new NativeRecipeCounter.Source(ContainerEnumName.CursorContainer, ++ includeCursor ? List.of(this.hudContainer.getItem(0)) : List.of())); ++ } ++ + public void setCurrentContainer(final Container container) { + this.setCurrentContainer(container, null); + } +@@ -401,6 +435,15 @@ public class InventoryTracker extends StoredObject { this.lastContainerGeneration = generation; this.currentContainerGeneration = generation; this.currentContainerOpenContext = openContext; @@ -4195,7 +4481,7 @@ index 99d26d410..6d498eac1 100644 } private void clearNativeContainerActions() { -@@ -408,6 +420,10 @@ public class InventoryTracker extends StoredObject { +@@ -408,6 +451,10 @@ public class InventoryTracker extends StoredObject { if (state != null) { state.clearActions(); } @@ -6149,6 +6435,216 @@ index 000000000..a4ce77f3e + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterFixtureTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterFixtureTest.java +new file mode 100644 +index 000000000..ca5ac58f5 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterFixtureTest.java +@@ -0,0 +1,132 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++ ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.zip.GZIPInputStream; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertFalse; ++ ++class NativeRecipeCounterFixtureTest { ++ ++ private static final List COLLECTIONS = List.of(ContainerEnumName.CraftingInputContainer, ++ ContainerEnumName.HotbarContainer, ContainerEnumName.InventoryContainer, ContainerEnumName.CursorContainer); ++ private static final List NAMES = List.of("crafting_input_items", "hotbar_items", "inventory_items", "cursor_items"); ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_RECIPE_COUNTER", matches = ".+") ++ void matchesCompleteNativeCounterExecutions() throws Exception { ++ try (var input = new InputStreamReader(new GZIPInputStream(Files.newInputStream( ++ Path.of(System.getenv("STACKANVIL_NATIVE_RECIPE_COUNTER")))), StandardCharsets.UTF_8)) { ++ verify(JsonParser.parseReader(input).getAsJsonObject()); ++ } ++ } ++ ++ static int verify(final JsonObject fixture) { ++ final var definitions = new ItemEntry[4]; ++ for (int index = 0; index < definitions.length; index++) { ++ definitions[index] = new ItemEntry("test:ordinary_" + (index + 1), index + 1, false, ItemVersion.Legacy, null); ++ } ++ final var registry = new ItemRegistry(definitions); ++ final var cases = fixture.getAsJsonArray("cases"); ++ assertFalse(cases.isEmpty()); ++ for (final var element : cases) { ++ final var row = element.getAsJsonObject(); ++ final var ingredients = row.getAsJsonArray("ingredients"); ++ final var rawSources = row.getAsJsonArray("sources"); ++ final List sources = new ArrayList<>(); ++ for (int source = 0; source < rawSources.size(); source++) { ++ final List items = new ArrayList<>(); ++ if (!rawSources.get(source).isJsonNull()) { ++ final var slots = rawSources.get(source).getAsJsonArray(); ++ for (int slot = 0; slot < slots.size(); slot++) { ++ final var value = slots.get(slot).getAsJsonObject(); ++ final var item = new BedrockItem(integer(value, "id", 1), (short) integer(value, "meta", 0), (byte) integer(value, "count", 1)); ++ item.setNetId(source * 1000 + slot); // Tracks the supplied concrete matcher interface, not count eligibility. ++ items.add(item); ++ } ++ } ++ sources.add(new NativeRecipeCounter.Source(COLLECTIONS.get(source), items)); ++ } ++ final List matches = new ArrayList<>(); ++ final var recipe = new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return ingredients.size(); ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ final var value = ingredients.get(index).getAsJsonObject(); ++ return value.has("empty") && value.get("empty").getAsBoolean(); ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ final int source = item.netId() / 1000; ++ final int slot = item.netId() % 1000; ++ final var value = rawSources.get(source).getAsJsonArray().get(slot).getAsJsonObject(); ++ boolean result = false; ++ for (final var candidate : value.getAsJsonArray("matches")) { ++ result |= candidate.getAsInt() == index; ++ } ++ matches.add(new Match(index, source, slot, result)); ++ return result; ++ } ++ }; ++ final var actual = NativeRecipeCounter.count(recipe, sources, item -> { ++ final var key = registry.recipeKey(item, null); ++ return key != null ? key.descriptorHash() : 0; ++ }); ++ final List> rounds = new ArrayList<>(); ++ for (final var batch : row.getAsJsonArray("batches")) { ++ final List round = new ArrayList<>(); ++ for (final var ingredient : batch.getAsJsonArray()) { ++ final var value = ingredient.getAsJsonObject(); ++ final List locations = new ArrayList<>(); ++ for (final var ref : value.getAsJsonArray("sources")) { ++ final var location = ref.getAsJsonObject(); ++ locations.add(new NativeRecipeCounter.Location(COLLECTIONS.get(NAMES.indexOf(location.get("collection").getAsString())), location.get("slot").getAsInt())); ++ } ++ round.add(new NativeRecipeCounter.IngredientCount(value.get("amount").getAsInt(), locations)); ++ } ++ rounds.add(round); ++ } ++ assertEquals(new NativeRecipeCounter.Result(row.get("count").getAsInt(), rounds), actual, row::toString); ++ final List expectedMatches = new ArrayList<>(); ++ for (final var match : row.getAsJsonArray("matches")) { ++ final var value = match.getAsJsonObject(); ++ expectedMatches.add(new Match(value.get("ingredient").getAsInt(), value.getAsJsonArray("source").get(0).getAsInt(), ++ value.getAsJsonArray("source").get(1).getAsInt(), value.get("result").getAsBoolean())); ++ } ++ assertEquals(expectedMatches, matches, row::toString); ++ } ++ return cases.size(); ++ } ++ ++ private static int integer(final JsonObject value, final String name, final int fallback) { ++ return value.has(name) ? value.get(name).getAsInt() : fallback; ++ } ++ ++ private record Match(int ingredient, int source, int slot, boolean result) { } ++ ++} +diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java +new file mode 100644 +index 000000000..56088325a +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java +@@ -0,0 +1,66 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import org.junit.jupiter.api.Test; ++ ++import java.util.List; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertThrows; ++ ++class NativeRecipeCounterTest { ++ ++ @Test ++ void sharesOneDescriptorAcrossIngredientsAndRetainsTheFinalPartialRound() { ++ final var recipe = recipe(3, false); ++ final var item = new BedrockItem(1, (short) 0, (byte) 8); ++ final var source = new NativeRecipeCounter.Source(ContainerEnumName.HotbarContainer, List.of(item)); ++ final var result = NativeRecipeCounter.count(recipe, List.of(source), ignored -> 1); ++ assertEquals(2, result.count()); ++ assertEquals(List.of(3, 3, 2), result.rounds().get(0).stream().map(NativeRecipeCounter.IngredientCount::amount).toList()); ++ assertEquals(List.of(1, 1, 0), result.rounds().get(1).stream().map(NativeRecipeCounter.IngredientCount::amount).toList()); ++ assertEquals(List.of(new NativeRecipeCounter.Location(ContainerEnumName.HotbarContainer, 0)), result.rounds().get(0).get(2).sources()); ++ assertEquals(List.of(), result.rounds().get(1).get(2).sources()); ++ } ++ ++ @Test ++ void snapshotsStackValuesAndDoesNotRequireNetworkIds() { ++ final var item = new BedrockItem(1, (short) 0, (byte) 4); ++ final var source = new NativeRecipeCounter.Source(ContainerEnumName.InventoryContainer, List.of(item)); ++ item.setAmount(0); ++ source.items().getFirst().setAmount(0); ++ assertEquals(4, NativeRecipeCounter.count(recipe(1, false), List.of(source), ignored -> 1).count()); ++ } ++ ++ @Test ++ void requiresConcreteAdmissionOfAtLeastOneNonemptyIngredient() { ++ assertThrows(IllegalArgumentException.class, () -> NativeRecipeCounter.count(recipe(0, true), List.of(), ignored -> 0)); ++ assertThrows(IllegalArgumentException.class, () -> NativeRecipeCounter.count(recipe(2, true), List.of(), ignored -> 0)); ++ } ++ ++ private static NativeRecipeCounter.Recipe recipe(final int count, final boolean empty) { ++ return new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return count; ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ return empty; ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ return true; ++ } ++ }; ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java new file mode 100644 index 000000000..fbc7a40e4 @@ -7688,6 +8184,140 @@ index 000000000..3f9bd4418 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingRecipeSourcesTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingRecipeSourcesTest.java +new file mode 100644 +index 000000000..b5ad79344 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingRecipeSourcesTest.java +@@ -0,0 +1,128 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.storage; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; ++import net.raphimc.viabedrock.api.model.container.block.EnchantmentContainer; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import org.junit.jupiter.api.Test; ++import org.mockito.Mockito; ++ ++import java.util.List; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertThrows; ++ ++class CraftingRecipeSourcesTest { ++ ++ @Test ++ void countsThePlayerGridAndInventoryWithoutArmorOffhandOrImplicitCursor() throws Exception { ++ final var context = context(); ++ final var tracker = context.inventory; ++ final var owner = tracker.getInventoryContainer(); ++ tracker.setCurrentContainer(owner); ++ tracker.getHudContainer().setItem(28, item(2)); ++ owner.setItem(0, item(3)); ++ owner.setItem(9, item(5)); ++ tracker.getHudContainer().setItem(0, item(7)); ++ tracker.getArmorContainer().setItem(0, item(50)); ++ tracker.getOffhandContainer().setItem(0, item(50)); ++ assertEquals(10, context.items.countRecipeInputs(recipe(), owner, false).count()); ++ final var result = context.items.countRecipeInputs(recipe(), owner, true); ++ assertEquals(17, result.count()); ++ assertEquals(List.of( ++ new NativeRecipeCounter.Location(ContainerEnumName.CraftingInputContainer, 0), ++ new NativeRecipeCounter.Location(ContainerEnumName.HotbarContainer, 0), ++ new NativeRecipeCounter.Location(ContainerEnumName.InventoryContainer, 0), ++ new NativeRecipeCounter.Location(ContainerEnumName.CursorContainer, 0)), result.rounds().getFirst().getFirst().sources()); ++ final var captured = tracker.craftingSources(owner, true); ++ owner.getItem(0).setAmount(0); ++ tracker.getHudContainer().getItem(28).setAmount(0); ++ assertEquals(17, NativeRecipeCounter.count(recipe(), captured, ignored -> 1).count()); ++ } ++ ++ @Test ++ void usesTheNineTableInputsAndRetainsCollectionRelativeSlots() throws Exception { ++ final var context = context(); ++ final var tracker = context.inventory; ++ final var table = new CraftingTableContainer(context.user, (byte) 5, null, null); ++ tracker.setCurrentContainer(table); ++ table.setItem(40, item(2)); ++ table.setItem(50, item(100)); ++ tracker.getHudContainer().setItem(28, item(100)); ++ tracker.getInventoryContainer().setItem(8, item(3)); ++ tracker.getInventoryContainer().setItem(35, item(5)); ++ final var result = context.items.countRecipeInputs(recipe(), table, false); ++ assertEquals(10, result.count()); ++ assertEquals(List.of( ++ new NativeRecipeCounter.Location(ContainerEnumName.CraftingInputContainer, 8), ++ new NativeRecipeCounter.Location(ContainerEnumName.HotbarContainer, 8), ++ new NativeRecipeCounter.Location(ContainerEnumName.InventoryContainer, 26)), result.rounds().getFirst().getFirst().sources()); ++ } ++ ++ @Test ++ void refusesStaleOwnersAndContainersWithoutCraftingInputs() throws Exception { ++ final var context = context(); ++ final var tracker = context.inventory; ++ final var table = new CraftingTableContainer(context.user, (byte) 5, null, null); ++ assertThrows(IllegalStateException.class, () -> tracker.craftingSources(table, false)); ++ tracker.setCurrentContainer(table); ++ assertThrows(IllegalStateException.class, () -> tracker.craftingSources(table.copy(), false)); ++ assertThrows(IllegalStateException.class, () -> tracker.craftingSources(null, false)); ++ final var other = context(); ++ final var enchantment = new EnchantmentContainer(other.user, (byte) 5, null, null); ++ other.inventory.setCurrentContainer(enchantment); ++ assertThrows(IllegalArgumentException.class, () -> other.inventory.craftingSources(enchantment, false)); ++ } ++ ++ private static Context context() throws Exception { ++ final var user = Mockito.mock(UserConnection.class); ++ final var tracker = new InventoryTracker(user); ++ final var requests = Mockito.mock(InventoryRequestTracker.class); ++ Mockito.when(requests.isPredictingClick()).thenReturn(true); ++ Mockito.when(user.get(InventoryRequestTracker.class)).thenReturn(requests); ++ Mockito.when(user.get(InventoryTracker.class)).thenReturn(tracker); ++ final var items = Mockito.mock(ItemRewriter.class, Mockito.CALLS_REAL_METHODS); ++ Mockito.doReturn(user).when(items).user(); ++ final var registry = ItemRewriter.class.getDeclaredField("registry"); ++ registry.setAccessible(true); ++ registry.set(items, new ItemRegistry(new ItemEntry[]{new ItemEntry("test:ordinary", 1, false, ItemVersion.Legacy, null)})); ++ return new Context(user, tracker, items); ++ } ++ ++ private static BedrockItem item(final int amount) { ++ return new BedrockItem(1, (short) 0, (byte) amount); ++ } ++ ++ private static NativeRecipeCounter.Recipe recipe() { ++ return new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return 1; ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ return false; ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ return true; ++ } ++ }; ++ } ++ ++ private record Context(UserConnection user, InventoryTracker inventory, ItemRewriter items) { } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/FurnaceRecipeRequestTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/FurnaceRecipeRequestTest.java new file mode 100644 index 000000000..f4a2b620f diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index b9ef6b86..0124dd47 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -437,3 +437,30 @@ These live checks verify the Java workflow; they do not establish native screen All five goal areas and the four original reports remain open. These changes target release 0.3.53. + + +## Native ingredient counting + +Count native ingredient availability separately from placement and stack request authority. +Group by concrete descriptor hash, retain the first stack's match set, and sort once by cardinality then descending quantity. +Preserve collection-relative source references, repeated allocation rounds and the final partial round. +Require at least one nonempty ingredient because the original counter does not terminate for empty recipes within the execution bounds. +Concrete recipe matching and registration admission remain the caller's unfinished integration work. + +The current-owner producer snapshots grid, hotbar, inventory and optional cursor collections in native order. +Player and table grids use four and nine inputs. Armor, offhand and preview cells remain outside those collections. +The connection rewriter supplies actual descriptor conversion and hashing. Network IDs do not gate ingredient counts. +These APIs do not replace item authority, request admission or placement. + +Complete original counter `0x14297b140` supplies 755 cases, 7,282 allocation rounds and 55,549 matcher calls. +Fifty controls cover large cardinality/quantity sorts, including native tie ordering. +Native descriptor construction, hashing, sorting, source-reference copying and nested cleanup execute. +Source backing, ordinary definitions, connection lookup and concrete recipe matching remain supplied interfaces. +The comparison checks counts, every round and reference, and exact matcher-call order. +See the [ingredient reference](../../../docs/native-player-inventory.md#ingredient-availability-rounds) for boundaries and source mappings. +Actual actor capture, concrete recipe admission/matching, complete availability evaluation, screen delivery, actions and pixels remain unfinished. + +The full four-project build passes 2,850 tests, with 163 optional skips and no failures or errors. +Checkstyle passes. All 1,483 core classes match the embedded add-on and proxy copies; all 1,191 client classes match the tested add-on. +The packaged counter matches all 755 controls and six focused regressions, and rejects six deliberate semantic changes. +Retained packaged search, output-unlock and progressive-model comparisons also pass. diff --git a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch index 542f4842..e6613e13 100644 --- a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 00b4acdf6d97ab3931f7d6ab1f031b5587e07be7 Mon Sep 17 00:00:00 2001 +From f0c42f6dfa61d40704757df31e736301dd165c41 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(trading): translate Bedrock merchant inventories in core @@ -878,7 +878,7 @@ index 000000000..ea4d95614 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 4cec1d2cb..37ced8c75 100644 +index 1f9574928..3c527af0a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -79,6 +79,7 @@ import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; @@ -1170,19 +1170,19 @@ index 8544e1ab3..829f807a6 100644 @Override diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java -index 6d498eac1..e0568afa5 100644 +index 6ea6cdaa1..0765fcb10 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryTracker.java -@@ -28,6 +28,8 @@ import net.raphimc.viabedrock.ViaBedrock; - import net.raphimc.viabedrock.api.chunk.section.BedrockChunkSection; - import net.raphimc.viabedrock.api.model.container.Container; +@@ -30,6 +30,8 @@ import net.raphimc.viabedrock.api.model.container.Container; import net.raphimc.viabedrock.api.model.container.ContainerOpenContext; + import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; + import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; +import net.raphimc.viabedrock.api.model.container.entity.MerchantContainer; +import net.raphimc.viabedrock.api.model.container.InputContainer; import net.raphimc.viabedrock.api.model.container.dynamic.BundleContainer; import net.raphimc.viabedrock.api.model.container.player.ArmorContainer; import net.raphimc.viabedrock.api.model.container.player.HudContainer; -@@ -305,6 +307,18 @@ public class InventoryTracker extends StoredObject { +@@ -310,6 +312,18 @@ public class InventoryTracker extends StoredObject { screens.tick(System.nanoTime()); } this.user().get(InventoryRequestTracker.class).tick(); @@ -1201,7 +1201,7 @@ index 6d498eac1..e0568afa5 100644 if (this.currentContainer != null && this.currentContainer.position() != null) { if (this.currentContainer.type() == ContainerType.INVENTORY) { return; -@@ -467,6 +481,11 @@ public class InventoryTracker extends StoredObject { +@@ -498,6 +512,11 @@ public class InventoryTracker extends StoredObject { } private void forceCloseCurrentContainer() { diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 8302da12..3aa48c7c 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From 07d653a4e82cf7b19d51e4d58ae91629fd04eca7 Mon Sep 17 00:00:00 2001 +From 410e041441aabf30c9f6488fa11fea13a8200b12 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): share bounded native UI programs and resource @@ -255,6 +255,13 @@ Complete original player search 143438780 and word matcher 143438da0 match game-type and standard-stream interfaces. Native formatting, normalization, validity, matching, vector allocation and cleanup execute. They do not prove production name acquisition, the full availability evaluator or pixels. + +Share the pinned STL sort through NativeSort so cooking priorities and native +ingredient groups retain the same comparator-equivalent ordering. Accept a +comparator directly instead of encoding multiple keys into a synthetic priority. +Keep the Microsoft STL license with the extracted algorithm. Existing cooking +order controls pass; ingredient cardinality/quantity order is checked against +complete original counter executions in the consuming recipe patch. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -276,7 +283,7 @@ production name acquisition, the full availability evaluator or pixels. .../api/resourcepack/ui/BrewingStandUi.java | 44 + .../api/resourcepack/ui/ContainerUi.java | 511 ++++++ .../resourcepack/ui/CookingNavigation.java | 83 + - .../resourcepack/ui/CookingRecipeOrder.java | 206 +++ + .../resourcepack/ui/CookingRecipeOrder.java | 62 + .../resourcepack/ui/CookingSearchText.java | 45 + .../api/resourcepack/ui/CookingUi.java | 433 +++++ .../api/resourcepack/ui/FormBindings.java | 290 ++++ @@ -297,6 +304,7 @@ production name acquisition, the full availability evaluator or pixels. .../api/resourcepack/ui/UiExpression.java | 308 ++++ .../api/resourcepack/ui/UiFonts.java | 241 +++ .../api/resourcepack/ui/UiTextures.java | 94 ++ + .../viabedrock/api/util/NativeSort.java | 168 ++ .../rewriter/ResourcePackRewriter.java | 2 + .../resourcepack/JsonUiResourceRewriter.java | 36 + .../protocol/storage/ResourcePackStorage.java | 41 +- @@ -338,7 +346,7 @@ production name acquisition, the full availability evaluator or pixels. .../api/resourcepack/ui/UiFontsTest.java | 192 +++ .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 82 files changed, 12932 insertions(+), 2 deletions(-) + 83 files changed, 12956 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -378,6 +386,7 @@ production name acquisition, the full availability evaluator or pixels. create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpression.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFonts.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/util/NativeSort.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/JsonUiResourceRewriter.java create mode 100644 src/main/resources/META-INF/licenses/utf8proc.txt create mode 100644 src/main/resources/assets/viabedrock/data/search_unicode_5_0.bin.gz @@ -2512,24 +2521,23 @@ index 000000000..3a96be76d +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeOrder.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeOrder.java new file mode 100644 -index 000000000..5d8e463b5 +index 000000000..e91da098a --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingRecipeOrder.java -@@ -0,0 +1,206 @@ +@@ -0,0 +1,62 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors + * SPDX-License-Identifier: GPL-3.0-or-later -+ * -+ * Sorting logic adapted from Microsoft STL, Copyright (c) Microsoft Corporation. -+ * Licensed under Apache-2.0 WITH LLVM-exception; see META-INF/licenses/Microsoft-STL.txt. + */ +package net.raphimc.viabedrock.api.resourcepack.ui; + ++import net.raphimc.viabedrock.api.util.NativeSort; ++ +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; -+import java.util.Collections; ++import java.util.Comparator; +import java.util.List; +import java.util.HashSet; +import java.util.Set; @@ -2567,9 +2575,7 @@ index 000000000..5d8e463b5 + } + + public static List sorted(final List entries, final ToIntFunction priority) { -+ final List result = new ArrayList<>(entries); -+ sort(result, priority, 0, result.size(), result.size()); -+ return List.copyOf(result); ++ return NativeSort.sorted(entries, Comparator.comparingInt(priority)); + } + + /** Native ungrouped collections expose a stable prefix of evaluator buckets 0, 1 and 2. */ @@ -2578,147 +2584,6 @@ index 000000000..5d8e463b5 + return RecipeCollection.visibleOrder(entries, evaluation, filtering); + } + -+ private static void sort(final List entries, final ToIntFunction priority, -+ int first, int last, int ideal) { -+ while (last - first > 32) { -+ if (ideal <= 0) { -+ heapSort(entries, priority, first, last); -+ return; -+ } -+ final int middle = first + (last - first) / 2; -+ final int end = last - 1; -+ if (end - first > 40) { -+ final int step = (last - first) / 8; -+ median(entries, priority, first, first + step, first + step * 2); -+ median(entries, priority, middle - step, middle, middle + step); -+ median(entries, priority, end - step * 2, end - step, end); -+ median(entries, priority, first + step, middle, end - step); -+ } else { -+ median(entries, priority, first, middle, end); -+ } -+ int pivotFirst = middle; -+ int pivotLast = middle + 1; -+ final int pivot = priority.applyAsInt(entries.get(middle)); -+ while (pivotFirst > first && priority.applyAsInt(entries.get(pivotFirst - 1)) == pivot) { -+ pivotFirst--; -+ } -+ while (pivotLast < last && priority.applyAsInt(entries.get(pivotLast)) == pivot) { -+ pivotLast++; -+ } -+ int upper = pivotLast; -+ int lower = pivotFirst; -+ while (true) { -+ while (upper < last) { -+ final int key = priority.applyAsInt(entries.get(upper)); -+ if (key < pivot) { -+ break; -+ } -+ if (key == pivot) { -+ Collections.swap(entries, pivotLast++, upper); -+ } -+ upper++; -+ } -+ while (lower > first) { -+ final int key = priority.applyAsInt(entries.get(lower - 1)); -+ if (key > pivot) { -+ break; -+ } -+ if (key == pivot) { -+ Collections.swap(entries, --pivotFirst, lower - 1); -+ } -+ lower--; -+ } -+ if (lower == first && upper == last) { -+ break; -+ } -+ if (lower == first) { -+ if (pivotLast != upper) { -+ Collections.swap(entries, pivotFirst, pivotLast); -+ } -+ pivotLast++; -+ Collections.swap(entries, pivotFirst++, upper++); -+ } else if (upper == last) { -+ Collections.swap(entries, --lower, --pivotFirst); -+ Collections.swap(entries, pivotFirst, --pivotLast); -+ } else { -+ Collections.swap(entries, upper++, --lower); -+ } -+ } -+ ideal = (ideal >> 1) + (ideal >> 2); -+ if (pivotFirst - first < last - pivotLast) { -+ sort(entries, priority, first, pivotFirst, ideal); -+ first = pivotLast; -+ } else { -+ sort(entries, priority, pivotLast, last, ideal); -+ last = pivotFirst; -+ } -+ } -+ for (int index = first + 1; index < last; index++) { -+ final T value = entries.get(index); -+ int hole = index; -+ while (hole > first && priority.applyAsInt(value) < priority.applyAsInt(entries.get(hole - 1))) { -+ entries.set(hole, entries.get(hole - 1)); -+ hole--; -+ } -+ entries.set(hole, value); -+ } -+ } -+ -+ private static void median(final List entries, final ToIntFunction priority, -+ final int first, final int middle, final int last) { -+ if (priority.applyAsInt(entries.get(middle)) < priority.applyAsInt(entries.get(first))) { -+ Collections.swap(entries, first, middle); -+ } -+ if (priority.applyAsInt(entries.get(last)) < priority.applyAsInt(entries.get(middle))) { -+ Collections.swap(entries, middle, last); -+ if (priority.applyAsInt(entries.get(middle)) < priority.applyAsInt(entries.get(first))) { -+ Collections.swap(entries, first, middle); -+ } -+ } -+ } -+ -+ private static void heapSort(final List entries, final ToIntFunction priority, -+ final int first, final int last) { -+ final int size = last - first; -+ for (int hole = size / 2; hole > 0; ) { -+ hole--; -+ reheap(entries, priority, first, hole, size, entries.get(first + hole)); -+ } -+ for (int remaining = size - 1; remaining > 0; remaining--) { -+ final T value = entries.get(first + remaining); -+ entries.set(first + remaining, entries.get(first)); -+ reheap(entries, priority, first, 0, remaining, value); -+ } -+ } -+ -+ private static void reheap(final List entries, final ToIntFunction priority, -+ final int first, int hole, final int bottom, final T value) { -+ final int top = hole; -+ final int lastParent = (bottom - 1) / 2; -+ int child = hole; -+ while (child < lastParent) { -+ child = child * 2 + 2; -+ if (priority.applyAsInt(entries.get(first + child)) < priority.applyAsInt(entries.get(first + child - 1))) { -+ child--; -+ } -+ entries.set(first + hole, entries.get(first + child)); -+ hole = child; -+ } -+ if (child == lastParent && bottom % 2 == 0) { -+ entries.set(first + hole, entries.get(first + bottom - 1)); -+ hole = bottom - 1; -+ } -+ while (hole > top) { -+ final int parent = (hole - 1) / 2; -+ if (priority.applyAsInt(entries.get(first + parent)) >= priority.applyAsInt(value)) { -+ break; -+ } -+ entries.set(first + hole, entries.get(first + parent)); -+ hole = parent; -+ } -+ entries.set(first + hole, value); -+ } -+ + private record RegistryEntry(T entry, byte[] key) { } + +} @@ -7899,6 +7764,180 @@ index 000000000..e597c1123 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/util/NativeSort.java b/src/main/java/net/raphimc/viabedrock/api/util/NativeSort.java +new file mode 100644 +index 000000000..20278d845 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/util/NativeSort.java +@@ -0,0 +1,168 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ * ++ * Sorting logic adapted from Microsoft STL, Copyright (c) Microsoft Corporation. ++ * Licensed under Apache-2.0 WITH LLVM-exception; see META-INF/licenses/Microsoft-STL.txt. ++ */ ++package net.raphimc.viabedrock.api.util; ++ ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.Comparator; ++import java.util.List; ++ ++/** The pinned native STL sort, including its ordering of comparator-equivalent entries. */ ++public final class NativeSort { ++ ++ private NativeSort() { } ++ ++ public static List sorted(final List entries, final Comparator order) { ++ final List result = new ArrayList<>(entries); ++ sort(result, order, 0, result.size(), result.size()); ++ return List.copyOf(result); ++ } ++ ++ private static void sort(final List entries, final Comparator order, ++ int first, int last, int ideal) { ++ while (last - first > 32) { ++ if (ideal <= 0) { ++ heapSort(entries, order, first, last); ++ return; ++ } ++ final int middle = first + (last - first) / 2; ++ final int end = last - 1; ++ if (end - first > 40) { ++ final int step = (last - first) / 8; ++ median(entries, order, first, first + step, first + step * 2); ++ median(entries, order, middle - step, middle, middle + step); ++ median(entries, order, end - step * 2, end - step, end); ++ median(entries, order, first + step, middle, end - step); ++ } else { ++ median(entries, order, first, middle, end); ++ } ++ int pivotFirst = middle; ++ int pivotLast = middle + 1; ++ final T pivot = entries.get(middle); ++ while (pivotFirst > first && order.compare(entries.get(pivotFirst - 1), pivot) == 0) { ++ pivotFirst--; ++ } ++ while (pivotLast < last && order.compare(entries.get(pivotLast), pivot) == 0) { ++ pivotLast++; ++ } ++ int upper = pivotLast; ++ int lower = pivotFirst; ++ while (true) { ++ while (upper < last) { ++ final int key = order.compare(entries.get(upper), pivot); ++ if (key < 0) { ++ break; ++ } ++ if (key == 0) { ++ Collections.swap(entries, pivotLast++, upper); ++ } ++ upper++; ++ } ++ while (lower > first) { ++ final int key = order.compare(entries.get(lower - 1), pivot); ++ if (key > 0) { ++ break; ++ } ++ if (key == 0) { ++ Collections.swap(entries, --pivotFirst, lower - 1); ++ } ++ lower--; ++ } ++ if (lower == first && upper == last) { ++ break; ++ } ++ if (lower == first) { ++ if (pivotLast != upper) { ++ Collections.swap(entries, pivotFirst, pivotLast); ++ } ++ pivotLast++; ++ Collections.swap(entries, pivotFirst++, upper++); ++ } else if (upper == last) { ++ Collections.swap(entries, --lower, --pivotFirst); ++ Collections.swap(entries, pivotFirst, --pivotLast); ++ } else { ++ Collections.swap(entries, upper++, --lower); ++ } ++ } ++ ideal = (ideal >> 1) + (ideal >> 2); ++ if (pivotFirst - first < last - pivotLast) { ++ sort(entries, order, first, pivotFirst, ideal); ++ first = pivotLast; ++ } else { ++ sort(entries, order, pivotLast, last, ideal); ++ last = pivotFirst; ++ } ++ } ++ for (int index = first + 1; index < last; index++) { ++ final T value = entries.get(index); ++ int hole = index; ++ while (hole > first && order.compare(value, entries.get(hole - 1)) < 0) { ++ entries.set(hole, entries.get(hole - 1)); ++ hole--; ++ } ++ entries.set(hole, value); ++ } ++ } ++ ++ private static void median(final List entries, final Comparator order, ++ final int first, final int middle, final int last) { ++ if (order.compare(entries.get(middle), entries.get(first)) < 0) { ++ Collections.swap(entries, first, middle); ++ } ++ if (order.compare(entries.get(last), entries.get(middle)) < 0) { ++ Collections.swap(entries, middle, last); ++ if (order.compare(entries.get(middle), entries.get(first)) < 0) { ++ Collections.swap(entries, first, middle); ++ } ++ } ++ } ++ ++ private static void heapSort(final List entries, final Comparator order, ++ final int first, final int last) { ++ final int size = last - first; ++ for (int hole = size / 2; hole > 0; ) { ++ hole--; ++ reheap(entries, order, first, hole, size, entries.get(first + hole)); ++ } ++ for (int remaining = size - 1; remaining > 0; remaining--) { ++ final T value = entries.get(first + remaining); ++ entries.set(first + remaining, entries.get(first)); ++ reheap(entries, order, first, 0, remaining, value); ++ } ++ } ++ ++ private static void reheap(final List entries, final Comparator order, ++ final int first, int hole, final int bottom, final T value) { ++ final int top = hole; ++ final int lastParent = (bottom - 1) / 2; ++ int child = hole; ++ while (child < lastParent) { ++ child = child * 2 + 2; ++ if (order.compare(entries.get(first + child), entries.get(first + child - 1)) < 0) { ++ child--; ++ } ++ entries.set(first + hole, entries.get(first + child)); ++ hole = child; ++ } ++ if (child == lastParent && bottom % 2 == 0) { ++ entries.set(first + hole, entries.get(first + bottom - 1)); ++ hole = bottom - 1; ++ } ++ while (hole > top) { ++ final int parent = (hole - 1) / 2; ++ if (order.compare(entries.get(first + parent), value) >= 0) { ++ break; ++ } ++ entries.set(first + hole, entries.get(first + parent)); ++ hole = parent; ++ } ++ entries.set(first + hole, value); ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ResourcePackRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ResourcePackRewriter.java index 245bb4494..88eb0f0c3 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ResourcePackRewriter.java diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 7e3b5645..1038c17a 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -533,3 +533,13 @@ The [player search reference](../../../docs/native-player-inventory.md#player-se All four builds pass with 2,842 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. All 1,472 core classes match both embedded copies; all 1,191 client classes match the tested add-on. The actual player availability evaluator, authoritative name acquisition, installed-screen search and pixels remain unfinished. + + +## Shared native sort + +Extract the pinned STL algorithm into `NativeSort` with an explicit comparator. +Cooking priorities retain their existing behavior, including the native ordering of equal priorities. +The deferred crafting consumer uses the same algorithm for ingredient cardinality and quantity. +Keep the Microsoft STL license beside the extracted implementation. +Existing cooking-order tests pass. The consuming patch compares large pair sorts against complete original counter executions. +This extraction does not install the player availability evaluator or change screen admission. From 69f2b702d636bff806c7877dfc395560fcdfe96b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:07:29 +0200 Subject: [PATCH 09/12] feat(crafting): bind resolved native recipe ingredients Compose resolved ordinary descriptors with the actual current-owner inventory counter. Preserve typed enchantment/lock guards, empty-descriptor userdata rules, wildcard auxiliaries, associated Block ownership and first-stack hash caching. Share source eligibility with cooking while keeping grid matching separate. Remove the catalog patch's superseded List import from its owning patch. Original Item construction and concrete shaped/shapeless matching provide 1443 direct matches and 260 complete counter controls. Packaged comparisons pass and reject six deliberate semantic changes. All four builds pass with 2854 tests and 163 optional skips, with matching embedded classes. Deferred names can resolve through alternate alias descriptors or block fallback. Keep that loader/admission boundary explicit instead of treating wire names as ordinary keys. Complete availability, installed screens and original reports remain unfinished. --- docs/native-player-inventory.md | 53 +++ ...edrock-recipes-and-place-ingredients.patch | 411 +++++++++++++++++- ...edrock-recipes-and-place-ingredients.pr.md | 24 + ...ain-bedrock-creative-catalog-entries.patch | 20 +- 4 files changed, 473 insertions(+), 35 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 7e114b28..9358fac4 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -599,3 +599,56 @@ All four projects build with 2,850 passing tests, 163 optional skips and no fail Checkstyle passes after full replay. All 1,483 core classes match both embedded copies; all 1,191 client classes match the tested add-on. The four original reports and all five goal areas remain active. + + +## Resolved ingredient matching + +`RecipeIngredientMatcher` implements the shared native Recipe matcher after ordinary descriptors resolve. +`ItemRewriter.matchesResolvedRecipeIngredient` supplies connection descriptor conversion and canonical associated block ownership. +`countResolvedRecipeInputs` combines that matcher with current-owner snapshots and the verified counter. +The input vector retains null keys as empty ingredient positions. The counter still requires at least one nonempty ingredient. +Outer ingredient quantities do not change this resolved matcher or divide counter allocations. + +The source guard runs before the matcher converts the stack. +An `ench` ListTag rejects the source, including an empty list. Other `ench` tag types do not activate that guard. +A nonzero `minecraft:item_lock` ByteTag also rejects the source. Zero ByteTags and other tag types do not activate that guard. +Cooking now shares these guards through the same helper. + +An empty ingredient matches an empty source descriptor only when source userdata is absent. +A present empty CompoundTag remains distinct from absent userdata in this matcher. +Nonempty descriptors retain wildcard auxiliaries and canonical associated block comparison. +These rules belong to Recipe source matching. The separate shaped and shapeless grid matchers use different paths and keep their existing behavior. + +The counter still caches the first nonzero stack's match result for each descriptor hash. +If that first stack is enchanted or locked, later ordinary stacks with the same hash remain unavailable. +If the first stack passes, later enchanted or locked stacks with that hash still contribute their quantities and source references. +Repeated source eligibility checks would change native counts. + +The original shaped factory `0x1432697f0` installs vtable `0x14e872280`. +The shapeless factory `0x14326a0a0` installs vtable `0x14e872300`. +Both tables select source matcher `0x14297a2e0` and descriptor comparison `0x14297a710`. +Both return true from virtual slot `0x50`, which activates output validation in registration `0x14324b590`. +This table evidence does not establish complete wire admission or every recipe class. + +The reference executes the original Item constructor before it supplies typed userdata. +That constructor clears the Item reference at zero quantity. Earlier raw zero-quantity seeds did not represent that construction and were corrected. +Original CompoundTag lookups `0x142548a90` and `0x142548b90`, descriptor construction/comparison, and the complete counter execute. +The capture contains 1,443 direct matches and 260 counter controls with 496 rounds and 3,305 matcher calls. +Thirty-nine shapeless controls also match the shared path. +Separate Java tests cover lazy conversion after source guards, real owner snapshots, empty positions and canonical associated block ownership. + +The harness supplies typed tag trees and metatype getters, stable ordinary definitions, registry lookup, source model backing and allocator/CRT interfaces. +Temporary native descriptor and counter allocations return to their baseline. +The capture does not execute binary NBT loading, complete source Item retirement, native actor/provider capture, installed screens, actions or pixels. + +The packaged core matches all 1,703 direct/counter controls and the three focused integration regressions. +Six deliberate regressions fail value comparisons: ignored enchantment lists, rejected non-list enchantment tags, rejected zero locks, ignored empty userdata, exact-only metadata and repeated source eligibility. +The retained 755 counter controls, 2,660 searches, 1,671 unlock/update comparisons and 508 progressive controls across 9,438 frames also pass. +All four builds pass with 2,854 tests, 163 optional skips and no failures or errors. Checkstyle passes. +All 1,485 core classes match both embedded copies; all 1,191 client classes match the tested add-on. + +Deferred wire names require another resolution stage before this API can consume their results. +Original resolver `0x142522910` can select a runtime alias descriptor or fall back to a block definition. +Treating every wire name as an ordinary Item key would skip those branches and can misclassify empty ingredients. +Deferred names, alias/tag/Molang payloads, full wire admission, actual recipe contexts and installed availability remain unfinished. +The four original reports and all five goal areas remain active. diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 6660f83e..6d89e957 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From 2fe27fb22c92a866412938d26c2dfdfa0fd3847d Mon Sep 17 00:00:00 2001 +From 329e219596dc460e8b903a299f6e1b6a697c5d77 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -451,17 +451,39 @@ connection definition lookup and concrete recipe matching remain supplied seams. Actor capture, concrete recipe admission/matching, full evaluator installation, screen delivery, actions and pixels remain unfinished. Targeted Java comparisons retain count, every round and source reference, and exact matcher-call order. + +Bind already resolved ordinary ingredient descriptors to the native Recipe source +matcher. Reject an ench ListTag and a nonzero item_lock ByteTag before source +conversion. Preserve empty descriptor matching's distinction between absent and +present userdata, and canonical associated Block ownership. Share source guards +with cooking without applying those guards to the separate grid matcher. + +Count resolved ingredient vectors through the actual current-owner source bridge; +retain empty positions and leave outer quantities outside this count routine. +Wire names are not all ordinary keys: native deferred name resolution can select +alias descriptors or block fallback. Keep that loader/admission boundary explicit +instead of inferring emptiness or substituting the existing wire-name matcher. + +Original shaped/shapeless tables14e872280/14e872300 share matcher14297a2e0 and +comparator14297a710. Actual Item constructor14247ead0, CompoundTag lookups, source +conversion, descriptor comparison and full counter execute in1443 direct cases +and260 counter controls. Typed tag trees/metatype getters, ordinary definitions, +registry lookup, model backing and allocator/CRT remain supplied boundaries. +The target constructor clears Item references at zero quantity; raw source seeds +must not preserve those definitions. Deferred/tag/Molang resolution, full wire +admission, actor capture, installed availability, actions and pixels remain open. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + .../model/container/CookingRecipeCatalog.java | 81 ++ .../model/container/CookingRecipeInputs.java | 53 ++ - .../model/container/CookingRecipeSources.java | 87 ++ + .../model/container/CookingRecipeSources.java | 80 ++ .../model/container/CraftingOutputPlan.java | 109 +++ .../model/container/CraftingPlacement.java | 229 +++++ .../container/FurnaceRecipePlacement.java | 219 +++++ .../NativeCookingRecipeSelection.java | 206 +++++ .../model/container/NativeRecipeCounter.java | 168 ++++ + .../container/RecipeIngredientMatcher.java | 38 + .../block/CraftingTableContainer.java | 100 ++- .../container/block/FurnaceContainer.java | 209 +++++ .../model/container/player/HudContainer.java | 3 + @@ -481,7 +503,7 @@ retain count, every round and source reference, and exact matcher-call order. .../protocol/packet/InventoryPackets.java | 146 +++- .../protocol/packet/JoinPackets.java | 1 + .../protocol/packet/MultiStatePackets.java | 55 ++ - .../protocol/rewriter/ItemRewriter.java | 59 +- + .../protocol/rewriter/ItemRewriter.java | 94 ++- .../protocol/storage/CraftingDataStorage.java | 3 +- .../protocol/storage/CraftingDataTracker.java | 626 ++++++++++++-- .../storage/InventoryOptionsStorage.java | 187 ++++ @@ -508,6 +530,7 @@ retain count, every round and source reference, and exact matcher-call order. .../packet/FurnaceOptionsPacketTest.java | 162 ++++ .../packet/InventoryOptionsPacketTest.java | 152 ++++ .../NativeFurnaceOptionsPacketTest.java | 214 +++++ + .../ResolvedRecipeIngredientTest.java | 256 ++++++ .../storage/CookingRecipeBookTest.java | 154 ++++ .../protocol/storage/CraftingCursorTest.java | 158 ++++ .../storage/CraftingDataTrackerTest.java | 409 +++++++++ @@ -520,7 +543,7 @@ retain count, every round and source reference, and exact matcher-call order. .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 81 +- - 68 files changed, 8544 insertions(+), 161 deletions(-) + 70 files changed, 8866 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -531,6 +554,7 @@ retain count, every round and source reference, and exact matcher-call order. create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeCookingRecipeSelection.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQuery.java @@ -555,6 +579,7 @@ retain count, every round and source reference, and exact matcher-call order. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/FurnaceOptionsPacketTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryOptionsPacketTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/NativeFurnaceOptionsPacketTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CookingRecipeBookTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingCursorTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTrackerTest.java @@ -801,10 +826,10 @@ index 000000000..d35a44bb8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeSources.java b/src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeSources.java new file mode 100644 -index 000000000..b2409a86a +index 000000000..c531ebe68 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeSources.java -@@ -0,0 +1,87 @@ +@@ -0,0 +1,80 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -812,8 +837,6 @@ index 000000000..b2409a86a + */ +package net.raphimc.viabedrock.api.model.container; + -+import com.viaversion.nbt.tag.ByteTag; -+import com.viaversion.nbt.tag.ListTag; +import net.raphimc.viabedrock.api.resourcepack.ui.CookingRecipeOrder; +import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; @@ -847,7 +870,7 @@ index 000000000..b2409a86a + final ItemRecipeKey key = identity.apply(item); + final int hash = key != null ? key.descriptorHash() : 0; + final Group group = groups.computeIfAbsent(hash, ignored -> new Group<>( -+ key != null && eligible(item) && matches.test(item))); ++ key != null && RecipeIngredientMatcher.eligible(item) && matches.test(item))); + if (group.accepted) { + group.sources.add(new Source<>(container, slot)); + group.amount += item.amount(); @@ -866,11 +889,6 @@ index 000000000..b2409a86a + return new Result<>(sources, amount); + } + -+ private static boolean eligible(final BedrockItem item) { -+ return item.tag() == null || !(item.tag().get("ench") instanceof ListTag) -+ && !(item.tag().get("minecraft:item_lock") instanceof ByteTag lock && lock.asByte() != 0); -+ } -+ + public record Source(C container, int slot) { } + + public record Result(List> sources, int amount) { @@ -1853,6 +1871,50 @@ index 000000000..27d44cefc + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java b/src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java +new file mode 100644 +index 000000000..1bce19556 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java +@@ -0,0 +1,38 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import com.viaversion.nbt.tag.ByteTag; ++import com.viaversion.nbt.tag.ListTag; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; ++ ++import java.util.function.Function; ++import java.util.function.IntFunction; ++ ++/** Native Recipe source matching after ordinary ingredient descriptors resolve. */ ++public final class RecipeIngredientMatcher { ++ ++ private RecipeIngredientMatcher() { } ++ ++ public static boolean eligible(final BedrockItem source) { ++ return source.tag() == null || !(source.tag().get("ench") instanceof ListTag) ++ && !(source.tag().get("minecraft:item_lock") instanceof ByteTag lock && lock.asByte() != 0); ++ } ++ ++ /** A null key represents the native empty descriptor, independently of its outer quantity. */ ++ public static boolean matches(final ItemRecipeKey ingredient, final BedrockItem source, ++ final Function descriptor, ++ final IntFunction ingredientBlocks) { ++ if (!eligible(source)) { ++ return false; ++ } ++ final ItemRecipeKey candidate = descriptor.apply(source); ++ return ingredient == null ? candidate == null && source.tag() == null ++ : ingredient.matches(candidate, ingredientBlocks); ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java index fec81fe6c..178d1ef15 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java @@ -3279,10 +3341,10 @@ index 0b8c600e9..4fef2c18d 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 26915529d..f83bb23aa 100644 +index 26915529d..fd460b8c4 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -40,9 +40,11 @@ import com.viaversion.viaversion.util.Key; +@@ -40,9 +40,12 @@ import com.viaversion.viaversion.util.Key; import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.api.model.BlockState; import net.raphimc.viabedrock.api.model.NativeItemNames; @@ -3291,11 +3353,12 @@ index 26915529d..f83bb23aa 100644 import net.raphimc.viabedrock.api.model.NativeItemStackData; +import net.raphimc.viabedrock.api.model.container.NativeItemStackMatcher; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.RecipeIngredientMatcher; +import net.raphimc.viabedrock.api.model.container.Container; import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -55,6 +57,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; +@@ -55,6 +58,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; import net.raphimc.viabedrock.protocol.model.ItemStackHeader; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; @@ -3303,7 +3366,7 @@ index 26915529d..f83bb23aa 100644 import net.raphimc.viabedrock.protocol.model.ItemStackingProperties; import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; import net.raphimc.viabedrock.protocol.model.ItemCooldown; -@@ -65,12 +68,14 @@ import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; +@@ -65,16 +69,21 @@ import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomAttachableResourceRewriter; import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomItemTextureResourceRewriter; import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; @@ -3318,7 +3381,14 @@ index 26915529d..f83bb23aa 100644 import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackDescriptorType; import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; -@@ -102,6 +107,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ++import java.util.ArrayList; ++import java.util.Collections; + import java.util.HashMap; ++import java.util.List; + import java.util.HashSet; + import java.util.Map; + import java.util.Objects; +@@ -102,6 +111,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Set componentItems; private final Int2ObjectMap blockItemValidBlockStates; private final Type itemType; @@ -3327,7 +3397,7 @@ index 26915529d..f83bb23aa 100644 private final Type optionalItemType; private final Type itemArrayType; private final Type newItemType; -@@ -189,6 +196,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -189,6 +200,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex this.items.getOrDefault("minecraft:shield", 0), false, this); @@ -3336,7 +3406,7 @@ index 26915529d..f83bb23aa 100644 this.optionalItemType = new OptionalType<>(this.itemType); this.itemArrayType = new ArrayType<>(this.itemType, BedrockTypes.UNSIGNED_VAR_INT); this.newItemType = -@@ -500,6 +509,16 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -500,6 +513,47 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? this.user().get(BlockStateRewriter.class).blockItemRecipeDescriptor(item.blockRuntimeId()) : null); } @@ -3349,11 +3419,42 @@ index 26915529d..f83bb23aa 100644 + return key != null ? key.descriptorHash() : 0; + }); + } ++ ++ /** Count already resolved ordinary descriptors. Wire names, tags and Molang need their own resolution. */ ++ public NativeRecipeCounter.Result countResolvedRecipeInputs(final List ingredients, ++ final Container owner, final boolean includeCursor) { ++ // Null entries retain empty ingredient positions. ++ final List snapshot = Collections.unmodifiableList(new ArrayList<>(ingredients)); ++ return this.countRecipeInputs(new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return snapshot.size(); ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ return snapshot.get(index) == null; ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ return ItemRewriter.this.matchesResolvedRecipeIngredient(snapshot.get(index), item); ++ } ++ }, owner, includeCursor); ++ } ++ ++ public boolean matchesResolvedRecipeIngredient(final ItemRecipeKey ingredient, final BedrockItem source) { ++ final var definition = ingredient != null ? this.registry.definition((short) ingredient.identifier()) : null; ++ final String association = definition != null ++ ? this.cookingProperties.getOrDefault(definition.identifier(), ItemCookingProperties.NONE).blockIdentifier() : null; ++ return RecipeIngredientMatcher.matches(ingredient, source, this::recipeKey, auxiliary -> association != null ++ ? this.user().get(BlockStateRewriter.class).blockItemState(association, (short) auxiliary) : null); ++ } + public boolean matchesRecipeItem(final String identifier, final int metadata, final BedrockItem item) { final ItemRecipeKey query = this.registry.namedRecipeKey(identifier, metadata); if (query == null || item == null || item.isEmpty()) { -@@ -515,6 +534,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -515,6 +569,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? blocks.blockItemState(queryAssociation, (short) auxiliary) : null); } @@ -3375,7 +3476,7 @@ index 26915529d..f83bb23aa 100644 public ItemRegistry getItemRegistry() { return this.registry; } -@@ -539,6 +573,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -539,6 +608,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return NativeItemStackMatcher.matchesIdentity(query, candidate, blocks::blockState, blocks::restrictionBlocks); } @@ -3397,7 +3498,7 @@ index 26915529d..f83bb23aa 100644 public boolean canMerge(final BedrockItem source, final BedrockItem destination) { if (destination == null || destination.isEmpty()) { return false; -@@ -582,6 +631,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -582,6 +666,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.itemType; } @@ -7445,6 +7546,268 @@ index 000000000..d66d6110c + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java +new file mode 100644 +index 000000000..062fae661 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java +@@ -0,0 +1,256 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.rewriter; ++ ++import com.viaversion.nbt.tag.*; ++import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.RecipeIngredientMatcher; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; ++import net.raphimc.viabedrock.api.model.BedrockBlockState; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; ++import net.raphimc.viabedrock.protocol.storage.InventoryTracker; ++import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++import org.mockito.Mockito; ++ ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.zip.GZIPInputStream; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class ResolvedRecipeIngredientTest { ++ ++ private static final List COLLECTIONS = List.of(ContainerEnumName.CraftingInputContainer, ++ ContainerEnumName.HotbarContainer, ContainerEnumName.InventoryContainer, ContainerEnumName.CursorContainer); ++ private static final List NAMES = List.of("crafting_input_items", "hotbar_items", "inventory_items", "cursor_items"); ++ ++ @Test ++ void rejectsTypedSourceGuardsBeforeDescriptorConversion() { ++ final var lookups = new AtomicInteger(); ++ final var source = new BedrockItem(1); ++ final var tag = new CompoundTag(); ++ tag.put("ench", new ListTag<>(IntTag.class)); ++ source.setTag(tag); ++ assertFalse(RecipeIngredientMatcher.matches(new ItemRecipeKey(1, (short) 0), source, ++ ignored -> { ++ lookups.incrementAndGet(); ++ return null; ++ }, ignored -> null)); ++ assertEquals(0, lookups.get()); ++ tag.remove("ench"); ++ tag.putByte("minecraft:item_lock", (byte) -1); ++ assertFalse(RecipeIngredientMatcher.matches(null, source, ++ ignored -> { ++ lookups.incrementAndGet(); ++ return null; ++ }, ignored -> null)); ++ assertEquals(0, lookups.get()); ++ } ++ ++ @Test ++ void composesResolvedIngredientsWithCurrentOwnerSourcesAndRetainsEmptyPositions() throws Exception { ++ final var user = Mockito.mock(UserConnection.class); ++ final var inventory = new InventoryTracker(user); ++ Mockito.when(user.get(InventoryTracker.class)).thenReturn(inventory); ++ final var requests = Mockito.mock(InventoryRequestTracker.class); ++ Mockito.when(requests.isPredictingClick()).thenReturn(true); ++ Mockito.when(user.get(InventoryRequestTracker.class)).thenReturn(requests); ++ final var items = items(user); ++ final var owner = inventory.getInventoryContainer(); ++ inventory.setCurrentContainer(owner); ++ final var grid = new BedrockItem(1, (short) 0, (byte) 4); ++ final var hotbar = new BedrockItem(1, (short) 0, (byte) 7); ++ inventory.getHudContainer().setItem(28, grid); ++ owner.setItem(0, hotbar); ++ final var ingredient = new ItemRecipeKey(1, (short) 0); ++ final var result = items.countResolvedRecipeInputs(Arrays.asList(ingredient, null, ingredient), owner, false); ++ assertEquals(5, result.count()); ++ assertEquals(List.of(6, 0, 5), result.rounds().getFirst().stream().map(NativeRecipeCounter.IngredientCount::amount).toList()); ++ final var enchanted = new CompoundTag(); ++ enchanted.put("ench", new ListTag<>(IntTag.class)); ++ grid.setTag(enchanted); ++ assertEquals(0, items.countResolvedRecipeInputs(List.of(ingredient), owner, false).count()); ++ grid.setTag(null); ++ hotbar.setTag(enchanted); ++ assertEquals(11, items.countResolvedRecipeInputs(List.of(ingredient), owner, false).count()); ++ assertThrows(IllegalArgumentException.class, () -> items.countResolvedRecipeInputs(Arrays.asList(null, null), owner, false)); ++ } ++ ++ @Test ++ void bindsResolvedIngredientsToCanonicalAssociatedBlockOwnership() throws Exception { ++ final var user = Mockito.mock(UserConnection.class); ++ final var items = items(user); ++ final var properties = ItemRewriter.class.getDeclaredField("cookingProperties"); ++ properties.setAccessible(true); ++ properties.set(items, Map.of("test:ordinary_1", new ItemCookingProperties(false, "test:block", false))); ++ final var blocks = Mockito.mock(BlockStateRewriter.class); ++ Mockito.when(user.get(BlockStateRewriter.class)).thenReturn(blocks); ++ final var state = Mockito.mock(BedrockBlockState.class); ++ Mockito.when(blocks.blockItemState("test:block", (short) 0)).thenReturn(state); ++ Mockito.when(blocks.blockItemState("test:block", (short) 3)).thenReturn(state); ++ final var source = new BedrockItem(1, (short) 3, (byte) 7); ++ assertTrue(items.matchesResolvedRecipeIngredient(new ItemRecipeKey(1, (short) 0), source)); ++ Mockito.when(blocks.blockItemState("test:block", (short) 3)).thenReturn(Mockito.mock(BedrockBlockState.class)); ++ assertFalse(items.matchesResolvedRecipeIngredient(new ItemRecipeKey(1, (short) 0), source)); ++ assertTrue(items.matchesResolvedRecipeIngredient(new ItemRecipeKey(1, Short.MAX_VALUE), source)); ++ assertFalse(items.matchesResolvedRecipeIngredient(new ItemRecipeKey(2, (short) 0), source)); ++ } ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_RECIPE_MATCHER", matches = ".+") ++ void matchesConcreteNativeRecipesAndFullCounterComposition() throws Exception { ++ try (var input = new InputStreamReader(new GZIPInputStream(Files.newInputStream( ++ Path.of(System.getenv("STACKANVIL_NATIVE_RECIPE_MATCHER")))), StandardCharsets.UTF_8)) { ++ verify(JsonParser.parseReader(input).getAsJsonObject()); ++ } ++ } ++ ++ static int verify(final JsonObject fixture) throws Exception { ++ final var items = items(Mockito.mock(UserConnection.class)); ++ final var rows = fixture.getAsJsonArray("rows"); ++ assertFalse(rows.isEmpty()); ++ for (final var element : rows) { ++ final var row = element.getAsJsonObject(); ++ assertEquals(row.get("matches").getAsBoolean(), items.matchesResolvedRecipeIngredient( ++ ingredient(row.getAsJsonObject("ingredient")), item(row.getAsJsonObject("item"))), row::toString); ++ } ++ for (final var element : fixture.getAsJsonArray("counts")) { ++ final var row = element.getAsJsonObject(); ++ final List ingredients = new ArrayList<>(); ++ for (final var value : row.getAsJsonArray("ingredients")) { ++ ingredients.add(ingredient(value.getAsJsonObject())); ++ } ++ final List sources = new ArrayList<>(); ++ final var rawSources = row.getAsJsonArray("sources"); ++ for (int index = 0; index < rawSources.size(); index++) { ++ final List stacks = new ArrayList<>(); ++ for (final var value : rawSources.get(index).getAsJsonArray()) { ++ final var stack = item(value.getAsJsonObject()); ++ stack.setNetId(index * 1000 + stacks.size()); ++ stacks.add(stack); ++ } ++ sources.add(new NativeRecipeCounter.Source(COLLECTIONS.get(index), stacks)); ++ } ++ final List matches = new ArrayList<>(); ++ final var recipe = new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return ingredients.size(); ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ return ingredients.get(index) == null; ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem stack) { ++ final boolean result = items.matchesResolvedRecipeIngredient(ingredients.get(index), stack); ++ matches.add(new Match(index, stack.netId() / 1000, stack.netId() % 1000, result)); ++ return result; ++ } ++ }; ++ final var result = NativeRecipeCounter.count(recipe, sources, stack -> { ++ final var key = items.recipeKey(stack); ++ return key != null ? key.descriptorHash() : 0; ++ }); ++ final List> rounds = new ArrayList<>(); ++ for (final var batch : row.getAsJsonArray("batches")) { ++ final List round = new ArrayList<>(); ++ for (final var entry : batch.getAsJsonArray()) { ++ final var value = entry.getAsJsonObject(); ++ final List locations = new ArrayList<>(); ++ for (final var location : value.getAsJsonArray("sources")) { ++ final var ref = location.getAsJsonObject(); ++ locations.add(new NativeRecipeCounter.Location(COLLECTIONS.get(NAMES.indexOf(ref.get("collection").getAsString())), ref.get("slot").getAsInt())); ++ } ++ round.add(new NativeRecipeCounter.IngredientCount(value.get("amount").getAsInt(), locations)); ++ } ++ rounds.add(round); ++ } ++ assertEquals(new NativeRecipeCounter.Result(row.get("count").getAsInt(), rounds), result, row::toString); ++ final List expected = new ArrayList<>(); ++ for (final var match : row.getAsJsonArray("matches")) { ++ final var value = match.getAsJsonObject(); ++ expected.add(new Match(value.get("ingredient").getAsInt(), value.getAsJsonArray("source").get(0).getAsInt(), ++ value.getAsJsonArray("source").get(1).getAsInt(), value.get("result").getAsBoolean())); ++ } ++ assertEquals(expected, matches, row::toString); ++ } ++ return rows.size() + fixture.getAsJsonArray("counts").size(); ++ } ++ ++ private static ItemRewriter items(final UserConnection user) throws Exception { ++ final var result = Mockito.mock(ItemRewriter.class, Mockito.CALLS_REAL_METHODS); ++ Mockito.doReturn(user).when(result).user(); ++ final var entries = new ItemEntry[4]; ++ for (int index = 0; index < entries.length; index++) { ++ entries[index] = new ItemEntry("test:ordinary_" + (index + 1), index + 1, false, ItemVersion.Legacy, null); ++ } ++ for (final var value : Map.of("registry", new ItemRegistry(entries), "cookingProperties", Map.of()).entrySet()) { ++ final var field = ItemRewriter.class.getDeclaredField(value.getKey()); ++ field.setAccessible(true); ++ field.set(result, value.getValue()); ++ } ++ return result; ++ } ++ ++ private static ItemRecipeKey ingredient(final JsonObject value) { ++ return value.has("empty") && value.get("empty").getAsBoolean() ? null ++ : new ItemRecipeKey(integer(value, "id", 1), (short) integer(value, "meta", 0)); ++ } ++ ++ private static BedrockItem item(final JsonObject value) { ++ final var result = new BedrockItem(integer(value, "id", 1), (short) integer(value, "meta", 0), (byte) integer(value, "count", 1)); ++ if (value.has("tag") && !value.get("tag").isJsonNull()) { ++ final var tag = new CompoundTag(); ++ for (final var entry : value.getAsJsonObject("tag").entrySet()) { ++ final int kind = entry.getValue().getAsJsonArray().get(0).getAsInt(); ++ final int scalar = entry.getValue().getAsJsonArray().get(1).getAsInt(); ++ tag.put(entry.getKey(), switch (kind) { ++ case 1 -> new ByteTag((byte) scalar); ++ case 2 -> new ShortTag((short) scalar); ++ case 3 -> new IntTag(scalar); ++ case 4 -> new LongTag(scalar); ++ case 5 -> new FloatTag(scalar); ++ case 6 -> new DoubleTag(scalar); ++ case 7 -> new ByteArrayTag(new byte[0]); ++ case 8 -> new StringTag(""); ++ case 9 -> new ListTag<>(IntTag.class); ++ case 10 -> new CompoundTag(); ++ case 11 -> new IntArrayTag(new int[0]); ++ default -> throw new IllegalArgumentException("Unsupported native fixture tag type"); ++ }); ++ } ++ result.setTag(tag); ++ } ++ return result; ++ } ++ ++ private static int integer(final JsonObject value, final String name, final int fallback) { ++ return value.has(name) ? value.get(name).getAsInt() : fallback; ++ } ++ ++ private record Match(int ingredient, int source, int slot, boolean result) { } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/CookingRecipeBookTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/CookingRecipeBookTest.java new file mode 100644 index 000000000..c0e69d179 diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index 0124dd47..e6225693 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -464,3 +464,27 @@ The full four-project build passes 2,850 tests, with 163 optional skips and no f Checkstyle passes. All 1,483 core classes match the embedded add-on and proxy copies; all 1,191 client classes match the tested add-on. The packaged counter matches all 755 controls and six focused regressions, and rejects six deliberate semantic changes. Retained packaged search, output-unlock and progressive-model comparisons also pass. + + +## Resolved Recipe source matcher + +Bind already resolved ordinary descriptors to native Recipe source matching and current-owner counter snapshots. +Reject an `ench` ListTag or nonzero `minecraft:item_lock` ByteTag before the matcher's source conversion. +Retain absent versus empty userdata for empty descriptors, wildcard metadata and canonical associated block ownership. +Share source guards with cooking. Keep grid matching on its separate path. +The first nonzero stack still controls eligibility for every later source with its descriptor hash. + +The original shaped and shapeless factories install the same matcher and comparator. +Actual Item construction, typed CompoundTag lookups, descriptor comparisons and the complete counter execute in the new reference. +It covers 1,443 direct matches and 260 counter controls, including 496 rounds and 3,305 matcher calls. +Thirty-nine shapeless controls also match. Typed metatype getters, ordinary definitions, registry lookup, source backing and allocator/CRT remain supplied interfaces. +The original constructor clears Item references at zero quantity; the fixture now executes that construction before typed userdata injection. + +`countResolvedRecipeInputs` accepts resolved descriptors with explicit empty positions. +Deferred wire names can select aliases or block fallback. Their resolution, tag/Molang payloads, complete wire admission and installed availability remain unfinished. +See the [resolved ingredient reference](../../../docs/native-player-inventory.md#resolved-ingredient-matching) for evidence and limits. + +The full four-project build passes 2,854 tests, with 163 optional skips and no failures or errors. Checkstyle passes. +All 1,485 core classes match both embedded copies; all 1,191 client classes match the tested add-on. +The packaged matcher passes 1,703 direct/counter controls and three focused integration regressions, and rejects six deliberate semantic changes. +All retained counter, search, output-unlock and progressive-model comparisons also pass. diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index 6072fe37..b182ef74 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From ed6445070fa141da6d14d4778cfc1cbb4dd4d269 Mon Sep 17 00:00:00 2001 +From 693589d7a931c55ea726ccf5a2249f5c2ef473fe Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -103,12 +103,12 @@ full screen ownership and installed-screen frame delivery remain open. .../protocol/model/CreativeCatalog.java | 266 ++++++++++++ .../model/CreativeInventoryModel.java | 193 +++++++++ .../protocol/packet/InventoryPackets.java | 198 ++++++--- - .../protocol/rewriter/ItemRewriter.java | 39 +- + .../protocol/rewriter/ItemRewriter.java | 38 +- .../storage/InventoryRequestTracker.java | 24 ++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++++++++ .../protocol/packet/InventoryPacketsTest.java | 266 +++++++++--- .../storage/InventoryRequestTrackerTest.java | 83 ++++ - 8 files changed, 1315 insertions(+), 136 deletions(-) + 8 files changed, 1314 insertions(+), 136 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java @@ -849,10 +849,10 @@ index a1a76a4d9..1f9574928 100644 public static boolean handleLegacyOffhandClick(final UserConnection user, final Container container, diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index d1e8b3c38..0176f672c 100644 +index 7942f081e..6a283a28c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -50,6 +50,8 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; +@@ -53,6 +53,8 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; import net.raphimc.viabedrock.protocol.data.ProtocolConstants; import net.raphimc.viabedrock.protocol.data.generated.bedrock.CustomItemTags; import net.raphimc.viabedrock.protocol.model.BedrockItem; @@ -861,11 +861,9 @@ index d1e8b3c38..0176f672c 100644 import net.raphimc.viabedrock.protocol.model.ItemEntry; import net.raphimc.viabedrock.protocol.model.ItemRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; -@@ -78,9 +80,11 @@ import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; - - import java.util.HashMap; +@@ -87,8 +89,9 @@ import java.util.HashMap; + import java.util.List; import java.util.HashSet; -+import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; @@ -874,7 +872,7 @@ index d1e8b3c38..0176f672c 100644 import java.util.logging.Level; public class ItemRewriter extends StoredObject implements NetworkItemStackContext { -@@ -113,7 +117,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -121,7 +124,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Type optionalNewItemType; private final Type newItemArrayType; private final Type stackRequestResultType; @@ -883,7 +881,7 @@ index d1e8b3c38..0176f672c 100644 static { // TODO: Add missing item nbt rewriters -@@ -450,29 +454,22 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -458,29 +461,22 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex "Translating Java items to Bedrock is not yet supported"); } From 7c4d66ce23e6d136b1e744eeabd8b307cfa6ef3b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:41:48 +0200 Subject: [PATCH 10/12] feat(crafting): resolve deferred native ingredient names Preserve native wildcard alias selection, auxiliary rewriting, block fallback and cached per-position resolution. Keep live alias membership independent of representatives and air-based emptiness, and query emptiness in the counter phases where native lookup occurs. Fold the model and current-owner bridge into the owning crafting patch. Verify packaged behavior against original resolver and mixed-counter controls while keeping actual registry acquisition, wire admission and installed availability explicit integration work. --- docs/native-player-inventory.md | 63 +- ...edrock-recipes-and-place-ingredients.patch | 622 +++++++++++++++++- ...edrock-recipes-and-place-ingredients.pr.md | 26 +- 3 files changed, 676 insertions(+), 35 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 9358fac4..a2d30ae4 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -650,5 +650,66 @@ All 1,485 core classes match both embedded copies; all 1,191 client classes matc Deferred wire names require another resolution stage before this API can consume their results. Original resolver `0x142522910` can select a runtime alias descriptor or fall back to a block definition. Treating every wire name as an ordinary Item key would skip those branches and can misclassify empty ingredients. -Deferred names, alias/tag/Molang payloads, full wire admission, actual recipe contexts and installed availability remain unfinished. +The model below implements deferred names and alias behavior against explicit registry interfaces. +Production registry acquisition, tag/Molang payload resolution, full wire admission, actual recipe contexts and installed availability remain unfinished. +The four original reports and all five goal areas remain active. + + +## Deferred names and complex aliases + +`NativeRecipeDescriptor` retains empty, ordinary, complex-alias and deferred values. +Name resolution narrows the requested auxiliary to a signed short before selecting a branch. +Only a wildcard request can select a registered complex alias before ordinary name lookup. +Ordinary lookup can rewrite the auxiliary. The resolver narrows that result before associated block validation and clamps it afterward. +A found Item with an invalid associated block state resolves empty. It does not fall back to another block. +An absent Item can use block fallback, which produces a wildcard descriptor independently of the requested auxiliary. +An empty name resolves empty without registry queries. + +Each ingredient position owns its deferred value. +The first query retains the resolved descriptor, including a missing result. +Source guards and source descriptor conversion precede deferred source matching. +The native clone and wrapper conversion establish this ownership and mutation behavior. +A complete recipe loader still needs to create and retire these values with their native owners. + +Alias representatives, emptiness and matching are separate data. +The representative can exist while every alias member is unavailable. It does not become an implicit member. +Emptiness enumerates current members and requires a definition other than `minecraft:air`. +Matching enumerates current members independently and compares their definition IDs with wildcard metadata. +An air-only alias can therefore count as empty yet match an air definition. +An ordinary air descriptor also retains its identity while its emptiness predicate returns true. +The model preserves those results instead of replacing every empty descriptor with a null key. +Member enumeration and lookup stay live and stop at the first qualifying member. + +The rich `ItemRewriter.countResolvedRecipeInputs` overload requires a complete native registry context. +It combines these descriptors with copied current-owner sources and the shared matcher/counter. +The counter now queries emptiness during each minimum and consumption phase after source matching. +An early cached active-slot vector changed native lookup phases and could hide changing alias membership. +Default descriptor double dispatch also visits the candidate first. The source matcher now retains that block-query order. +The separate generic key API retains its own query contract. + +The reference executes complete resolver `0x142522910`, legacy/default constructors, alias construction, enumeration, matching and wrapper emptiness. +Original deferred clone `0x142521bc0` owns the copied name and auxiliary before counter evaluation. +Native air predicate `0x142525430` reads the definition name independently of its numeric ID. +The original global initializer supplies the `minecraft:air` symbol used by that predicate. +Original string/hash construction, allocation, result destruction and counter cleanup execute. +The capture covers 1,904 resolution/emptiness controls and 4,032 descriptor comparisons. +A separate 220-case mixed counter capture covers 514 rounds and 1,179 registry calls. +It checks total counts, every round and source reference, and exact lookup order through deferred resolution and repeated emptiness queries. +All post-resolution ingredient payloads and counter temporaries are destroyed, leaving no tracked native allocations. + +The captures supply registry name lookup and auxiliary rewriting, alias existence, representatives, member vectors and member lookup. +They also supply BlockLegacy fallback/state interfaces, stable ordinary definitions, typed source backing and allocator/CRT/TLS services. +The mixed counter corpus uses ordinary sources without associated blocks; associated block controls remain in the resolver corpus. +Diagnostics use a supplied boundary and are excluded from Java lookup-order comparisons. +These controls do not establish actual registry acquisition, full block/registry implementations, wire admission, actor construction or complete source Item retirement. + +The packaged core matches both corpora and three focused rich-descriptor integration regressions. +Nine deliberate changes fail value or lookup-order comparisons, including early emptiness caching and repeated deferred resolution. +The retained counter, source matcher, search, unlock and progressive-model comparisons also pass. +All four builds pass with 2,859 tests, 163 optional skips and no failures or errors. Checkstyle passes. +All 1,493 core classes match both embedded copies; all 1,191 client classes match the tested add-on. + +The registry interface has no default empty tables or replacement data. +Production must supply actual alias registration, ordered members, auxiliary rewriting and block fallback from the same native context. +Tag/Molang resolution, recipe contexts, installed evaluators, screen lifecycle, physical input, actions and native pixels remain unfinished. The four original reports and all five goal areas remain active. diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 6d89e957..171f6fbf 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From 329e219596dc460e8b903a299f6e1b6a697c5d77 Mon Sep 17 00:00:00 2001 +From 61dac61c590fd9dd48f7a72d7af3b6fd5dd91da8 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -472,6 +472,30 @@ registry lookup, model backing and allocator/CRT remain supplied boundaries. The target constructor clears Item references at zero quantity; raw source seeds must not preserve those definitions. Deferred/tag/Molang resolution, full wire admission, actor capture, installed availability, actions and pixels remain open. + +Resolve deferred names through the native short-auxiliary, complex-alias and +BlockLegacy fallback branches. Keep ordinary identity, alias representative, +live member enumeration, air-based emptiness and matching separate. Cache each +owned deferred position's first resolution, including failure, after source +guards and source conversion. Preserve later alias membership queries. + +Count rich descriptors through the existing current-owner source bridge with +an explicit complete registry context. Recheck emptiness during native minimum +and consumption phases after source matching, rather than caching active slots +before grouping. Default descriptor double dispatch visits the candidate first; +retain that block-query order without changing the separate generic key API. +The registry interface has no fabricated missing-data or alias fallback defaults. +Actual registry acquisition and installed availability remain integration work. + +Complete original resolver142522910, descriptor cloning/resolution, air predicate, +alias enumeration/matching, default comparison and destruction match1904 controls +with4032 comparisons. A further220 original full-counter controls verify514 rounds +and1179 registry calls with exact lookup phases and post-resolution cleanup. +Registry name/alias/member/BlockLegacy interfaces, ordinary definitions and source +backing remain supplied boundaries. Native actor capture, complete wire admission, +remaining tag/Molang resolution, installed screens, input/actions and pixels remain +unfinished. The mixed fixture initializes the actual legacy ID before fallback; +no production rule was weakened for its initial missing construction field. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + @@ -482,7 +506,8 @@ admission, actor capture, installed availability, actions and pixels remain open .../model/container/CraftingPlacement.java | 229 +++++ .../container/FurnaceRecipePlacement.java | 219 +++++ .../NativeCookingRecipeSelection.java | 206 +++++ - .../model/container/NativeRecipeCounter.java | 168 ++++ + .../model/container/NativeRecipeCounter.java | 163 ++++ + .../container/NativeRecipeDescriptor.java | 166 ++++ .../container/RecipeIngredientMatcher.java | 38 + .../block/CraftingTableContainer.java | 100 ++- .../container/block/FurnaceContainer.java | 209 +++++ @@ -503,7 +528,7 @@ admission, actor capture, installed availability, actions and pixels remain open .../protocol/packet/InventoryPackets.java | 146 +++- .../protocol/packet/JoinPackets.java | 1 + .../protocol/packet/MultiStatePackets.java | 55 ++ - .../protocol/rewriter/ItemRewriter.java | 94 ++- + .../protocol/rewriter/ItemRewriter.java | 118 ++- .../protocol/storage/CraftingDataStorage.java | 3 +- .../protocol/storage/CraftingDataTracker.java | 626 ++++++++++++-- .../storage/InventoryOptionsStorage.java | 187 ++++ @@ -524,13 +549,14 @@ admission, actor capture, installed availability, actions and pixels remain open .../container/FurnaceRecipePlacementTest.java | 418 +++++++++ .../NativeRecipeCounterFixtureTest.java | 132 +++ .../container/NativeRecipeCounterTest.java | 66 ++ + .../container/NativeRecipeDescriptorTest.java | 305 +++++++ .../player/InventoryContainerTest.java | 131 +++ .../model/recipe/MolangTagQueryTest.java | 53 ++ .../model/recipe/RecipeOwnershipTest.java | 52 ++ .../packet/FurnaceOptionsPacketTest.java | 162 ++++ .../packet/InventoryOptionsPacketTest.java | 152 ++++ .../NativeFurnaceOptionsPacketTest.java | 214 +++++ - .../ResolvedRecipeIngredientTest.java | 256 ++++++ + .../ResolvedRecipeIngredientTest.java | 282 +++++++ .../storage/CookingRecipeBookTest.java | 154 ++++ .../protocol/storage/CraftingCursorTest.java | 158 ++++ .../storage/CraftingDataTrackerTest.java | 409 +++++++++ @@ -543,7 +569,7 @@ admission, actor capture, installed availability, actions and pixels remain open .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 81 +- - 70 files changed, 8866 insertions(+), 161 deletions(-) + 72 files changed, 9382 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -554,6 +580,7 @@ admission, actor capture, installed availability, actions and pixels remain open create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeCookingRecipeSelection.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java @@ -573,6 +600,7 @@ admission, actor capture, installed availability, actions and pixels remain open create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacementTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterFixtureTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOwnershipTest.java @@ -1699,10 +1727,10 @@ index 000000000..50b7fdf1d +} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java new file mode 100644 -index 000000000..27d44cefc +index 000000000..8b981b8a2 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java -@@ -0,0 +1,168 @@ +@@ -0,0 +1,163 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1734,16 +1762,6 @@ index 000000000..27d44cefc + public static Result count(final Recipe recipe, final List sources, + final ToIntFunction descriptorHash) { + final int ingredientCount = recipe.ingredientCount(); -+ final boolean[] active = new boolean[ingredientCount]; -+ boolean anyActive = false; -+ for (int index = 0; index < ingredientCount; index++) { -+ active[index] = !recipe.emptyIngredient(index); -+ anyActive |= active[index]; -+ } -+ if (!anyActive) { -+ throw new IllegalArgumentException("Native recipe counting requires a nonempty ingredient"); -+ } -+ + final Map byHash = new HashMap<>(); + final List groups = new ArrayList<>(); + for (final Source source : sources) { @@ -1808,20 +1826,25 @@ index 000000000..27d44cefc + } + } + int minimum = Integer.MAX_VALUE; ++ boolean anyActive = false; + final List round = new ArrayList<>(ingredientCount); + for (int index = 0; index < ingredientCount; index++) { + round.add(new IngredientCount(amounts[index], locations.get(index))); -+ if (active[index]) { ++ if (!recipe.emptyIngredient(index)) { ++ anyActive = true; + minimum = Math.min(minimum, amounts[index]); + } + } ++ if (!anyActive) { ++ throw new IllegalArgumentException("Native recipe counting requires a nonempty ingredient"); ++ } + rounds.add(round); + if (minimum <= 0) { + return new Result(count, rounds); + } + count += minimum; + for (int index = 0; index < ingredientCount; index++) { -+ if (active[index]) { ++ if (!recipe.emptyIngredient(index)) { + choices[index].amount -= minimum; + } + } @@ -1871,9 +1894,181 @@ index 000000000..27d44cefc + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java +new file mode 100644 +index 000000000..588c91dc2 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java +@@ -0,0 +1,166 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; ++ ++import java.util.List; ++import java.util.Objects; ++import java.util.function.Function; ++ ++/** Resolved native descriptor values. Registry queries remain live for alias enumeration. */ ++public sealed interface NativeRecipeDescriptor permits NativeRecipeDescriptor.Empty, NativeRecipeDescriptor.Ordinary, ++ NativeRecipeDescriptor.ComplexAlias, NativeRecipeDescriptor.Deferred { ++ ++ static NativeRecipeDescriptor resolveName(final String name, final int auxiliary, final Registry registry) { ++ if (name.isEmpty()) { ++ return Empty.INSTANCE; ++ } ++ final short requested = (short) auxiliary; ++ if (requested == Short.MAX_VALUE && registry.hasComplexAlias(name)) { ++ return new ComplexAlias(name, registry.aliasRepresentative(name)); ++ } ++ final Lookup lookup = registry.lookupItem(name, requested); ++ if (lookup == null || lookup.item == null) { ++ final ItemEntry fallback = registry.fallbackBlockItem(name); ++ return fallback == null ? Empty.INSTANCE : new Ordinary(fallback, Short.MAX_VALUE); ++ } ++ final short resolved = (short) lookup.auxiliary; ++ if (resolved != Short.MAX_VALUE && registry.hasAssociatedBlock(lookup.item) ++ && registry.blockState(lookup.item, resolved) == null) { ++ return Empty.INSTANCE; ++ } ++ return new Ordinary(lookup.item, (short) Math.max(0, resolved)); ++ } ++ ++ boolean isEmpty(Registry registry); ++ ++ /** Match the default concrete descriptor produced from a source stack after native source guards. */ ++ default boolean matchesSource(final BedrockItem source, final Registry registry, ++ final Function descriptor) { ++ if (!RecipeIngredientMatcher.eligible(source)) { ++ return false; ++ } ++ final ItemRecipeKey candidate = descriptor.apply(source); ++ return this.matchesCandidate(candidate, source.tag() != null, registry); ++ } ++ ++ private boolean matchesCandidate(final ItemRecipeKey candidate, final boolean userdata, final Registry registry) { ++ return switch (this) { ++ case Empty _ -> candidate == null && !userdata; ++ case Deferred deferred -> deferred.value(registry).matchesCandidate(candidate, userdata, registry); ++ case Ordinary ordinary -> candidate != null && candidate.matches(ordinary.key(), auxiliary -> registry.hasAssociatedBlock(ordinary.item) ++ ? registry.blockState(ordinary.item, (short) auxiliary) : null); ++ case ComplexAlias alias -> { ++ if (candidate == null) { ++ yield false; ++ } ++ boolean matched = false; ++ for (final String member : registry.aliasMembers(alias.name)) { ++ final ItemEntry item = registry.aliasMember(member); ++ if (item != null && (item.id() & 0xFFFF) == candidate.identifier()) { ++ matched = true; ++ break; ++ } ++ } ++ yield matched; ++ } ++ }; ++ } ++ ++ /** These are native registry/block interfaces, not a fallback policy for unavailable data. */ ++ interface Registry { ++ boolean hasComplexAlias(String name); ++ ++ Lookup lookupItem(String name, short auxiliary); ++ ++ ItemEntry fallbackBlockItem(String name); ++ ++ boolean hasAssociatedBlock(ItemEntry item); ++ ++ Object blockState(ItemEntry item, short auxiliary); ++ ++ ItemEntry aliasRepresentative(String name); ++ ++ List aliasMembers(String name); ++ ++ ItemEntry aliasMember(String name); ++ } ++ ++ record Lookup(ItemEntry item, int auxiliary) { } ++ ++ /** Each native ingredient position owns its deferred value and retains the first resolution. */ ++ final class Deferred implements NativeRecipeDescriptor { ++ private final String name; ++ private final short auxiliary; ++ private NativeRecipeDescriptor resolved; ++ ++ public Deferred(final String name, final int auxiliary) { ++ this.name = Objects.requireNonNull(name); ++ this.auxiliary = (short) auxiliary; ++ } ++ ++ private NativeRecipeDescriptor value(final Registry registry) { ++ if (this.resolved == null) { ++ this.resolved = resolveName(this.name, this.auxiliary, registry); ++ } ++ return this.resolved; ++ } ++ ++ @Override ++ public boolean isEmpty(final Registry registry) { ++ return this.value(registry).isEmpty(registry); ++ } ++ } ++ ++ enum Empty implements NativeRecipeDescriptor { ++ INSTANCE; ++ ++ @Override ++ public boolean isEmpty(final Registry registry) { ++ return true; ++ } ++ } ++ ++ record Ordinary(ItemEntry item, short auxiliary) implements NativeRecipeDescriptor { ++ public Ordinary { ++ Objects.requireNonNull(item); ++ if (auxiliary < 0) { ++ throw new IllegalArgumentException("Resolved descriptor auxiliary must be nonnegative"); ++ } ++ } ++ ++ public ItemRecipeKey key() { ++ return new ItemRecipeKey(this.item.id() & 0xFFFF, this.auxiliary); ++ } ++ ++ @Override ++ public boolean isEmpty(final Registry registry) { ++ return this.item.identifier().equals("minecraft:air"); ++ } ++ } ++ ++ /** The representative is retained for presentation. Membership controls emptiness and matching. */ ++ record ComplexAlias(String name, ItemEntry representative) implements NativeRecipeDescriptor { ++ public ComplexAlias { ++ Objects.requireNonNull(name); ++ } ++ ++ @Override ++ public boolean isEmpty(final Registry registry) { ++ for (final String member : registry.aliasMembers(this.name)) { ++ final ItemEntry item = registry.aliasMember(member); ++ if (item != null && !item.identifier().equals("minecraft:air")) { ++ return false; ++ } ++ } ++ return true; ++ } ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java b/src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java new file mode 100644 -index 000000000..1bce19556 +index 000000000..d7c863992 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java @@ -0,0 +1,38 @@ @@ -1911,7 +2106,7 @@ index 000000000..1bce19556 + } + final ItemRecipeKey candidate = descriptor.apply(source); + return ingredient == null ? candidate == null && source.tag() == null -+ : ingredient.matches(candidate, ingredientBlocks); ++ : candidate != null && candidate.matches(ingredient, ingredientBlocks); + } + +} @@ -3341,10 +3536,10 @@ index 0b8c600e9..4fef2c18d 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 26915529d..fd460b8c4 100644 +index 26915529d..9892f998f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -40,9 +40,12 @@ import com.viaversion.viaversion.util.Key; +@@ -40,9 +40,13 @@ import com.viaversion.viaversion.util.Key; import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.api.model.BlockState; import net.raphimc.viabedrock.api.model.NativeItemNames; @@ -3353,12 +3548,13 @@ index 26915529d..fd460b8c4 100644 import net.raphimc.viabedrock.api.model.NativeItemStackData; +import net.raphimc.viabedrock.api.model.container.NativeItemStackMatcher; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; +import net.raphimc.viabedrock.api.model.container.RecipeIngredientMatcher; +import net.raphimc.viabedrock.api.model.container.Container; import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -55,6 +58,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; +@@ -55,6 +59,7 @@ import net.raphimc.viabedrock.protocol.model.ItemRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; import net.raphimc.viabedrock.protocol.model.ItemStackHeader; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; @@ -3366,7 +3562,7 @@ index 26915529d..fd460b8c4 100644 import net.raphimc.viabedrock.protocol.model.ItemStackingProperties; import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; import net.raphimc.viabedrock.protocol.model.ItemCooldown; -@@ -65,16 +69,21 @@ import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; +@@ -65,16 +70,21 @@ import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomAttachableResourceRewriter; import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomItemTextureResourceRewriter; import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; @@ -3388,7 +3584,7 @@ index 26915529d..fd460b8c4 100644 import java.util.HashSet; import java.util.Map; import java.util.Objects; -@@ -102,6 +111,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -102,6 +112,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Set componentItems; private final Int2ObjectMap blockItemValidBlockStates; private final Type itemType; @@ -3397,7 +3593,7 @@ index 26915529d..fd460b8c4 100644 private final Type optionalItemType; private final Type itemArrayType; private final Type newItemType; -@@ -189,6 +200,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -189,6 +201,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex this.items.getOrDefault("minecraft:shield", 0), false, this); @@ -3406,7 +3602,7 @@ index 26915529d..fd460b8c4 100644 this.optionalItemType = new OptionalType<>(this.itemType); this.itemArrayType = new ArrayType<>(this.itemType, BedrockTypes.UNSIGNED_VAR_INT); this.newItemType = -@@ -500,6 +513,47 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -500,6 +514,70 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? this.user().get(BlockStateRewriter.class).blockItemRecipeDescriptor(item.blockRuntimeId()) : null); } @@ -3450,11 +3646,34 @@ index 26915529d..fd460b8c4 100644 + return RecipeIngredientMatcher.matches(ingredient, source, this::recipeKey, auxiliary -> association != null + ? this.user().get(BlockStateRewriter.class).blockItemState(association, (short) auxiliary) : null); + } ++ ++ /** The caller supplies the complete native registry context, including complex alias membership. */ ++ public NativeRecipeCounter.Result countResolvedRecipeInputs(final List ingredients, ++ final NativeRecipeDescriptor.Registry registry, ++ final Container owner, final boolean includeCursor) { ++ final List snapshot = List.copyOf(ingredients); ++ return this.countRecipeInputs(new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return snapshot.size(); ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ return snapshot.get(index).isEmpty(registry); ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ return snapshot.get(index).matchesSource(item, registry, ItemRewriter.this::recipeKey); ++ } ++ }, owner, includeCursor); ++ } + public boolean matchesRecipeItem(final String identifier, final int metadata, final BedrockItem item) { final ItemRecipeKey query = this.registry.namedRecipeKey(identifier, metadata); if (query == null || item == null || item.isEmpty()) { -@@ -515,6 +569,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -515,6 +593,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? blocks.blockItemState(queryAssociation, (short) auxiliary) : null); } @@ -3476,7 +3695,7 @@ index 26915529d..fd460b8c4 100644 public ItemRegistry getItemRegistry() { return this.registry; } -@@ -539,6 +608,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -539,6 +632,21 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return NativeItemStackMatcher.matchesIdentity(query, candidate, blocks::blockState, blocks::restrictionBlocks); } @@ -3498,7 +3717,7 @@ index 26915529d..fd460b8c4 100644 public boolean canMerge(final BedrockItem source, final BedrockItem destination) { if (destination == null || destination.isEmpty()) { return false; -@@ -582,6 +666,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -582,6 +690,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.itemType; } @@ -6746,6 +6965,317 @@ index 000000000..56088325a + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java +new file mode 100644 +index 000000000..8229fc94e +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java +@@ -0,0 +1,305 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.nbt.tag.ListTag; ++import com.viaversion.nbt.tag.IntTag; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import com.viaversion.viaversion.libs.gson.JsonArray; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++ ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.HashMap; ++import java.util.stream.IntStream; ++import java.util.Map; ++import java.util.zip.GZIPInputStream; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class NativeRecipeDescriptorTest { ++ ++ @Test ++ void aliasEmptinessAndMatchingRemainIndependentOfTheRepresentative() { ++ final var options = new JsonObject(); ++ options.addProperty("alias", true); ++ options.addProperty("aliasItem", 3); ++ options.addProperty("airId", 1); ++ options.add("members", JsonParser.parseString("[0,1]")); ++ final var registry = new Registry(options); ++ final var alias = NativeRecipeDescriptor.resolveName("test:alias", 32767, registry); ++ assertEquals(registry.definitions.get(3), ((NativeRecipeDescriptor.ComplexAlias) alias).representative()); ++ assertTrue(alias.isEmpty(registry)); ++ assertTrue(alias.matchesSource(new BedrockItem(1), registry, item -> registry.items.recipeKey(item, null))); ++ assertFalse(alias.matchesSource(new BedrockItem(3), registry, item -> registry.items.recipeKey(item, null))); ++ options.add("members", JsonParser.parseString("[2]")); ++ assertFalse(alias.isEmpty(registry)); ++ assertTrue(alias.matchesSource(new BedrockItem(2), registry, item -> registry.items.recipeKey(item, null))); ++ assertFalse(alias.matchesSource(new BedrockItem(1), registry, item -> registry.items.recipeKey(item, null))); ++ } ++ ++ @Test ++ void deferredValuesResolveLazilyAndRetainTheFirstResult() { ++ final var options = new JsonObject(); ++ options.addProperty("item", 1); ++ final var registry = new Registry(options); ++ final var descriptor = new NativeRecipeDescriptor.Deferred("test:item", 0); ++ final var source = new BedrockItem(1); ++ final var tag = new CompoundTag(); ++ tag.put("ench", new ListTag<>(IntTag.class)); ++ source.setTag(tag); ++ assertFalse(descriptor.matchesSource(source, registry, item -> registry.items.recipeKey(item, null))); ++ assertTrue(registry.calls.isEmpty()); ++ source.setTag(null); ++ assertTrue(descriptor.matchesSource(source, registry, item -> registry.items.recipeKey(item, null))); ++ options.addProperty("item", 2); ++ registry.calls.clear(); ++ assertFalse(descriptor.isEmpty(registry)); ++ assertTrue(descriptor.matchesSource(source, registry, item -> registry.items.recipeKey(item, null))); ++ assertTrue(registry.calls.isEmpty()); ++ final var missing = new NativeRecipeDescriptor.Deferred("test:missing", 0); ++ options.addProperty("item", 0); ++ assertTrue(missing.isEmpty(registry)); ++ options.addProperty("item", 1); ++ registry.calls.clear(); ++ assertTrue(missing.isEmpty(registry)); ++ assertTrue(registry.calls.isEmpty()); ++ } ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_MIXED_RECIPE_COUNTER", matches = ".+") ++ void matchesNativeLazyResolutionAndRepeatedEmptinessDuringCounterRounds() throws Exception { ++ try (var input = new InputStreamReader(new GZIPInputStream(Files.newInputStream( ++ Path.of(System.getenv("STACKANVIL_NATIVE_MIXED_RECIPE_COUNTER")))), StandardCharsets.UTF_8)) { ++ verifyCounter(JsonParser.parseReader(input).getAsJsonObject()); ++ } ++ } ++ ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_NATIVE_DEFERRED_INGREDIENTS", matches = ".+") ++ void matchesCompleteNativeNameResolutionEmptinessAndDescriptorComparisons() throws Exception { ++ try (var input = new InputStreamReader(new GZIPInputStream(Files.newInputStream( ++ Path.of(System.getenv("STACKANVIL_NATIVE_DEFERRED_INGREDIENTS")))), StandardCharsets.UTF_8)) { ++ verify(JsonParser.parseReader(input).getAsJsonObject()); ++ } ++ } ++ ++ static int verify(final JsonObject fixture) { ++ final var rows = fixture.getAsJsonArray("rows"); ++ assertFalse(rows.isEmpty()); ++ for (final var element : rows) { ++ final var row = element.getAsJsonObject(); ++ final var registry = new Registry(row.getAsJsonObject("options")); ++ final var descriptor = NativeRecipeDescriptor.resolveName(row.get("inputName").getAsString(), row.get("inputAux").getAsInt(), registry); ++ final String kind = switch (descriptor) { ++ case NativeRecipeDescriptor.Deferred _ -> throw new AssertionError("Deferred value escaped explicit resolution"); ++ case NativeRecipeDescriptor.Empty _ -> "empty"; ++ case NativeRecipeDescriptor.Ordinary _ -> "ordinary"; ++ case NativeRecipeDescriptor.ComplexAlias _ -> "alternate"; ++ }; ++ assertEquals(row.get("kind").getAsString(), kind, row::toString); ++ assertEquals(lookups(row.getAsJsonArray("calls")), registry.calls, row::toString); ++ if (descriptor instanceof NativeRecipeDescriptor.Ordinary ordinary) { ++ assertEquals(row.get("id").getAsInt(), ordinary.key().identifier(), row::toString); ++ assertEquals(row.get("aux").getAsShort(), ordinary.auxiliary(), row::toString); ++ } else if (descriptor instanceof NativeRecipeDescriptor.ComplexAlias alias) { ++ assertEquals(row.get("name").getAsString(), alias.name(), row::toString); ++ assertEquals(row.get("id").isJsonNull() ? null : row.get("id").getAsInt(), ++ alias.representative() == null ? null : alias.representative().id(), row::toString); ++ } ++ registry.calls.clear(); ++ assertEquals(row.get("empty").getAsBoolean(), descriptor.isEmpty(registry), row::toString); ++ assertEquals(row.has("emptyCalls") ? lookups(row.getAsJsonArray("emptyCalls")) : List.of(), registry.calls, row::toString); ++ if (row.has("matches")) { ++ for (final var match : row.getAsJsonArray("matches")) { ++ final var value = match.getAsJsonObject(); ++ registry.calls.clear(); ++ final var source = new BedrockItem(value.get("id").getAsInt(), (short) 3, (byte) 1); ++ assertEquals(value.get("matched").getAsBoolean(), descriptor.matchesSource(source, registry, ++ item -> registry.items.recipeKey(item, null)), row::toString); ++ assertEquals(lookups(value.getAsJsonArray("calls")), registry.calls, row::toString); ++ } ++ } ++ } ++ return rows.size(); ++ } ++ ++ static int verifyCounter(final JsonObject fixture) { ++ final var collections = List.of( ++ ContainerEnumName.CraftingInputContainer, ++ ContainerEnumName.HotbarContainer, ++ ContainerEnumName.InventoryContainer, ++ ContainerEnumName.CursorContainer); ++ final var names = List.of("crafting_input_items", "hotbar_items", "inventory_items", "cursor_items"); ++ final var rows = fixture.getAsJsonArray("rows"); ++ assertFalse(rows.isEmpty()); ++ for (final var element : rows) { ++ final var row = element.getAsJsonObject(); ++ final var registry = new Registry(row.getAsJsonObject("options")); ++ final List ingredients = new ArrayList<>(); ++ for (final var input : row.getAsJsonArray("ingredients")) { ++ final var value = input.getAsJsonObject(); ++ ingredients.add(switch (value.get("kind").getAsString()) { ++ case "empty" -> NativeRecipeDescriptor.Empty.INSTANCE; ++ case "ordinary" -> new NativeRecipeDescriptor.Ordinary(registry.definitions.get(value.get("id").getAsInt()), ++ value.has("aux") ? value.get("aux").getAsShort() : 0); ++ case "deferred" -> new NativeRecipeDescriptor.Deferred(value.get("name").getAsString(), value.get("aux").getAsInt()); ++ default -> throw new AssertionError("Unsupported native ingredient type"); ++ }); ++ } ++ final List sources = new ArrayList<>(); ++ final var rawSources = row.getAsJsonArray("sources"); ++ for (int index = 0; index < rawSources.size(); index++) { ++ final List stacks = new ArrayList<>(); ++ for (final var item : rawSources.get(index).getAsJsonArray()) { ++ final var value = item.getAsJsonObject(); ++ stacks.add(new BedrockItem(value.get("id").getAsInt(), value.get("meta").getAsShort(), (byte) value.get("count").getAsInt())); ++ } ++ sources.add(new NativeRecipeCounter.Source(collections.get(index), stacks)); ++ } ++ final var recipe = new NativeRecipeCounter.Recipe() { ++ @Override ++ public int ingredientCount() { ++ return ingredients.size(); ++ } ++ ++ @Override ++ public boolean emptyIngredient(final int index) { ++ return ingredients.get(index).isEmpty(registry); ++ } ++ ++ @Override ++ public boolean matchesIngredient(final int index, final BedrockItem source) { ++ return ingredients.get(index).matchesSource(source, registry, item -> registry.items.recipeKey(item, null)); ++ } ++ }; ++ final var actual = NativeRecipeCounter.count(recipe, sources, item -> { ++ final var key = registry.items.recipeKey(item, null); ++ return key == null ? 0 : key.descriptorHash(); ++ }); ++ final List> rounds = new ArrayList<>(); ++ for (final var batch : row.getAsJsonArray("batches")) { ++ final List round = new ArrayList<>(); ++ for (final var input : batch.getAsJsonArray()) { ++ final var value = input.getAsJsonObject(); ++ final List locations = new ArrayList<>(); ++ for (final var location : value.getAsJsonArray("sources")) { ++ final var ref = location.getAsJsonObject(); ++ locations.add(new NativeRecipeCounter.Location(collections.get(names.indexOf(ref.get("collection").getAsString())), ref.get("slot").getAsInt())); ++ } ++ round.add(new NativeRecipeCounter.IngredientCount(value.get("amount").getAsInt(), locations)); ++ } ++ rounds.add(round); ++ } ++ assertEquals(new NativeRecipeCounter.Result(row.get("count").getAsInt(), rounds), actual, row::toString); ++ assertEquals(lookups(row.getAsJsonArray("calls")), registry.calls, row::toString); ++ } ++ return rows.size(); ++ } ++ ++ private static List lookups(final JsonArray calls) { ++ return calls.asList().stream().map(value -> value.getAsJsonObject()) ++ .filter(value -> !value.get("kind").getAsString().equals("diagnostic")).toList(); ++ } ++ ++ private static final class Registry implements NativeRecipeDescriptor.Registry { ++ private final JsonObject options; ++ private final Map definitions; ++ private final ItemRegistry items; ++ private final Object block = new Object(); ++ private final List calls = new ArrayList<>(); ++ ++ private Registry(final JsonObject options) { ++ this.options = options; ++ final var values = new HashMap(); ++ for (int id = 1; id <= 4; id++) { ++ values.put(id, new ItemEntry(id == this.integer("airId", 0) ? "minecraft:air" : "test:ordinary_" + id, ++ id, false, ItemVersion.Legacy, null)); ++ } ++ this.definitions = Map.copyOf(values); ++ this.items = new ItemRegistry(values.values().toArray(ItemEntry[]::new)); ++ } ++ ++ @Override ++ public boolean hasComplexAlias(final String name) { ++ this.record("aliasTest").addProperty("name", name); ++ return this.flag("alias"); ++ } ++ ++ @Override ++ public NativeRecipeDescriptor.Lookup lookupItem(final String name, final short auxiliary) { ++ final var call = this.record("nameLookup"); ++ call.addProperty("name", name); ++ call.addProperty("aux", auxiliary); ++ return new NativeRecipeDescriptor.Lookup(this.definitions.get(this.integer("item", 0)), this.integer("lookupAux", auxiliary)); ++ } ++ ++ @Override ++ public ItemEntry fallbackBlockItem(final String name) { ++ this.record("blockFallback"); ++ return this.flag("fallback") ? this.definitions.get(this.integer("legacyId", 1)) : null; ++ } ++ ++ @Override ++ public boolean hasAssociatedBlock(final ItemEntry item) { ++ return this.flag("associated"); ++ } ++ ++ @Override ++ public Object blockState(final ItemEntry item, final short auxiliary) { ++ this.record("blockState").addProperty("aux", auxiliary & 0xFFFF); ++ return !this.options.has("validState") || this.flag("validState") ? this.block : null; ++ } ++ ++ @Override ++ public ItemEntry aliasRepresentative(final String name) { ++ this.record("aliasBase").addProperty("name", name); ++ return this.definitions.get(this.integer("aliasItem", 0)); ++ } ++ ++ @Override ++ public List aliasMembers(final String name) { ++ this.record("aliasMembers"); ++ return this.options.has("members") ? IntStream.range(0, this.options.getAsJsonArray("members").size()) ++ .mapToObj(Integer::toString).toList() : List.of(); ++ } ++ ++ @Override ++ public ItemEntry aliasMember(final String name) { ++ final int id = this.options.getAsJsonArray("members").get(Integer.parseInt(name)).getAsInt(); ++ this.record("memberLookup").addProperty("id", id); ++ return this.definitions.get(id); ++ } ++ ++ private boolean flag(final String name) { ++ return this.options.has(name) && this.options.get(name).getAsBoolean(); ++ } ++ ++ private int integer(final String name, final int fallback) { ++ return this.options.has(name) ? this.options.get(name).getAsInt() : fallback; ++ } ++ ++ private JsonObject record(final String kind) { ++ final var result = new JsonObject(); ++ result.addProperty("kind", kind); ++ this.calls.add(result); ++ return result; ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java new file mode 100644 index 000000000..fbc7a40e4 @@ -7548,10 +8078,10 @@ index 000000000..d66d6110c +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java new file mode 100644 -index 000000000..062fae661 +index 000000000..e324f3944 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/ResolvedRecipeIngredientTest.java -@@ -0,0 +1,256 @@ +@@ -0,0 +1,282 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7564,6 +8094,7 @@ index 000000000..062fae661 +import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viaversion.libs.gson.JsonParser; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; +import net.raphimc.viabedrock.api.model.container.RecipeIngredientMatcher; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; @@ -7651,6 +8182,31 @@ index 000000000..062fae661 + } + + @Test ++ void countsRichAliasInputsThroughTheCurrentOwnerWithLiveMembership() throws Exception { ++ final var user = Mockito.mock(UserConnection.class); ++ final var inventory = new InventoryTracker(user); ++ Mockito.when(user.get(InventoryTracker.class)).thenReturn(inventory); ++ final var requests = Mockito.mock(InventoryRequestTracker.class); ++ Mockito.when(requests.isPredictingClick()).thenReturn(true); ++ Mockito.when(user.get(InventoryRequestTracker.class)).thenReturn(requests); ++ final var items = items(user); ++ final var owner = inventory.getInventoryContainer(); ++ inventory.setCurrentContainer(owner); ++ inventory.getHudContainer().setItem(28, new BedrockItem(1, (short) 0, (byte) 4)); ++ owner.setItem(0, new BedrockItem(1, (short) 0, (byte) 7)); ++ final var definition = new ItemEntry("test:ordinary_1", 1, false, ItemVersion.Legacy, null); ++ final var registry = Mockito.mock(NativeRecipeDescriptor.Registry.class); ++ Mockito.when(registry.aliasMembers("test:alias")).thenReturn(List.of()); ++ Mockito.when(registry.aliasMember("member")).thenReturn(definition); ++ final List ingredients = List.of( ++ new NativeRecipeDescriptor.ComplexAlias("test:alias", definition), ++ new NativeRecipeDescriptor.Ordinary(definition, (short) 0)); ++ assertEquals(11, items.countResolvedRecipeInputs(ingredients, registry, owner, false).count()); ++ Mockito.when(registry.aliasMembers("test:alias")).thenReturn(List.of("member")); ++ assertEquals(5, items.countResolvedRecipeInputs(ingredients, registry, owner, false).count()); ++ } ++ ++ @Test + void bindsResolvedIngredientsToCanonicalAssociatedBlockOwnership() throws Exception { + final var user = Mockito.mock(UserConnection.class); + final var items = items(user); diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index e6225693..01fdefd9 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -481,10 +481,34 @@ Thirty-nine shapeless controls also match. Typed metatype getters, ordinary defi The original constructor clears Item references at zero quantity; the fixture now executes that construction before typed userdata injection. `countResolvedRecipeInputs` accepts resolved descriptors with explicit empty positions. -Deferred wire names can select aliases or block fallback. Their resolution, tag/Molang payloads, complete wire admission and installed availability remain unfinished. +Deferred wire names can select aliases or block fallback. The model below resolves them against explicit registry interfaces. +Actual registry acquisition, tag/Molang payloads, complete wire admission and installed availability remain unfinished. See the [resolved ingredient reference](../../../docs/native-player-inventory.md#resolved-ingredient-matching) for evidence and limits. The full four-project build passes 2,854 tests, with 163 optional skips and no failures or errors. Checkstyle passes. All 1,485 core classes match both embedded copies; all 1,191 client classes match the tested add-on. The packaged matcher passes 1,703 direct/counter controls and three focused integration regressions, and rejects six deliberate semantic changes. All retained counter, search, output-unlock and progressive-model comparisons also pass. + + +## Deferred name and alias descriptors + +Resolve names through native signed-short narrowing, wildcard alias selection, ordinary lookup rewriting and BlockLegacy fallback. +Validate associated block state before metadata clamping. Keep a failed found Item separate from an absent Item eligible for fallback. +Retain each deferred position's first resolution, including failure, after source guards and conversion. +Preserve alias representatives independently of live membership, air-based emptiness and wildcard matching. + +The rich current-owner counter overload requires an explicit complete registry context. +Query emptiness during native minimum and consumption phases instead of caching active slots before matching. +Keep candidate-first default descriptor dispatch separate from the generic key API's contract. + +Complete original routines provide 1,904 resolution/emptiness controls, 4,032 comparisons and 220 full counter controls. +The mixed counters cover 514 rounds and 1,179 registry calls with exact lookup order and post-resolution cleanup. +Registry, alias/member, BlockLegacy/state and typed source interfaces remain supplied boundaries. +Actual registry acquisition, tag/Molang payload resolution, full wire admission and installed availability remain unfinished. +The [deferred descriptor reference](../../../docs/native-player-inventory.md#deferred-names-and-complex-aliases) records the contract and limits. + +The full four-project build passes 2,859 tests, with 163 optional skips and no failures or errors. Checkstyle passes. +All 1,493 core classes match both embedded copies; all 1,191 client classes match the tested add-on. +The packaged core matches both new corpora and three focused integration regressions. +Nine deliberate changes fail value or lookup-order comparisons. All retained packaged comparisons also pass. From b2bf517208cf878fcdb19f39d2245db503bbfab5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:56:38 +0200 Subject: [PATCH 11/12] feat(crafting): retain native ItemRegistry alias tables Expose exact alias admission and ordered member names from the connection registry using the pinned native ItemRegistry tables. Block aliases and restriction groups differ in registration, redirect targets and member order, so they cannot supply these queries. Fold the data and queries into the owning crafting patch. Verify complete native initialization and cleanup, 2,127 original getter controls, deliberate regressions and the packaged build. Keep metadata callbacks, live definition ownership and installed availability as explicit remaining integration work. --- docs/native-player-inventory.md | 39 +- ...edrock-recipes-and-place-ingredients.patch | 947 +++++++++++++++++- ...edrock-recipes-and-place-ingredients.pr.md | 24 + 3 files changed, 1007 insertions(+), 3 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index a2d30ae4..888a69c3 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -710,6 +710,43 @@ All four builds pass with 2,859 tests, 163 optional skips and no failures or err All 1,493 core classes match both embedded copies; all 1,191 client classes match the tested add-on. The registry interface has no default empty tables or replacement data. -Production must supply actual alias registration, ordered members, auxiliary rewriting and block fallback from the same native context. +The ItemRegistry section below supplies built-in alias registrations and ordered member names. +Production must still supply auxiliary rewriting, live definition ownership and block fallback from the same native context. Tag/Molang resolution, recipe contexts, installed evaluators, screen lifecycle, physical input, actions and native pixels remain unfinished. The four original reports and all five goal areas remain active. + + +## Constructor-owned ItemRegistry aliases + +The connection ItemRegistry now exposes native alias admission and ordered member names. +It loads 89 name redirects and 60 complex aliases with 561 member names from the pinned target. +Each getter applies one exact redirect before reading the complex table. +It does not qualify names, parse legacy metadata or follow redirect chains. +An unregistered name returns the native empty member list. Missing or mismatched asset data fails immediately. + +ItemRegistry aliases have their own registrations and member order. +For example, the ItemRegistry `wood` group places all ordinary woods before stripped woods. +It also includes item groups such as boats, buckets, dyes and spawn eggs. +Its `double_stone_slab` redirect selects `stone_block_slab`. +The block registry and restriction tables cannot supply these ItemRegistry queries. + +The reference executes complete original ItemRegistry initialization `0x143480dc0` after native vanilla block and state construction. +It uses target 1.26.51.1, build 51061372, protocol 2193, the original 1.26.50 BaseGameVersion and owned default Experiments. +The empty parsed network vector establishes constructor-owned data before packet updates. +It does not establish normal server packet contents or wire admission. +All 60 aliases also have constructed native factory definitions at that point. +Input, registry, TLS, static and CRT cleanup finish with no remaining native allocations or held locks. + +These APIs expose names, not resolved Item definitions or metadata callbacks. +Ordinary named lookup can use a redirect with metadata, then a complex-alias callback, and reset the auxiliary after that callback. +Each alias's ordered members do not define that callback's metadata mapping. +Live member and representative ownership, ordinary lookup rewriting, block fallback and complete Java registry construction remain unfinished. +The rich descriptor model still requires that complete registry context before installed recipe evaluation can use it. +All four original reports and all five goal areas remain active. + +The packaged connection registry matches all 2,127 original getter controls, including 86 registered results and 767 member-name comparisons. +The controls query every constructed factory key and both names in each native redirect. +Three deliberate changes fail value comparisons: ignored redirects, reversed member order and substituted block tables. +The retained descriptor, counter, source matcher, search, unlock and progressive-model comparisons also pass. +All four builds pass with 2,860 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. +All 1,495 core classes and the alias asset match both embedded copies; all 1,191 client classes match the tested add-on. diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 171f6fbf..5325a667 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From 61dac61c590fd9dd48f7a72d7af3b6fd5dd91da8 Mon Sep 17 00:00:00 2001 +From de915fc6aa2b306b1f57cda915278e15f70db07a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -496,6 +496,27 @@ backing remain supplied boundaries. Native actor capture, complete wire admissio remaining tag/Molang resolution, installed screens, input/actions and pixels remain unfinished. The mixed fixture initializes the actual legacy ID before fallback; no production rule was weakened for its initial missing construction field. + +Retain constructor-owned ItemRegistry alias data independently of block +registry aliases and block restriction groups. The native tables contain +89 redirects,60 complex aliases and561 ordered member names. Exact alias +queries apply one redirect, without qualification or legacy-name parsing. +Expose those verified name queries through the connection ItemRegistry. +Missing data is fatal; a genuinely unregistered alias returns the native +empty member list. Do not infer metadata callbacks or live definitions +from the member vector. + +Evidence: complete original ItemRegistry initialization143480dc0 follows +actual native vanilla block/state construction for target1.26.51.1, +build51061372,protocol2193, with original1.26.50 BaseGameVersion and owned +default Experiments. Read actual map nodes and execute original alias +existence1434870d0 and member getter143487440. The empty parsed network +vector establishes pre-packet constructor data, not wire admission or +normal server state. Input,registry,TLS,static and CRT cleanup complete +with no remaining native allocations or held locks. Item aliases differ +from block groups in membership, ordering and redirect destinations. +Full auxiliary callbacks, member/representative resolution, underlying +registry construction on Java and installed availability remain unfinished. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + @@ -516,8 +537,10 @@ no production rule was weakened for its initial missing construction field. .../viabedrock/api/util/PacketFactory.java | 2 + .../viabedrock/protocol/BedrockProtocol.java | 2 + .../protocol/ServerboundBedrockPackets.java | 1 + + .../protocol/model/BuiltinItemAliases.java | 62 ++ .../protocol/model/FurnaceOptions.java | 17 + .../protocol/model/InventoryOptions.java | 10 + + .../protocol/model/ItemRegistry.java | 10 + .../protocol/model/recipe/ItemDescriptor.java | 13 +- .../protocol/model/recipe/MolangTagQuery.java | 64 ++ .../protocol/model/recipe/MultiRecipe.java | 46 +- @@ -540,6 +563,7 @@ no production rule was weakened for its initial missing construction field. .../types/model/FurnaceOptionsType.java | 34 + .../types/model/InventoryOptionsType.java | 35 + .../types/recipe/CraftingDataReader.java | 66 +- + .../viabedrock/data/bedrock/item_aliases.json | 778 +++++++++++++++++ .../api/model/NativeFurnaceOptionsTest.java | 57 ++ .../CookingRecipeAvailabilityTest.java | 94 +++ .../container/CookingRecipeInputsTest.java | 180 ++++ @@ -551,6 +575,7 @@ no production rule was weakened for its initial missing construction field. .../container/NativeRecipeCounterTest.java | 66 ++ .../container/NativeRecipeDescriptorTest.java | 305 +++++++ .../player/InventoryContainerTest.java | 131 +++ + .../model/ItemRegistryAliasesTest.java | 36 + .../model/recipe/MolangTagQueryTest.java | 53 ++ .../model/recipe/RecipeOwnershipTest.java | 52 ++ .../packet/FurnaceOptionsPacketTest.java | 162 ++++ @@ -569,7 +594,7 @@ no production rule was weakened for its initial missing construction field. .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 81 +- - 72 files changed, 9382 insertions(+), 161 deletions(-) + 76 files changed, 10268 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -582,6 +607,7 @@ no production rule was weakened for its initial missing construction field. create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/RecipeIngredientMatcher.java + create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQuery.java @@ -591,6 +617,7 @@ no production rule was weakened for its initial missing construction field. create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/types/item/BedrockRecipeResultType.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/types/model/FurnaceOptionsType.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/types/model/InventoryOptionsType.java + create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptionsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailabilityTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CookingRecipeInputsTest.java @@ -602,6 +629,7 @@ no production rule was weakened for its initial missing construction field. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryAliasesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOwnershipTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/FurnaceOptionsPacketTest.java @@ -2836,6 +2864,74 @@ index 3effc3865..44f257b9e 100644 LOADING_SCREEN(MinecraftPacketIds.ServerboundLoadingScreenPacket.getValue()), DEBUG_DIAGNOSTICS(MinecraftPacketIds.ServerboundDiagnosticsPacket.getValue()), CLIENT_CAMERA_AIM_ASSIST(MinecraftPacketIds.ClientCameraAimAssist.getValue()), +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java +new file mode 100644 +index 000000000..88cd02e44 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java +@@ -0,0 +1,62 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model; ++ ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import net.raphimc.viabedrock.protocol.data.ProtocolConstants; ++ ++import java.io.IOException; ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++ ++/** Constructor-owned ItemRegistry names, separate from block registry aliases and restriction groups. */ ++final class BuiltinItemAliases { ++ ++ private static final Tables TABLES = load(); ++ ++ private BuiltinItemAliases() { } ++ ++ static boolean hasComplexAlias(final String name) { ++ return TABLES.members.containsKey(redirect(name)); ++ } ++ ++ static List members(final String name) { ++ // The original getter returns its shared empty vector for an unregistered name. ++ return TABLES.members.getOrDefault(redirect(name), List.of()); ++ } ++ ++ private static String redirect(final String name) { ++ // These getters apply one exact redirect, without legacy-name parsing or qualification. ++ return TABLES.redirects.getOrDefault(name, name); ++ } ++ ++ private static Tables load() { ++ final String path = "/assets/viabedrock/data/bedrock/item_aliases.json"; ++ try (var stream = BuiltinItemAliases.class.getResourceAsStream(path)) { ++ if (stream == null) { ++ throw new IllegalStateException("Missing native item alias definitions"); ++ } ++ final var data = JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8)).getAsJsonObject(); ++ if (data.get("format").getAsInt() != 1 || data.get("protocol").getAsInt() != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ throw new IllegalStateException("Item alias definitions do not match the target protocol"); ++ } ++ final Map redirects = new HashMap<>(); ++ data.getAsJsonObject("redirects").entrySet().forEach(entry -> redirects.put(entry.getKey(), entry.getValue().getAsString())); ++ final Map> members = new HashMap<>(); ++ data.getAsJsonObject("complexAliases").entrySet().forEach(entry -> members.put(entry.getKey(), entry.getValue().getAsJsonArray().asList() ++ .stream().map(value -> value.getAsString()).toList())); ++ return new Tables(Map.copyOf(redirects), Map.copyOf(members)); ++ } catch (final IOException error) { ++ throw new IllegalStateException("Failed to load native item alias definitions", error); ++ } ++ } ++ ++ private record Tables(Map redirects, Map> members) { } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java b/src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java new file mode 100644 index 000000000..605d03d49 @@ -2875,6 +2971,27 @@ index 000000000..a3d828beb +/** Native player-inventory preferences, including independent inventory and crafting layouts. */ +public record InventoryOptions(int leftTab, int rightTab, boolean filtering, int inventoryLayout, int craftingLayout) { +} +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +index d8bf335cc..664711ae0 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +@@ -135,6 +135,16 @@ public final class ItemRegistry { + return Map.copyOf(result); + } + ++ /** Native exact-name alias admission, including one registry redirect before the complex table. */ ++ public boolean hasComplexAlias(final String name) { ++ return BuiltinItemAliases.hasComplexAlias(name); ++ } ++ ++ /** Ordered native member names. Definition lookup remains a separate live registry operation. */ ++ public List complexAliasMembers(final String name) { ++ return BuiltinItemAliases.members(name); ++ } ++ + /** Named descriptors narrow to signed-short metadata before resolving concrete identity. */ + public ItemRecipeKey namedRecipeKey(final String identifier, final int metadata) { + final ItemEntry entry = this.definition(identifier); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ItemDescriptor.java b/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ItemDescriptor.java index d62d7b11f..69e695e4e 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ItemDescriptor.java @@ -5573,6 +5690,790 @@ index 535f52d1c..87212a2d5 100644 } private CraftingDataReader() { } +diff --git a/src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json b/src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json +new file mode 100644 +index 000000000..cf23fac36 +--- /dev/null ++++ b/src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json +@@ -0,0 +1,778 @@ ++{ ++ "complexAliases": { ++ "minecraft:anvil": [ ++ "minecraft:anvil", ++ "minecraft:chipped_anvil", ++ "minecraft:damaged_anvil", ++ "minecraft:deprecated_anvil" ++ ], ++ "minecraft:banner_pattern": [ ++ "minecraft:creeper_banner_pattern", ++ "minecraft:skull_banner_pattern", ++ "minecraft:flower_banner_pattern", ++ "minecraft:mojang_banner_pattern", ++ "minecraft:field_masoned_banner_pattern", ++ "minecraft:bordure_indented_banner_pattern", ++ "minecraft:piglin_banner_pattern", ++ "minecraft:globe_banner_pattern" ++ ], ++ "minecraft:boat": [ ++ "minecraft:oak_boat", ++ "minecraft:spruce_boat", ++ "minecraft:birch_boat", ++ "minecraft:jungle_boat", ++ "minecraft:acacia_boat", ++ "minecraft:dark_oak_boat", ++ "minecraft:mangrove_boat", ++ "minecraft:bamboo_raft", ++ "minecraft:cherry_boat", ++ "minecraft:pale_oak_boat" ++ ], ++ "minecraft:bucket": [ ++ "minecraft:bucket", ++ "minecraft:milk_bucket", ++ "minecraft:water_bucket", ++ "minecraft:lava_bucket", ++ "minecraft:cod_bucket", ++ "minecraft:salmon_bucket", ++ "minecraft:tropical_fish_bucket", ++ "minecraft:pufferfish_bucket", ++ "minecraft:powder_snow_bucket", ++ "minecraft:axolotl_bucket", ++ "minecraft:tadpole_bucket", ++ "minecraft:sulfur_cube_bucket" ++ ], ++ "minecraft:carpet": [ ++ "minecraft:white_carpet", ++ "minecraft:orange_carpet", ++ "minecraft:magenta_carpet", ++ "minecraft:light_blue_carpet", ++ "minecraft:yellow_carpet", ++ "minecraft:lime_carpet", ++ "minecraft:pink_carpet", ++ "minecraft:gray_carpet", ++ "minecraft:light_gray_carpet", ++ "minecraft:cyan_carpet", ++ "minecraft:purple_carpet", ++ "minecraft:blue_carpet", ++ "minecraft:brown_carpet", ++ "minecraft:green_carpet", ++ "minecraft:red_carpet", ++ "minecraft:black_carpet" ++ ], ++ "minecraft:chemistry_table": [ ++ "minecraft:compound_creator", ++ "minecraft:material_reducer", ++ "minecraft:element_constructor", ++ "minecraft:lab_table" ++ ], ++ "minecraft:chest_boat": [ ++ "minecraft:oak_chest_boat", ++ "minecraft:spruce_chest_boat", ++ "minecraft:birch_chest_boat", ++ "minecraft:jungle_chest_boat", ++ "minecraft:acacia_chest_boat", ++ "minecraft:dark_oak_chest_boat", ++ "minecraft:mangrove_chest_boat", ++ "minecraft:bamboo_chest_raft", ++ "minecraft:cherry_chest_boat", ++ "minecraft:pale_oak_chest_boat", ++ "minecraft:poplar_chest_boat" ++ ], ++ "minecraft:coal": [ ++ "minecraft:coal", ++ "minecraft:charcoal" ++ ], ++ "minecraft:cobblestone_wall": [ ++ "minecraft:cobblestone_wall", ++ "minecraft:mossy_cobblestone_wall", ++ "minecraft:granite_wall", ++ "minecraft:diorite_wall", ++ "minecraft:andesite_wall", ++ "minecraft:sandstone_wall", ++ "minecraft:brick_wall", ++ "minecraft:stone_brick_wall", ++ "minecraft:mossy_stone_brick_wall", ++ "minecraft:nether_brick_wall", ++ "minecraft:end_stone_brick_wall", ++ "minecraft:prismarine_wall", ++ "minecraft:red_sandstone_wall", ++ "minecraft:red_nether_brick_wall" ++ ], ++ "minecraft:colored_torch_bp": [ ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_purple" ++ ], ++ "minecraft:colored_torch_rg": [ ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_green" ++ ], ++ "minecraft:concrete": [ ++ "minecraft:white_concrete", ++ "minecraft:orange_concrete", ++ "minecraft:magenta_concrete", ++ "minecraft:light_blue_concrete", ++ "minecraft:yellow_concrete", ++ "minecraft:lime_concrete", ++ "minecraft:pink_concrete", ++ "minecraft:gray_concrete", ++ "minecraft:light_gray_concrete", ++ "minecraft:cyan_concrete", ++ "minecraft:purple_concrete", ++ "minecraft:blue_concrete", ++ "minecraft:brown_concrete", ++ "minecraft:green_concrete", ++ "minecraft:red_concrete", ++ "minecraft:black_concrete" ++ ], ++ "minecraft:concrete_powder": [ ++ "minecraft:white_concrete_powder", ++ "minecraft:orange_concrete_powder", ++ "minecraft:magenta_concrete_powder", ++ "minecraft:light_blue_concrete_powder", ++ "minecraft:yellow_concrete_powder", ++ "minecraft:lime_concrete_powder", ++ "minecraft:pink_concrete_powder", ++ "minecraft:gray_concrete_powder", ++ "minecraft:light_gray_concrete_powder", ++ "minecraft:cyan_concrete_powder", ++ "minecraft:purple_concrete_powder", ++ "minecraft:blue_concrete_powder", ++ "minecraft:brown_concrete_powder", ++ "minecraft:green_concrete_powder", ++ "minecraft:red_concrete_powder", ++ "minecraft:black_concrete_powder" ++ ], ++ "minecraft:coral": [ ++ "minecraft:tube_coral", ++ "minecraft:brain_coral", ++ "minecraft:bubble_coral", ++ "minecraft:fire_coral", ++ "minecraft:horn_coral", ++ "minecraft:dead_tube_coral", ++ "minecraft:dead_brain_coral", ++ "minecraft:dead_bubble_coral", ++ "minecraft:dead_fire_coral", ++ "minecraft:dead_horn_coral" ++ ], ++ "minecraft:coral_block": [ ++ "minecraft:tube_coral_block", ++ "minecraft:brain_coral_block", ++ "minecraft:bubble_coral_block", ++ "minecraft:fire_coral_block", ++ "minecraft:horn_coral_block", ++ "minecraft:dead_tube_coral_block", ++ "minecraft:dead_brain_coral_block", ++ "minecraft:dead_bubble_coral_block", ++ "minecraft:dead_fire_coral_block", ++ "minecraft:dead_horn_coral_block" ++ ], ++ "minecraft:coral_fan": [ ++ "minecraft:tube_coral_fan", ++ "minecraft:brain_coral_fan", ++ "minecraft:bubble_coral_fan", ++ "minecraft:fire_coral_fan", ++ "minecraft:horn_coral_fan" ++ ], ++ "minecraft:coral_fan_dead": [ ++ "minecraft:dead_tube_coral_fan", ++ "minecraft:dead_brain_coral_fan", ++ "minecraft:dead_bubble_coral_fan", ++ "minecraft:dead_fire_coral_fan", ++ "minecraft:dead_horn_coral_fan" ++ ], ++ "minecraft:dirt": [ ++ "minecraft:dirt", ++ "minecraft:coarse_dirt" ++ ], ++ "minecraft:double_plant": [ ++ "minecraft:sunflower", ++ "minecraft:lilac", ++ "minecraft:tall_grass", ++ "minecraft:large_fern", ++ "minecraft:rose_bush", ++ "minecraft:peony" ++ ], ++ "minecraft:double_stone_block_slab": [ ++ "minecraft:smooth_stone_double_slab", ++ "minecraft:sandstone_double_slab", ++ "minecraft:petrified_oak_double_slab", ++ "minecraft:cobblestone_double_slab", ++ "minecraft:brick_double_slab", ++ "minecraft:stone_brick_double_slab", ++ "minecraft:quartz_double_slab", ++ "minecraft:nether_brick_double_slab" ++ ], ++ "minecraft:double_stone_block_slab2": [ ++ "minecraft:red_sandstone_double_slab", ++ "minecraft:purpur_double_slab", ++ "minecraft:prismarine_double_slab", ++ "minecraft:dark_prismarine_double_slab", ++ "minecraft:prismarine_brick_double_slab", ++ "minecraft:mossy_cobblestone_double_slab", ++ "minecraft:smooth_sandstone_double_slab", ++ "minecraft:red_nether_brick_double_slab" ++ ], ++ "minecraft:double_stone_block_slab3": [ ++ "minecraft:end_stone_brick_double_slab", ++ "minecraft:smooth_red_sandstone_double_slab", ++ "minecraft:polished_andesite_double_slab", ++ "minecraft:andesite_double_slab", ++ "minecraft:diorite_double_slab", ++ "minecraft:polished_diorite_double_slab", ++ "minecraft:granite_double_slab", ++ "minecraft:polished_granite_double_slab" ++ ], ++ "minecraft:double_stone_block_slab4": [ ++ "minecraft:mossy_stone_brick_double_slab", ++ "minecraft:smooth_quartz_double_slab", ++ "minecraft:normal_stone_double_slab", ++ "minecraft:cut_sandstone_double_slab", ++ "minecraft:cut_red_sandstone_double_slab" ++ ], ++ "minecraft:dye": [ ++ "minecraft:ink_sac", ++ "minecraft:red_dye", ++ "minecraft:green_dye", ++ "minecraft:cocoa_beans", ++ "minecraft:lapis_lazuli", ++ "minecraft:purple_dye", ++ "minecraft:cyan_dye", ++ "minecraft:light_gray_dye", ++ "minecraft:gray_dye", ++ "minecraft:pink_dye", ++ "minecraft:lime_dye", ++ "minecraft:yellow_dye", ++ "minecraft:light_blue_dye", ++ "minecraft:magenta_dye", ++ "minecraft:orange_dye", ++ "minecraft:bone_meal", ++ "minecraft:black_dye", ++ "minecraft:brown_dye", ++ "minecraft:blue_dye", ++ "minecraft:white_dye" ++ ], ++ "minecraft:fence": [ ++ "minecraft:oak_fence", ++ "minecraft:spruce_fence", ++ "minecraft:birch_fence", ++ "minecraft:jungle_fence", ++ "minecraft:acacia_fence", ++ "minecraft:dark_oak_fence" ++ ], ++ "minecraft:hard_stained_glass": [ ++ "minecraft:hard_white_stained_glass", ++ "minecraft:hard_orange_stained_glass", ++ "minecraft:hard_magenta_stained_glass", ++ "minecraft:hard_light_blue_stained_glass", ++ "minecraft:hard_yellow_stained_glass", ++ "minecraft:hard_lime_stained_glass", ++ "minecraft:hard_pink_stained_glass", ++ "minecraft:hard_gray_stained_glass", ++ "minecraft:hard_light_gray_stained_glass", ++ "minecraft:hard_cyan_stained_glass", ++ "minecraft:hard_purple_stained_glass", ++ "minecraft:hard_blue_stained_glass", ++ "minecraft:hard_brown_stained_glass", ++ "minecraft:hard_green_stained_glass", ++ "minecraft:hard_red_stained_glass", ++ "minecraft:hard_black_stained_glass" ++ ], ++ "minecraft:hard_stained_glass_pane": [ ++ "minecraft:hard_white_stained_glass_pane", ++ "minecraft:hard_orange_stained_glass_pane", ++ "minecraft:hard_magenta_stained_glass_pane", ++ "minecraft:hard_light_blue_stained_glass_pane", ++ "minecraft:hard_yellow_stained_glass_pane", ++ "minecraft:hard_lime_stained_glass_pane", ++ "minecraft:hard_pink_stained_glass_pane", ++ "minecraft:hard_gray_stained_glass_pane", ++ "minecraft:hard_light_gray_stained_glass_pane", ++ "minecraft:hard_cyan_stained_glass_pane", ++ "minecraft:hard_purple_stained_glass_pane", ++ "minecraft:hard_blue_stained_glass_pane", ++ "minecraft:hard_brown_stained_glass_pane", ++ "minecraft:hard_green_stained_glass_pane", ++ "minecraft:hard_red_stained_glass_pane", ++ "minecraft:hard_black_stained_glass_pane" ++ ], ++ "minecraft:leaves": [ ++ "minecraft:oak_leaves", ++ "minecraft:spruce_leaves", ++ "minecraft:birch_leaves", ++ "minecraft:jungle_leaves" ++ ], ++ "minecraft:leaves2": [ ++ "minecraft:acacia_leaves", ++ "minecraft:dark_oak_leaves" ++ ], ++ "minecraft:light_block": [ ++ "minecraft:light_block_0", ++ "minecraft:light_block_1", ++ "minecraft:light_block_2", ++ "minecraft:light_block_3", ++ "minecraft:light_block_4", ++ "minecraft:light_block_5", ++ "minecraft:light_block_6", ++ "minecraft:light_block_7", ++ "minecraft:light_block_8", ++ "minecraft:light_block_9", ++ "minecraft:light_block_10", ++ "minecraft:light_block_11", ++ "minecraft:light_block_12", ++ "minecraft:light_block_13", ++ "minecraft:light_block_14", ++ "minecraft:light_block_15" ++ ], ++ "minecraft:log": [ ++ "minecraft:oak_log", ++ "minecraft:spruce_log", ++ "minecraft:birch_log", ++ "minecraft:jungle_log" ++ ], ++ "minecraft:log2": [ ++ "minecraft:acacia_log", ++ "minecraft:dark_oak_log" ++ ], ++ "minecraft:monster_egg": [ ++ "minecraft:infested_stone", ++ "minecraft:infested_cobblestone", ++ "minecraft:infested_stone_bricks", ++ "minecraft:infested_mossy_stone_bricks", ++ "minecraft:infested_cracked_stone_bricks", ++ "minecraft:infested_chiseled_stone_bricks" ++ ], ++ "minecraft:planks": [ ++ "minecraft:oak_planks", ++ "minecraft:spruce_planks", ++ "minecraft:birch_planks", ++ "minecraft:jungle_planks", ++ "minecraft:acacia_planks", ++ "minecraft:dark_oak_planks" ++ ], ++ "minecraft:prismarine": [ ++ "minecraft:prismarine", ++ "minecraft:dark_prismarine", ++ "minecraft:prismarine_bricks" ++ ], ++ "minecraft:purpur_block": [ ++ "minecraft:purpur_block", ++ "minecraft:deprecated_purpur_block_1", ++ "minecraft:purpur_pillar", ++ "minecraft:deprecated_purpur_block_2" ++ ], ++ "minecraft:quartz_block": [ ++ "minecraft:quartz_block", ++ "minecraft:chiseled_quartz_block", ++ "minecraft:quartz_pillar", ++ "minecraft:smooth_quartz" ++ ], ++ "minecraft:red_flower": [ ++ "minecraft:poppy", ++ "minecraft:blue_orchid", ++ "minecraft:allium", ++ "minecraft:azure_bluet", ++ "minecraft:red_tulip", ++ "minecraft:orange_tulip", ++ "minecraft:white_tulip", ++ "minecraft:pink_tulip", ++ "minecraft:oxeye_daisy", ++ "minecraft:cornflower", ++ "minecraft:lily_of_the_valley" ++ ], ++ "minecraft:red_sandstone": [ ++ "minecraft:red_sandstone", ++ "minecraft:chiseled_red_sandstone", ++ "minecraft:cut_red_sandstone", ++ "minecraft:smooth_red_sandstone" ++ ], ++ "minecraft:sand": [ ++ "minecraft:sand", ++ "minecraft:red_sand" ++ ], ++ "minecraft:sandstone": [ ++ "minecraft:sandstone", ++ "minecraft:chiseled_sandstone", ++ "minecraft:cut_sandstone", ++ "minecraft:smooth_sandstone" ++ ], ++ "minecraft:sapling": [ ++ "minecraft:oak_sapling", ++ "minecraft:spruce_sapling", ++ "minecraft:birch_sapling", ++ "minecraft:jungle_sapling", ++ "minecraft:acacia_sapling", ++ "minecraft:dark_oak_sapling" ++ ], ++ "minecraft:shulker_box": [ ++ "minecraft:white_shulker_box", ++ "minecraft:orange_shulker_box", ++ "minecraft:magenta_shulker_box", ++ "minecraft:light_blue_shulker_box", ++ "minecraft:yellow_shulker_box", ++ "minecraft:lime_shulker_box", ++ "minecraft:pink_shulker_box", ++ "minecraft:gray_shulker_box", ++ "minecraft:light_gray_shulker_box", ++ "minecraft:cyan_shulker_box", ++ "minecraft:purple_shulker_box", ++ "minecraft:blue_shulker_box", ++ "minecraft:brown_shulker_box", ++ "minecraft:green_shulker_box", ++ "minecraft:red_shulker_box", ++ "minecraft:black_shulker_box" ++ ], ++ "minecraft:skull": [ ++ "minecraft:skeleton_skull", ++ "minecraft:wither_skeleton_skull", ++ "minecraft:zombie_head", ++ "minecraft:player_head", ++ "minecraft:creeper_head", ++ "minecraft:dragon_head", ++ "minecraft:piglin_head" ++ ], ++ "minecraft:spawn_egg": [ ++ "minecraft:chicken_spawn_egg", ++ "minecraft:cow_spawn_egg", ++ "minecraft:pig_spawn_egg", ++ "minecraft:sheep_spawn_egg", ++ "minecraft:wolf_spawn_egg", ++ "minecraft:mooshroom_spawn_egg", ++ "minecraft:creeper_spawn_egg", ++ "minecraft:enderman_spawn_egg", ++ "minecraft:silverfish_spawn_egg", ++ "minecraft:skeleton_spawn_egg", ++ "minecraft:slime_spawn_egg", ++ "minecraft:spider_spawn_egg", ++ "minecraft:zombie_spawn_egg", ++ "minecraft:zombie_pigman_spawn_egg", ++ "minecraft:villager_spawn_egg", ++ "minecraft:squid_spawn_egg", ++ "minecraft:ocelot_spawn_egg", ++ "minecraft:witch_spawn_egg", ++ "minecraft:bat_spawn_egg", ++ "minecraft:ghast_spawn_egg", ++ "minecraft:magma_cube_spawn_egg", ++ "minecraft:blaze_spawn_egg", ++ "minecraft:cave_spider_spawn_egg", ++ "minecraft:horse_spawn_egg", ++ "minecraft:rabbit_spawn_egg", ++ "minecraft:endermite_spawn_egg", ++ "minecraft:guardian_spawn_egg", ++ "minecraft:stray_spawn_egg", ++ "minecraft:husk_spawn_egg", ++ "minecraft:wither_skeleton_spawn_egg", ++ "minecraft:donkey_spawn_egg", ++ "minecraft:mule_spawn_egg", ++ "minecraft:skeleton_horse_spawn_egg", ++ "minecraft:zombie_horse_spawn_egg", ++ "minecraft:shulker_spawn_egg", ++ "minecraft:npc_spawn_egg", ++ "minecraft:elder_guardian_spawn_egg", ++ "minecraft:polar_bear_spawn_egg", ++ "minecraft:llama_spawn_egg", ++ "minecraft:trader_llama_spawn_egg", ++ "minecraft:vindicator_spawn_egg", ++ "minecraft:evoker_spawn_egg", ++ "minecraft:vex_spawn_egg", ++ "minecraft:zombie_villager_spawn_egg", ++ "minecraft:parrot_spawn_egg", ++ "minecraft:tropical_fish_spawn_egg", ++ "minecraft:cod_spawn_egg", ++ "minecraft:pufferfish_spawn_egg", ++ "minecraft:salmon_spawn_egg", ++ "minecraft:drowned_spawn_egg", ++ "minecraft:dolphin_spawn_egg", ++ "minecraft:turtle_spawn_egg", ++ "minecraft:phantom_spawn_egg", ++ "minecraft:agent_spawn_egg", ++ "minecraft:cat_spawn_egg", ++ "minecraft:panda_spawn_egg", ++ "minecraft:fox_spawn_egg", ++ "minecraft:bee_spawn_egg", ++ "minecraft:wandering_trader_spawn_egg", ++ "minecraft:pillager_spawn_egg", ++ "minecraft:ravager_spawn_egg", ++ "minecraft:piglin_spawn_egg", ++ "minecraft:piglin_brute_spawn_egg", ++ "minecraft:hoglin_spawn_egg", ++ "minecraft:zoglin_spawn_egg", ++ "minecraft:strider_spawn_egg", ++ "minecraft:goat_spawn_egg", ++ "minecraft:axolotl_bucket", ++ "minecraft:glow_squid_spawn_egg", ++ "minecraft:iron_golem_spawn_egg", ++ "minecraft:snow_golem_spawn_egg", ++ "minecraft:ender_dragon_spawn_egg", ++ "minecraft:wither_spawn_egg", ++ "minecraft:frog_spawn_egg", ++ "minecraft:tadpole_spawn_egg", ++ "minecraft:allay_spawn_egg", ++ "minecraft:warden_spawn_egg", ++ "minecraft:camel_spawn_egg", ++ "minecraft:sniffer_spawn_egg", ++ "minecraft:breeze_spawn_egg", ++ "minecraft:armadillo_spawn_egg", ++ "minecraft:bogged_spawn_egg", ++ "minecraft:creaking_spawn_egg", ++ "minecraft:happy_ghast_spawn_egg", ++ "minecraft:copper_golem_spawn_egg", ++ "minecraft:nautilus_spawn_egg", ++ "minecraft:sulfur_cube_spawn_egg" ++ ], ++ "minecraft:sponge": [ ++ "minecraft:sponge", ++ "minecraft:wet_sponge" ++ ], ++ "minecraft:stained_glass": [ ++ "minecraft:white_stained_glass", ++ "minecraft:orange_stained_glass", ++ "minecraft:magenta_stained_glass", ++ "minecraft:light_blue_stained_glass", ++ "minecraft:yellow_stained_glass", ++ "minecraft:lime_stained_glass", ++ "minecraft:pink_stained_glass", ++ "minecraft:gray_stained_glass", ++ "minecraft:light_gray_stained_glass", ++ "minecraft:cyan_stained_glass", ++ "minecraft:purple_stained_glass", ++ "minecraft:blue_stained_glass", ++ "minecraft:brown_stained_glass", ++ "minecraft:green_stained_glass", ++ "minecraft:red_stained_glass", ++ "minecraft:black_stained_glass" ++ ], ++ "minecraft:stained_glass_pane": [ ++ "minecraft:white_stained_glass_pane", ++ "minecraft:orange_stained_glass_pane", ++ "minecraft:magenta_stained_glass_pane", ++ "minecraft:light_blue_stained_glass_pane", ++ "minecraft:yellow_stained_glass_pane", ++ "minecraft:lime_stained_glass_pane", ++ "minecraft:pink_stained_glass_pane", ++ "minecraft:gray_stained_glass_pane", ++ "minecraft:light_gray_stained_glass_pane", ++ "minecraft:cyan_stained_glass_pane", ++ "minecraft:purple_stained_glass_pane", ++ "minecraft:blue_stained_glass_pane", ++ "minecraft:brown_stained_glass_pane", ++ "minecraft:green_stained_glass_pane", ++ "minecraft:red_stained_glass_pane", ++ "minecraft:black_stained_glass_pane" ++ ], ++ "minecraft:stained_hardened_clay": [ ++ "minecraft:white_terracotta", ++ "minecraft:orange_terracotta", ++ "minecraft:magenta_terracotta", ++ "minecraft:light_blue_terracotta", ++ "minecraft:yellow_terracotta", ++ "minecraft:lime_terracotta", ++ "minecraft:pink_terracotta", ++ "minecraft:gray_terracotta", ++ "minecraft:light_gray_terracotta", ++ "minecraft:cyan_terracotta", ++ "minecraft:purple_terracotta", ++ "minecraft:blue_terracotta", ++ "minecraft:brown_terracotta", ++ "minecraft:green_terracotta", ++ "minecraft:red_terracotta", ++ "minecraft:black_terracotta" ++ ], ++ "minecraft:stone": [ ++ "minecraft:stone", ++ "minecraft:granite", ++ "minecraft:polished_granite", ++ "minecraft:diorite", ++ "minecraft:polished_diorite", ++ "minecraft:andesite", ++ "minecraft:polished_andesite" ++ ], ++ "minecraft:stone_block_slab": [ ++ "minecraft:smooth_stone_slab", ++ "minecraft:sandstone_slab", ++ "minecraft:petrified_oak_slab", ++ "minecraft:cobblestone_slab", ++ "minecraft:brick_slab", ++ "minecraft:stone_brick_slab", ++ "minecraft:quartz_slab", ++ "minecraft:nether_brick_slab" ++ ], ++ "minecraft:stone_block_slab2": [ ++ "minecraft:red_sandstone_slab", ++ "minecraft:purpur_slab", ++ "minecraft:prismarine_slab", ++ "minecraft:dark_prismarine_slab", ++ "minecraft:prismarine_brick_slab", ++ "minecraft:mossy_cobblestone_slab", ++ "minecraft:smooth_sandstone_slab", ++ "minecraft:red_nether_brick_slab" ++ ], ++ "minecraft:stone_block_slab3": [ ++ "minecraft:end_stone_brick_slab", ++ "minecraft:smooth_red_sandstone_slab", ++ "minecraft:polished_andesite_slab", ++ "minecraft:andesite_slab", ++ "minecraft:diorite_slab", ++ "minecraft:polished_diorite_slab", ++ "minecraft:granite_slab", ++ "minecraft:polished_granite_slab" ++ ], ++ "minecraft:stone_block_slab4": [ ++ "minecraft:mossy_stone_brick_slab", ++ "minecraft:smooth_quartz_slab", ++ "minecraft:normal_stone_slab", ++ "minecraft:cut_sandstone_slab", ++ "minecraft:cut_red_sandstone_slab" ++ ], ++ "minecraft:stonebrick": [ ++ "minecraft:stone_bricks", ++ "minecraft:mossy_stone_bricks", ++ "minecraft:cracked_stone_bricks", ++ "minecraft:chiseled_stone_bricks", ++ "minecraft:stone_bricks" ++ ], ++ "minecraft:tallgrass": [ ++ "minecraft:short_grass", ++ "minecraft:short_grass", ++ "minecraft:fern", ++ "minecraft:fern" ++ ], ++ "minecraft:tnt": [ ++ "minecraft:tnt", ++ "minecraft:underwater_tnt" ++ ], ++ "minecraft:wood": [ ++ "minecraft:oak_wood", ++ "minecraft:spruce_wood", ++ "minecraft:birch_wood", ++ "minecraft:jungle_wood", ++ "minecraft:acacia_wood", ++ "minecraft:dark_oak_wood", ++ "minecraft:stripped_oak_wood", ++ "minecraft:stripped_spruce_wood", ++ "minecraft:stripped_birch_wood", ++ "minecraft:stripped_jungle_wood", ++ "minecraft:stripped_acacia_wood", ++ "minecraft:stripped_dark_oak_wood" ++ ], ++ "minecraft:wooden_slab": [ ++ "minecraft:oak_slab", ++ "minecraft:spruce_slab", ++ "minecraft:birch_slab", ++ "minecraft:jungle_slab", ++ "minecraft:acacia_slab", ++ "minecraft:dark_oak_slab" ++ ], ++ "minecraft:wool": [ ++ "minecraft:white_wool", ++ "minecraft:orange_wool", ++ "minecraft:magenta_wool", ++ "minecraft:light_blue_wool", ++ "minecraft:yellow_wool", ++ "minecraft:lime_wool", ++ "minecraft:pink_wool", ++ "minecraft:gray_wool", ++ "minecraft:light_gray_wool", ++ "minecraft:cyan_wool", ++ "minecraft:purple_wool", ++ "minecraft:blue_wool", ++ "minecraft:brown_wool", ++ "minecraft:green_wool", ++ "minecraft:red_wool", ++ "minecraft:black_wool" ++ ] ++ }, ++ "format": 1, ++ "protocol": 2193, ++ "redirects": { ++ "minecraft:appleenchanted": "enchanted_golden_apple", ++ "minecraft:carrotonastick": "carrot_on_a_stick", ++ "minecraft:chain": "minecraft:iron_chain", ++ "minecraft:chorus_fruit_popped": "popped_chorus_fruit", ++ "minecraft:clownfish": "tropical_fish", ++ "minecraft:concretepowder": "minecraft:concrete_powder", ++ "minecraft:cooked_fish": "cooked_cod", ++ "minecraft:darkoak_sign": "dark_oak_sign", ++ "minecraft:double_stone_slab": "minecraft:stone_block_slab", ++ "minecraft:double_stone_slab2": "minecraft:stone_block_slab2", ++ "minecraft:double_stone_slab3": "minecraft:stone_block_slab3", ++ "minecraft:double_stone_slab4": "minecraft:stone_block_slab4", ++ "minecraft:emptymap": "empty_map", ++ "minecraft:fireball": "fire_charge", ++ "minecraft:fireworks": "firework_rocket", ++ "minecraft:fireworkscharge": "firework_star", ++ "minecraft:fish": "cod", ++ "minecraft:glazedterracotta.black": "minecraft:black_glazed_terracotta", ++ "minecraft:glazedterracotta.blue": "minecraft:blue_glazed_terracotta", ++ "minecraft:glazedterracotta.brown": "minecraft:brown_glazed_terracotta", ++ "minecraft:glazedterracotta.cyan": "minecraft:cyan_glazed_terracotta", ++ "minecraft:glazedterracotta.gray": "minecraft:gray_glazed_terracotta", ++ "minecraft:glazedterracotta.green": "minecraft:green_glazed_terracotta", ++ "minecraft:glazedterracotta.light_blue": "minecraft:light_blue_glazed_terracotta", ++ "minecraft:glazedterracotta.lime": "minecraft:lime_glazed_terracotta", ++ "minecraft:glazedterracotta.magenta": "minecraft:magenta_glazed_terracotta", ++ "minecraft:glazedterracotta.orange": "minecraft:orange_glazed_terracotta", ++ "minecraft:glazedterracotta.pink": "minecraft:pink_glazed_terracotta", ++ "minecraft:glazedterracotta.purple": "minecraft:purple_glazed_terracotta", ++ "minecraft:glazedterracotta.red": "minecraft:red_glazed_terracotta", ++ "minecraft:glazedterracotta.silver": "minecraft:silver_glazed_terracotta", ++ "minecraft:glazedterracotta.white": "minecraft:white_glazed_terracotta", ++ "minecraft:glazedterracotta.yellow": "minecraft:yellow_glazed_terracotta", ++ "minecraft:grass": "minecraft:grass_block", ++ "minecraft:horsearmordiamond": "diamond_horse_armor", ++ "minecraft:horsearmorgold": "golden_horse_armor", ++ "minecraft:horsearmoriron": "iron_horse_armor", ++ "minecraft:horsearmorleather": "leather_horse_armor", ++ "minecraft:invisiblebedrock": "minecraft:invisible_bedrock", ++ "minecraft:lodestonecompass": "lodestone_compass", ++ "minecraft:map": "filled_map", ++ "minecraft:melon": "melon_slice", ++ "minecraft:movingblock": "minecraft:moving_block", ++ "minecraft:muttoncooked": "cooked_mutton", ++ "minecraft:muttonraw": "mutton", ++ "minecraft:nametag": "minecraft:name_tag", ++ "minecraft:netherstar": "nether_star", ++ "minecraft:photo": "minecraft:photo_item", ++ "minecraft:pistonarmcollision": "minecraft:piston_arm_collision", ++ "minecraft:prismarineshard": "prismarine_shard", ++ "minecraft:real_double_stone_slab": "minecraft:double_stone_block_slab", ++ "minecraft:real_double_stone_slab2": "minecraft:double_stone_block_slab2", ++ "minecraft:real_double_stone_slab3": "minecraft:double_stone_block_slab3", ++ "minecraft:real_double_stone_slab4": "minecraft:double_stone_block_slab4", ++ "minecraft:record_11": "music_disc_11", ++ "minecraft:record_13": "music_disc_13", ++ "minecraft:record_5": "music_disc_5", ++ "minecraft:record_blocks": "music_disc_blocks", ++ "minecraft:record_bounce": "music_disc_bounce", ++ "minecraft:record_cat": "music_disc_cat", ++ "minecraft:record_chirp": "music_disc_chirp", ++ "minecraft:record_creator": "music_disc_creator", ++ "minecraft:record_creator_music_box": "music_disc_creator_music_box", ++ "minecraft:record_far": "music_disc_far", ++ "minecraft:record_lava_chicken": "music_disc_lava_chicken", ++ "minecraft:record_mall": "music_disc_mall", ++ "minecraft:record_mellohi": "music_disc_mellohi", ++ "minecraft:record_otherside": "music_disc_otherside", ++ "minecraft:record_pigstep": "music_disc_pigstep", ++ "minecraft:record_precipice": "music_disc_precipice", ++ "minecraft:record_relic": "music_disc_relic", ++ "minecraft:record_stal": "music_disc_stal", ++ "minecraft:record_strad": "music_disc_strad", ++ "minecraft:record_tears": "music_disc_tears", ++ "minecraft:record_wait": "music_disc_wait", ++ "minecraft:record_ward": "music_disc_ward", ++ "minecraft:reeds": "sugar_cane", ++ "minecraft:scute": "turtle_scute", ++ "minecraft:sealantern": "minecraft:sea_lantern", ++ "minecraft:sign": "oak_sign", ++ "minecraft:speckled_melon": "glistering_melon_slice", ++ "minecraft:stickypistonarmcollision": "minecraft:sticky_piston_arm_collision", ++ "minecraft:stone_slab": "minecraft:stone_block_slab", ++ "minecraft:stone_slab2": "minecraft:stone_block_slab2", ++ "minecraft:stone_slab3": "minecraft:stone_block_slab3", ++ "minecraft:stone_slab4": "minecraft:stone_block_slab4", ++ "minecraft:totem": "totem_of_undying", ++ "minecraft:turtle_shell_piece": "turtle_scute", ++ "minecraft:yellow_flower": "minecraft:dandelion" ++ } ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptionsTest.java b/src/test/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptionsTest.java new file mode 100644 index 000000000..71312816c @@ -7413,6 +8314,48 @@ index 000000000..fbc7a40e4 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryAliasesTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryAliasesTest.java +new file mode 100644 +index 000000000..a2cef8438 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryAliasesTest.java +@@ -0,0 +1,36 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model; ++ ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import org.junit.jupiter.api.Test; ++ ++import java.nio.file.Files; ++import java.nio.file.Path; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assumptions.assumeTrue; ++ ++final class ItemRegistryAliasesTest { ++ ++ @Test ++ void matchesActualNativeRegistryAliasGetters() throws Exception { ++ final String path = System.getenv("STACKANVIL_NATIVE_ITEM_ALIASES"); ++ assumeTrue(path != null && !path.isBlank(), "Native ItemRegistry alias corpus is optional"); ++ final var registry = new ItemRegistry(new ItemEntry[0]); ++ try (var reader = Files.newBufferedReader(Path.of(path))) { ++ final var reference = JsonParser.parseReader(reader).getAsJsonObject().getAsJsonObject("nativeIngredientRegistry"); ++ for (final var value : reference.getAsJsonArray("lookupRows")) { ++ final var row = value.getAsJsonObject(); ++ final String name = row.get("name").getAsString(); ++ assertEquals(row.get("hasComplexAlias").getAsBoolean(), registry.hasComplexAlias(name), name); ++ assertEquals(row.getAsJsonArray("members").asList().stream().map(member -> member.getAsString()).toList(), ++ registry.complexAliasMembers(name), name); ++ } ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java new file mode 100644 index 000000000..284e08781 diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index 01fdefd9..ed0fc6cb 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -512,3 +512,27 @@ The full four-project build passes 2,859 tests, with 163 optional skips and no f All 1,493 core classes match both embedded copies; all 1,191 client classes match the tested add-on. The packaged core matches both new corpora and three focused integration regressions. Nine deliberate changes fail value or lookup-order comparisons. All retained packaged comparisons also pass. + + +## Constructor-owned ItemRegistry aliases + +Expose exact alias admission and ordered member names through the connection registry. +Load the native ItemRegistry's 89 redirects and 60 complex aliases with 561 members. +Apply one redirect before complex lookup. Preserve exact names and member order. +Require the pinned asset and protocol; unregistered names retain the native empty-vector result. + +Complete original ItemRegistry initialization follows native block/state construction under the recorded default version and Experiments. +The empty parsed network vector establishes pre-packet constructor data, not actual server admission. +All native input, registry, TLS, static and CRT cleanup completes with no remaining allocations or held locks. +Item alias registrations differ from block aliases and restriction groups, including wood order, item-only groups and slab redirects. +The [ItemRegistry alias reference](../../../docs/native-player-inventory.md#constructor-owned-itemregistry-aliases) records the evidence and limits. + +Ordered member names do not replace auxiliary callbacks or live definition ownership. +Ordinary named lookup rewriting, member/representative lookup, block fallback, complete Java registry construction and installed availability remain unfinished. + +The packaged connection registry matches all 2,127 original getter controls, including 86 registered results and 767 member-name comparisons. +The controls query every constructed factory key and both names in each native redirect. +Three deliberate changes fail value comparisons: ignored redirects, reversed member order and substituted block tables. +The retained descriptor, counter, source matcher, search, unlock and progressive-model comparisons also pass. +All four builds pass with 2,860 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. +All 1,495 core classes and the alias asset match both embedded copies; all 1,191 client classes match the tested add-on. From 8f8733327904d0ef256867c87696b5852927ca63 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:28:22 +0200 Subject: [PATCH 12/12] feat(crafting): preserve native item name and metadata rewrites Parse legacy spellings and preserve the constructor-owned metadata callbacks separately from ordered alias members. Resolve a redirect target through its callback, clear auxiliary data at the native phase and retain exact signed-word and byte-guard behavior. Fold the implementation and callback data into the crafting patch. Verify packaged results against complete original parser and registry rewrite controls, with explicit guards for unsupported native vector indexing. Component-priority definition ownership, Item construction and installed availability remain unfinished. --- docs/native-player-inventory.md | 49 +- ...edrock-recipes-and-place-ingredients.patch | 1118 +++++++++++++++-- ...edrock-recipes-and-place-ingredients.pr.md | 25 + 3 files changed, 1102 insertions(+), 90 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 888a69c3..e09728a5 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -737,10 +737,10 @@ It does not establish normal server packet contents or wire admission. All 60 aliases also have constructed native factory definitions at that point. Input, registry, TLS, static and CRT cleanup finish with no remaining native allocations or held locks. -These APIs expose names, not resolved Item definitions or metadata callbacks. +The alias predicates and member getters expose names. The model below supplies metadata callback rewrites separately. Ordinary named lookup can use a redirect with metadata, then a complex-alias callback, and reset the auxiliary after that callback. Each alias's ordered members do not define that callback's metadata mapping. -Live member and representative ownership, ordinary lookup rewriting, block fallback and complete Java registry construction remain unfinished. +Live member and representative ownership, full ordinary definition lookup, block fallback and complete Java registry construction remain unfinished. The rich descriptor model still requires that complete registry context before installed recipe evaluation can use it. All four original reports and all five goal areas remain active. @@ -750,3 +750,48 @@ Three deliberate changes fail value comparisons: ignored redirects, reversed mem The retained descriptor, counter, source matcher, search, unlock and progressive-model comparisons also pass. All four builds pass with 2,860 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. All 1,495 core classes and the alias asset match both embedded copies; all 1,191 client classes match the tested add-on. + + +## Native name parsing and metadata rewrites + +`NativeItemName.parse` produces a name and auxiliary value before definition lookup. +It preserves ASCII case folding, native colon fields, tile-prefix removal and numeric suffix rules. +Empty input produces no query. Invalid or out-of-range numeric suffixes retain the incoming auxiliary. +The second-field digit test reads a C string from that field's start, including later colon fields until a NUL. +A namespaced third field accepts a C-locale numeric prefix. It does not require the complete field to be numeric. + +`ItemRegistry.rewriteLegacyName` accepts an already parsed query after the caller checks component definitions. +It applies one name redirect, or the metadata-qualified redirect when no direct redirect exists. +It parses the redirect target and then applies that target's complex callback. +It does not follow another name redirect. +For example, `minecraft:double_stone_slab` with auxiliary 3 becomes `minecraft:cobblestone_slab` with auxiliary 0. +Stopping at `minecraft:stone_block_slab` would omit the callback. +Each callback clears the auxiliary after selecting its target, even when the selected name remains equal. +Keep deferred wildcard alias selection on its earlier raw-name path. + +The pinned asset now requires all 60 callback definitions. +Thirty-eight callbacks index their registered members. The other 22 use 439 explicit metadata values and native default targets. +Preserve signed-short callback arguments, the low-byte guards and the signed remainder rules. +Missing callback data fails instead of selecting an inferred member rule. +Some high-word metadata combinations pass a native byte guard but index outside its vectors. +The Java fallback rejects those unsupported inputs explicitly. Complete caller admission remains unfinished. +These guards do not establish a normal native result for the excluded inputs. + +The reference executes complete parser `0x140ad1110` and name/auxiliary rewriter `0x1434735e0` after actual block/state and ItemRegistry construction. +It supplies read-only UTF-8 caller backing, allocation services and ordinary C-runtime imports. +The supplied numeric import follows the [Windows C-runtime conversion contract](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strtol-wcstol-strtol-l-wcstol-l?view=msvc-170). +Native conversion dispatch, range checks, name parsing, redirects, alias callbacks and owned string retirement still execute. +The capture covers 2,501 parser controls and 22,917 rewrites. It records 46 unsupported callback input combinations separately. +Input, registry, TLS, static and CRT cleanup finishes with no remaining native allocations or held locks. +A separate primitive parser capture also matches 552 controls. + +These APIs do not resolve live Item definitions or replace full recipe admission. +Component-map precedence, ordinary/block definition lookup, member/representative ownership, actual Item construction and complete registry replacement remain unfinished. +The rich descriptor Registry still requires those inputs before installed availability can use it. +All four original reports and all five goal areas remain active. + +The packaged core matches all 2,501 parser controls, 22,917 rewrites and 46 explicit unsupported-index guards. +Nine deliberate parser or callback changes fail native value comparisons. +The 2,127 alias getter controls and three retained alias regressions also pass, along with the earlier descriptor, counter, search, unlock and progressive comparisons. +All four builds pass with 2,862 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. +All 1,498 core classes and the alias asset match both embedded copies; all 1,191 client classes match the tested add-on. diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 5325a667..dcd9e273 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From de915fc6aa2b306b1f57cda915278e15f70db07a Mon Sep 17 00:00:00 2001 +From 893cfc84d58fd9c0b6d818c9a7f21cd0e5f3f94a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(crafting): expose Bedrock recipes and place ingredients @@ -517,84 +517,112 @@ with no remaining native allocations or held locks. Item aliases differ from block groups in membership, ordering and redirect destinations. Full auxiliary callbacks, member/representative resolution, underlying registry construction on Java and installed availability remain unfinished. + +Parse native legacy names before definition lookup. Preserve ASCII case +folding, colon-field and C-string suffix behavior, tile-prefix removal, +C-locale numeric prefixes and the accepted auxiliary range. Keep parsed +queries separate from component-definition precedence and Item ownership. + +Extend the owning ItemRegistry data with all 60 metadata callbacks: +38 member-indexed callbacks and 22 explicit mappings with 439 values. +Resolve one redirect, parse its target, then apply the selected callback. +Callbacks clear auxiliary data after selecting the target. Keep member +order distinct from metadata selection and preserve signed-short callback +arguments, native default targets and low-byte/full-word index rules. + +Evidence: complete original parser140ad1110 and registry rewrite1434735e0 +match 2501 parser controls and 22917 name/auxiliary rewrites after actual +native block/state and ItemRegistry construction. Native result strings, +input, registry, TLS, static and CRT cleanup complete with no remaining +native allocations or held locks. Read-only UTF-8 caller backing, ordinary +Windows C-locale CRT primitives and allocation services remain supplied +boundaries; no game parsing, range decision or alias result is hosted. + +Some high-word metadata values pass a native low-byte guard but index +outside callback vectors. Reject these unsupported inputs explicitly in +Java; there is no normal native lookup result to fabricate. Complete +caller admission, live component/member/representative definitions, Item +construction, underlying lookup and installed availability remain open. --- - .../api/model/NativeFurnaceOptions.java | 44 + - .../container/CookingRecipeAvailability.java | 30 + - .../model/container/CookingRecipeCatalog.java | 81 ++ - .../model/container/CookingRecipeInputs.java | 53 ++ - .../model/container/CookingRecipeSources.java | 80 ++ - .../model/container/CraftingOutputPlan.java | 109 +++ - .../model/container/CraftingPlacement.java | 229 +++++ - .../container/FurnaceRecipePlacement.java | 219 +++++ - .../NativeCookingRecipeSelection.java | 206 +++++ - .../model/container/NativeRecipeCounter.java | 163 ++++ - .../container/NativeRecipeDescriptor.java | 166 ++++ - .../container/RecipeIngredientMatcher.java | 38 + - .../block/CraftingTableContainer.java | 100 ++- - .../container/block/FurnaceContainer.java | 209 +++++ - .../model/container/player/HudContainer.java | 3 + - .../container/player/InventoryContainer.java | 190 ++++- - .../viabedrock/api/util/PacketFactory.java | 2 + - .../viabedrock/protocol/BedrockProtocol.java | 2 + - .../protocol/ServerboundBedrockPackets.java | 1 + - .../protocol/model/BuiltinItemAliases.java | 62 ++ - .../protocol/model/FurnaceOptions.java | 17 + - .../protocol/model/InventoryOptions.java | 10 + - .../protocol/model/ItemRegistry.java | 10 + - .../protocol/model/recipe/ItemDescriptor.java | 13 +- - .../protocol/model/recipe/MolangTagQuery.java | 64 ++ - .../protocol/model/recipe/MultiRecipe.java | 46 +- - .../model/recipe/RecipeOutputKey.java | 9 + - .../protocol/model/recipe/ShapedRecipe.java | 65 +- - .../model/recipe/ShapelessRecipe.java | 6 +- - .../protocol/model/recipe/SmithingRecipe.java | 4 +- - .../protocol/packet/InventoryPackets.java | 146 +++- - .../protocol/packet/JoinPackets.java | 1 + - .../protocol/packet/MultiStatePackets.java | 55 ++ - .../protocol/rewriter/ItemRewriter.java | 118 ++- - .../protocol/storage/CraftingDataStorage.java | 3 +- - .../protocol/storage/CraftingDataTracker.java | 626 ++++++++++++-- - .../storage/InventoryOptionsStorage.java | 187 ++++ - .../storage/InventoryRequestTracker.java | 7 + - .../protocol/storage/InventoryTracker.java | 47 ++ - .../storage/NativeCookingUiState.java | 502 +++++++++++ - .../protocol/types/BedrockTypes.java | 2 + - .../types/item/BedrockRecipeResultType.java | 23 + - .../types/model/FurnaceOptionsType.java | 34 + - .../types/model/InventoryOptionsType.java | 35 + - .../types/recipe/CraftingDataReader.java | 66 +- - .../viabedrock/data/bedrock/item_aliases.json | 778 +++++++++++++++++ - .../api/model/NativeFurnaceOptionsTest.java | 57 ++ - .../CookingRecipeAvailabilityTest.java | 94 +++ - .../container/CookingRecipeInputsTest.java | 180 ++++ - .../container/CookingRecipeSourcesTest.java | 146 ++++ - .../container/CraftingOutputPlanTest.java | 84 ++ - .../container/CraftingPlacementTest.java | 161 ++++ - .../container/FurnaceRecipePlacementTest.java | 418 +++++++++ - .../NativeRecipeCounterFixtureTest.java | 132 +++ - .../container/NativeRecipeCounterTest.java | 66 ++ - .../container/NativeRecipeDescriptorTest.java | 305 +++++++ - .../player/InventoryContainerTest.java | 131 +++ - .../model/ItemRegistryAliasesTest.java | 36 + - .../model/recipe/MolangTagQueryTest.java | 53 ++ - .../model/recipe/RecipeOwnershipTest.java | 52 ++ - .../packet/FurnaceOptionsPacketTest.java | 162 ++++ - .../packet/InventoryOptionsPacketTest.java | 152 ++++ - .../NativeFurnaceOptionsPacketTest.java | 214 +++++ - .../ResolvedRecipeIngredientTest.java | 282 +++++++ - .../storage/CookingRecipeBookTest.java | 154 ++++ - .../protocol/storage/CraftingCursorTest.java | 158 ++++ - .../storage/CraftingDataTrackerTest.java | 409 +++++++++ - .../storage/CraftingRecipeSourcesTest.java | 128 +++ - .../storage/FurnaceRecipeRequestTest.java | 797 ++++++++++++++++++ - .../storage/NativeCookingUiStateTest.java | 619 ++++++++++++++ - .../storage/NativeRecipeOutputUnlockTest.java | 166 ++++ - ...NativeRecipeRegistrationReferenceTest.java | 104 +++ - .../item/BedrockRecipeResultTypeTest.java | 106 +++ - .../types/model/FurnaceOptionsTypeTest.java | 75 ++ - .../types/model/InventoryOptionsTypeTest.java | 46 + - .../types/recipe/CraftingDataReaderTest.java | 81 +- - 76 files changed, 10268 insertions(+), 161 deletions(-) + .../api/model/NativeFurnaceOptions.java | 44 + + .../container/CookingRecipeAvailability.java | 30 + + .../model/container/CookingRecipeCatalog.java | 81 + + .../model/container/CookingRecipeInputs.java | 53 + + .../model/container/CookingRecipeSources.java | 80 + + .../model/container/CraftingOutputPlan.java | 109 ++ + .../model/container/CraftingPlacement.java | 229 +++ + .../container/FurnaceRecipePlacement.java | 219 +++ + .../NativeCookingRecipeSelection.java | 206 +++ + .../model/container/NativeRecipeCounter.java | 163 ++ + .../container/NativeRecipeDescriptor.java | 166 ++ + .../container/RecipeIngredientMatcher.java | 38 + + .../block/CraftingTableContainer.java | 100 +- + .../container/block/FurnaceContainer.java | 209 +++ + .../model/container/player/HudContainer.java | 3 + + .../container/player/InventoryContainer.java | 190 ++- + .../viabedrock/api/util/PacketFactory.java | 2 + + .../viabedrock/protocol/BedrockProtocol.java | 2 + + .../protocol/ServerboundBedrockPackets.java | 1 + + .../protocol/model/BuiltinItemAliases.java | 126 ++ + .../protocol/model/FurnaceOptions.java | 17 + + .../protocol/model/InventoryOptions.java | 10 + + .../protocol/model/ItemRegistry.java | 15 + + .../protocol/model/NativeItemName.java | 78 + + .../protocol/model/recipe/ItemDescriptor.java | 13 +- + .../protocol/model/recipe/MolangTagQuery.java | 64 + + .../protocol/model/recipe/MultiRecipe.java | 46 +- + .../model/recipe/RecipeOutputKey.java | 9 + + .../protocol/model/recipe/ShapedRecipe.java | 65 +- + .../model/recipe/ShapelessRecipe.java | 6 +- + .../protocol/model/recipe/SmithingRecipe.java | 4 +- + .../protocol/packet/InventoryPackets.java | 146 +- + .../protocol/packet/JoinPackets.java | 1 + + .../protocol/packet/MultiStatePackets.java | 55 + + .../protocol/rewriter/ItemRewriter.java | 118 +- + .../protocol/storage/CraftingDataStorage.java | 3 +- + .../protocol/storage/CraftingDataTracker.java | 626 ++++++- + .../storage/InventoryOptionsStorage.java | 187 +++ + .../storage/InventoryRequestTracker.java | 7 + + .../protocol/storage/InventoryTracker.java | 47 + + .../storage/NativeCookingUiState.java | 502 ++++++ + .../protocol/types/BedrockTypes.java | 2 + + .../types/item/BedrockRecipeResultType.java | 23 + + .../types/model/FurnaceOptionsType.java | 34 + + .../types/model/InventoryOptionsType.java | 35 + + .../types/recipe/CraftingDataReader.java | 66 +- + .../viabedrock/data/bedrock/item_aliases.json | 1470 +++++++++++++++++ + .../api/model/NativeFurnaceOptionsTest.java | 57 + + .../CookingRecipeAvailabilityTest.java | 94 ++ + .../container/CookingRecipeInputsTest.java | 180 ++ + .../container/CookingRecipeSourcesTest.java | 146 ++ + .../container/CraftingOutputPlanTest.java | 84 + + .../container/CraftingPlacementTest.java | 161 ++ + .../container/FurnaceRecipePlacementTest.java | 418 +++++ + .../NativeRecipeCounterFixtureTest.java | 132 ++ + .../container/NativeRecipeCounterTest.java | 66 + + .../container/NativeRecipeDescriptorTest.java | 305 ++++ + .../player/InventoryContainerTest.java | 131 ++ + .../model/ItemRegistryAliasesTest.java | 36 + + .../protocol/model/NativeItemNameTest.java | 61 + + .../model/recipe/MolangTagQueryTest.java | 53 + + .../model/recipe/RecipeOwnershipTest.java | 52 + + .../packet/FurnaceOptionsPacketTest.java | 162 ++ + .../packet/InventoryOptionsPacketTest.java | 152 ++ + .../NativeFurnaceOptionsPacketTest.java | 214 +++ + .../ResolvedRecipeIngredientTest.java | 282 ++++ + .../storage/CookingRecipeBookTest.java | 154 ++ + .../protocol/storage/CraftingCursorTest.java | 158 ++ + .../storage/CraftingDataTrackerTest.java | 409 +++++ + .../storage/CraftingRecipeSourcesTest.java | 128 ++ + .../storage/FurnaceRecipeRequestTest.java | 797 +++++++++ + .../storage/NativeCookingUiStateTest.java | 619 +++++++ + .../storage/NativeRecipeOutputUnlockTest.java | 166 ++ + ...NativeRecipeRegistrationReferenceTest.java | 104 ++ + .../item/BedrockRecipeResultTypeTest.java | 106 ++ + .../types/model/FurnaceOptionsTypeTest.java | 75 + + .../types/model/InventoryOptionsTypeTest.java | 46 + + .../types/recipe/CraftingDataReaderTest.java | 81 +- + 78 files changed, 11168 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -610,6 +638,7 @@ registry construction on Java and installed availability remain unfinished. create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java + create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/NativeItemName.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQuery.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOutputKey.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryOptionsStorage.java @@ -630,6 +659,7 @@ registry construction on Java and installed availability remain unfinished. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemRegistryAliasesTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/NativeItemNameTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/recipe/RecipeOwnershipTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/FurnaceOptionsPacketTest.java @@ -2866,10 +2896,10 @@ index 3effc3865..44f257b9e 100644 CLIENT_CAMERA_AIM_ASSIST(MinecraftPacketIds.ClientCameraAimAssist.getValue()), diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java new file mode 100644 -index 000000000..88cd02e44 +index 000000000..0a358e6ec --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java -@@ -0,0 +1,62 @@ +@@ -0,0 +1,126 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2885,6 +2915,7 @@ index 000000000..88cd02e44 +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; ++import java.util.Locale; +import java.util.Map; + +/** Constructor-owned ItemRegistry names, separate from block registry aliases and restriction groups. */ @@ -2903,6 +2934,20 @@ index 000000000..88cd02e44 + return TABLES.members.getOrDefault(redirect(name), List.of()); + } + ++ static NativeItemName rewrite(final NativeItemName query) { ++ String redirected = TABLES.redirects.get(query.identifier()); ++ final boolean metadataRedirect = redirected == null; ++ if (metadataRedirect) { ++ redirected = TABLES.redirects.get(query.identifier() + ':' + (query.auxiliary() == Short.MAX_VALUE ? 0 : query.auxiliary())); ++ } ++ final NativeItemName target = redirected != null ? NativeItemName.parse(redirected, query.auxiliary()) : query; ++ final Callback callback = TABLES.callbacks.get(target.identifier()); ++ if (callback != null) { ++ return new NativeItemName(callback.target(target.auxiliary()), 0); ++ } ++ return redirected != null && metadataRedirect ? new NativeItemName(target.identifier(), 0) : target; ++ } ++ + private static String redirect(final String name) { + // These getters apply one exact redirect, without legacy-name parsing or qualification. + return TABLES.redirects.getOrDefault(name, name); @@ -2915,7 +2960,7 @@ index 000000000..88cd02e44 + throw new IllegalStateException("Missing native item alias definitions"); + } + final var data = JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8)).getAsJsonObject(); -+ if (data.get("format").getAsInt() != 1 || data.get("protocol").getAsInt() != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ if (data.get("format").getAsInt() != 2 || data.get("protocol").getAsInt() != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { + throw new IllegalStateException("Item alias definitions do not match the target protocol"); + } + final Map redirects = new HashMap<>(); @@ -2923,13 +2968,62 @@ index 000000000..88cd02e44 + final Map> members = new HashMap<>(); + data.getAsJsonObject("complexAliases").entrySet().forEach(entry -> members.put(entry.getKey(), entry.getValue().getAsJsonArray().asList() + .stream().map(value -> value.getAsString()).toList())); -+ return new Tables(Map.copyOf(redirects), Map.copyOf(members)); ++ final Map callbacks = new HashMap<>(); ++ data.getAsJsonObject("callbacks").entrySet().forEach(entry -> { ++ final var value = entry.getValue().getAsJsonObject(); ++ final Kind kind = Kind.valueOf(value.get("kind").getAsString().toUpperCase(Locale.ROOT)); ++ final List values = kind == Kind.MEMBERS ? members.get(entry.getKey()) ++ : value.getAsJsonArray("values").asList().stream().map(target -> target.getAsString()).toList(); ++ if (values == null || kind == Kind.MEMBERS && values.isEmpty()) { ++ throw new IllegalStateException("Missing native alias callback values"); ++ } ++ final String fallback = kind == Kind.MEMBERS ? values.getFirst() : value.get("fallback").getAsString(); ++ final int guardLimit = kind == Kind.GUARDED_WORD || kind == Kind.SIGNED_REMAINDER ? value.get("guardLimit").getAsInt() : 0; ++ if (guardLimit < 0 || guardLimit > 255) { ++ throw new IllegalStateException("Native alias callback guard exceeds its byte field"); ++ } ++ callbacks.put(entry.getKey(), new Callback(kind, values, fallback, guardLimit)); ++ }); ++ if (!callbacks.keySet().equals(members.keySet()) || redirects.values().stream().anyMatch(String::isEmpty)) { ++ throw new IllegalStateException("Incomplete native item alias callbacks"); ++ } ++ return new Tables(Map.copyOf(redirects), Map.copyOf(members), Map.copyOf(callbacks)); + } catch (final IOException error) { + throw new IllegalStateException("Failed to load native item alias definitions", error); + } + } + -+ private record Tables(Map redirects, Map> members) { } ++ private record Tables(Map redirects, Map> members, Map callbacks) { } ++ ++ private enum Kind { ++ MEMBERS, ++ WORD, ++ GUARDED_WORD, ++ SIGNED_REMAINDER ++ } ++ ++ private record Callback(Kind kind, List values, String fallback, int guardLimit) { ++ private String target(final int auxiliary) { ++ final short narrowed = (short) auxiliary; ++ final int lowByte = narrowed & 0xFF; ++ final int index = switch (this.kind) { ++ case MEMBERS, WORD -> narrowed; ++ case GUARDED_WORD -> { ++ if ((narrowed & 0xFFFF) >= 256 && lowByte < this.guardLimit) { ++ throw new IllegalArgumentException("Metadata indexes outside native alias storage"); ++ } ++ yield narrowed; ++ } ++ case SIGNED_REMAINDER -> { ++ if (narrowed < 0 && lowByte < this.guardLimit && narrowed % 4 < 0) { ++ throw new IllegalArgumentException("Metadata indexes outside native alias storage"); ++ } ++ yield lowByte; ++ } ++ }; ++ return index >= 0 && index < this.values.size() ? this.values.get(index) : this.fallback; ++ } ++ } + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java b/src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java @@ -2972,10 +3066,10 @@ index 000000000..a3d828beb +public record InventoryOptions(int leftTab, int rightTab, boolean filtering, int inventoryLayout, int craftingLayout) { +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -index d8bf335cc..664711ae0 100644 +index d8bf335cc..906695c99 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java -@@ -135,6 +135,16 @@ public final class ItemRegistry { +@@ -135,6 +135,21 @@ public final class ItemRegistry { return Map.copyOf(result); } @@ -2988,10 +3082,99 @@ index d8bf335cc..664711ae0 100644 + public List complexAliasMembers(final String name) { + return BuiltinItemAliases.members(name); + } ++ ++ /** Native alias fallback for a parsed name. The caller must select component definitions first. */ ++ public NativeItemName rewriteLegacyName(final NativeItemName query) { ++ return BuiltinItemAliases.rewrite(query); ++ } + /** Named descriptors narrow to signed-short metadata before resolving concrete identity. */ public ItemRecipeKey namedRecipeKey(final String identifier, final int metadata) { final ItemEntry entry = this.definition(identifier); +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/NativeItemName.java b/src/main/java/net/raphimc/viabedrock/protocol/model/NativeItemName.java +new file mode 100644 +index 000000000..5ceed2311 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/NativeItemName.java +@@ -0,0 +1,78 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model; ++ ++import java.util.Objects; ++ ++/** A name and auxiliary value before native definition lookup and Item construction. */ ++public record NativeItemName(String identifier, int auxiliary) { ++ ++ public NativeItemName { ++ Objects.requireNonNull(identifier); ++ } ++ ++ /** Parse the native legacy spelling. An absent name does not construct a lookup query. */ ++ public static NativeItemName parse(final String input, final int auxiliary) { ++ if (input.isEmpty()) { ++ return null; ++ } ++ final String[] parts = input.split(":", -1); ++ final int count = parts.length - (parts[parts.length - 1].isEmpty() ? 1 : 0); ++ final boolean numeric = count >= 2 && numericSuffix(input.substring(input.indexOf(':') + 1)); ++ final String namespace = count < 2 || numeric ? "minecraft" : lowercase(parts[0]); ++ String name = lowercase(parts[count < 2 || numeric ? 0 : 1]); ++ final Integer parsed = numeric ? metadata(parts[1]) : count >= 3 ? metadata(parts[2]) : null; ++ if (name.startsWith("tile.")) { ++ name = name.substring(5); ++ } ++ return new NativeItemName(namespace + ':' + name, parsed != null ? parsed : auxiliary); ++ } ++ ++ private static boolean numericSuffix(final String value) { ++ // The native digit predicate reads a C string from the second field's start. ++ for (int index = 0; index < value.length() && value.charAt(index) != 0; index++) { ++ final char digit = value.charAt(index); ++ if (digit < '0' || digit > '9') { ++ return false; ++ } ++ } ++ return true; ++ } ++ ++ private static Integer metadata(final String value) { ++ int index = 0; ++ while (index < value.length() && whitespace(value.charAt(index))) { ++ index++; ++ } ++ final boolean negative = index < value.length() && value.charAt(index) == '-'; ++ if (index < value.length() && (value.charAt(index) == '+' || negative)) { ++ index++; ++ } ++ final int start = index; ++ int result = 0; ++ while (index < value.length() && value.charAt(index) >= '0' && value.charAt(index) <= '9') { ++ result = result * 10 + value.charAt(index++) - '0'; ++ if (result > 255) { ++ return null; ++ } ++ } ++ return index == start || negative && result != 0 ? null : result; ++ } ++ ++ private static boolean whitespace(final char value) { ++ return value == ' ' || value >= '\t' && value <= '\r'; ++ } ++ ++ private static String lowercase(final String value) { ++ final StringBuilder result = new StringBuilder(value.length()); ++ for (int index = 0; index < value.length(); index++) { ++ final char character = value.charAt(index); ++ result.append(character >= 'A' && character <= 'Z' ? (char) (character + ('a' - 'A')) : character); ++ } ++ return result.toString(); ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ItemDescriptor.java b/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ItemDescriptor.java index d62d7b11f..69e695e4e 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/recipe/ItemDescriptor.java @@ -5692,11 +5875,703 @@ index 535f52d1c..87212a2d5 100644 private CraftingDataReader() { } diff --git a/src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json b/src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json new file mode 100644 -index 000000000..cf23fac36 +index 000000000..c71740565 --- /dev/null +++ b/src/main/resources/assets/viabedrock/data/bedrock/item_aliases.json -@@ -0,0 +1,778 @@ +@@ -0,0 +1,1470 @@ +{ ++ "callbacks": { ++ "minecraft:anvil": { ++ "fallback": "minecraft:anvil", ++ "guardLimit": 16, ++ "kind": "guarded_word", ++ "values": [ ++ "minecraft:anvil", ++ "minecraft:anvil", ++ "minecraft:anvil", ++ "minecraft:anvil", ++ "minecraft:chipped_anvil", ++ "minecraft:chipped_anvil", ++ "minecraft:chipped_anvil", ++ "minecraft:chipped_anvil", ++ "minecraft:damaged_anvil", ++ "minecraft:damaged_anvil", ++ "minecraft:damaged_anvil", ++ "minecraft:damaged_anvil", ++ "minecraft:deprecated_anvil", ++ "minecraft:deprecated_anvil", ++ "minecraft:deprecated_anvil", ++ "minecraft:deprecated_anvil" ++ ] ++ }, ++ "minecraft:banner_pattern": { ++ "fallback": "minecraft:creeper_banner_pattern", ++ "kind": "word", ++ "values": [ ++ "minecraft:creeper_banner_pattern", ++ "minecraft:skull_banner_pattern", ++ "minecraft:flower_banner_pattern", ++ "minecraft:mojang_banner_pattern", ++ "minecraft:field_masoned_banner_pattern", ++ "minecraft:bordure_indented_banner_pattern", ++ "minecraft:piglin_banner_pattern", ++ "minecraft:globe_banner_pattern" ++ ] ++ }, ++ "minecraft:boat": { ++ "fallback": "minecraft:oak_boat", ++ "kind": "word", ++ "values": [ ++ "minecraft:oak_boat", ++ "minecraft:spruce_boat", ++ "minecraft:birch_boat", ++ "minecraft:jungle_boat", ++ "minecraft:acacia_boat", ++ "minecraft:dark_oak_boat", ++ "minecraft:mangrove_boat", ++ "minecraft:bamboo_raft", ++ "minecraft:cherry_boat", ++ "minecraft:pale_oak_boat" ++ ] ++ }, ++ "minecraft:bucket": { ++ "fallback": "minecraft:bucket", ++ "kind": "word", ++ "values": [ ++ "minecraft:bucket", ++ "minecraft:milk_bucket", ++ "minecraft:cod_bucket", ++ "minecraft:salmon_bucket", ++ "minecraft:tropical_fish_bucket", ++ "minecraft:pufferfish_bucket", ++ "minecraft:bucket", ++ "minecraft:bucket", ++ "minecraft:water_bucket", ++ "minecraft:bucket", ++ "minecraft:lava_bucket", ++ "minecraft:powder_snow_bucket", ++ "minecraft:axolotl_bucket", ++ "minecraft:tadpole_bucket", ++ "minecraft:sulfur_cube_bucket" ++ ] ++ }, ++ "minecraft:carpet": { ++ "kind": "members" ++ }, ++ "minecraft:chemistry_table": { ++ "fallback": "minecraft:compound_creator", ++ "guardLimit": 16, ++ "kind": "guarded_word", ++ "values": [ ++ "minecraft:compound_creator", ++ "minecraft:compound_creator", ++ "minecraft:compound_creator", ++ "minecraft:compound_creator", ++ "minecraft:material_reducer", ++ "minecraft:material_reducer", ++ "minecraft:material_reducer", ++ "minecraft:material_reducer", ++ "minecraft:element_constructor", ++ "minecraft:element_constructor", ++ "minecraft:element_constructor", ++ "minecraft:element_constructor", ++ "minecraft:lab_table", ++ "minecraft:lab_table", ++ "minecraft:lab_table", ++ "minecraft:lab_table" ++ ] ++ }, ++ "minecraft:chest_boat": { ++ "fallback": "minecraft:oak_chest_boat", ++ "kind": "word", ++ "values": [ ++ "minecraft:oak_chest_boat", ++ "minecraft:spruce_chest_boat", ++ "minecraft:birch_chest_boat", ++ "minecraft:jungle_chest_boat", ++ "minecraft:acacia_chest_boat", ++ "minecraft:dark_oak_chest_boat", ++ "minecraft:mangrove_chest_boat", ++ "minecraft:bamboo_chest_raft", ++ "minecraft:cherry_chest_boat", ++ "minecraft:pale_oak_chest_boat", ++ "minecraft:poplar_chest_boat" ++ ] ++ }, ++ "minecraft:coal": { ++ "fallback": "minecraft:coal", ++ "kind": "word", ++ "values": [ ++ "minecraft:coal", ++ "minecraft:charcoal" ++ ] ++ }, ++ "minecraft:cobblestone_wall": { ++ "kind": "members" ++ }, ++ "minecraft:colored_torch_bp": { ++ "fallback": "minecraft:colored_torch_blue", ++ "kind": "word", ++ "values": [ ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_blue", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple", ++ "minecraft:colored_torch_purple" ++ ] ++ }, ++ "minecraft:colored_torch_rg": { ++ "fallback": "minecraft:colored_torch_red", ++ "kind": "word", ++ "values": [ ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_red", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green", ++ "minecraft:colored_torch_green" ++ ] ++ }, ++ "minecraft:concrete": { ++ "kind": "members" ++ }, ++ "minecraft:concrete_powder": { ++ "kind": "members" ++ }, ++ "minecraft:coral": { ++ "fallback": "minecraft:tube_coral", ++ "kind": "word", ++ "values": [ ++ "minecraft:tube_coral", ++ "minecraft:brain_coral", ++ "minecraft:bubble_coral", ++ "minecraft:fire_coral", ++ "minecraft:horn_coral", ++ "minecraft:tube_coral", ++ "minecraft:tube_coral", ++ "minecraft:tube_coral", ++ "minecraft:dead_tube_coral", ++ "minecraft:dead_brain_coral", ++ "minecraft:dead_bubble_coral", ++ "minecraft:dead_fire_coral", ++ "minecraft:dead_horn_coral" ++ ] ++ }, ++ "minecraft:coral_block": { ++ "fallback": "minecraft:tube_coral_block", ++ "kind": "word", ++ "values": [ ++ "minecraft:tube_coral_block", ++ "minecraft:brain_coral_block", ++ "minecraft:bubble_coral_block", ++ "minecraft:fire_coral_block", ++ "minecraft:horn_coral_block", ++ "minecraft:tube_coral_block", ++ "minecraft:tube_coral_block", ++ "minecraft:tube_coral_block", ++ "minecraft:dead_tube_coral_block", ++ "minecraft:dead_brain_coral_block", ++ "minecraft:dead_bubble_coral_block", ++ "minecraft:dead_fire_coral_block", ++ "minecraft:dead_horn_coral_block", ++ "minecraft:dead_tube_coral_block", ++ "minecraft:dead_tube_coral_block", ++ "minecraft:dead_tube_coral_block" ++ ] ++ }, ++ "minecraft:coral_fan": { ++ "kind": "members" ++ }, ++ "minecraft:coral_fan_dead": { ++ "kind": "members" ++ }, ++ "minecraft:dirt": { ++ "kind": "members" ++ }, ++ "minecraft:double_plant": { ++ "kind": "members" ++ }, ++ "minecraft:double_stone_block_slab": { ++ "kind": "members" ++ }, ++ "minecraft:double_stone_block_slab2": { ++ "kind": "members" ++ }, ++ "minecraft:double_stone_block_slab3": { ++ "kind": "members" ++ }, ++ "minecraft:double_stone_block_slab4": { ++ "kind": "members" ++ }, ++ "minecraft:dye": { ++ "fallback": "minecraft:ink_sac", ++ "kind": "word", ++ "values": [ ++ "minecraft:ink_sac", ++ "minecraft:red_dye", ++ "minecraft:green_dye", ++ "minecraft:cocoa_beans", ++ "minecraft:lapis_lazuli", ++ "minecraft:purple_dye", ++ "minecraft:cyan_dye", ++ "minecraft:light_gray_dye", ++ "minecraft:gray_dye", ++ "minecraft:pink_dye", ++ "minecraft:lime_dye", ++ "minecraft:yellow_dye", ++ "minecraft:light_blue_dye", ++ "minecraft:magenta_dye", ++ "minecraft:orange_dye", ++ "minecraft:bone_meal", ++ "minecraft:black_dye", ++ "minecraft:brown_dye", ++ "minecraft:blue_dye", ++ "minecraft:white_dye" ++ ] ++ }, ++ "minecraft:fence": { ++ "kind": "members" ++ }, ++ "minecraft:hard_stained_glass": { ++ "kind": "members" ++ }, ++ "minecraft:hard_stained_glass_pane": { ++ "kind": "members" ++ }, ++ "minecraft:leaves": { ++ "fallback": "minecraft:oak_leaves", ++ "kind": "word", ++ "values": [ ++ "minecraft:oak_leaves", ++ "minecraft:spruce_leaves", ++ "minecraft:birch_leaves", ++ "minecraft:jungle_leaves", ++ "minecraft:oak_leaves", ++ "minecraft:spruce_leaves", ++ "minecraft:birch_leaves", ++ "minecraft:jungle_leaves", ++ "minecraft:oak_leaves", ++ "minecraft:spruce_leaves", ++ "minecraft:birch_leaves", ++ "minecraft:jungle_leaves", ++ "minecraft:oak_leaves", ++ "minecraft:spruce_leaves", ++ "minecraft:birch_leaves", ++ "minecraft:jungle_leaves" ++ ] ++ }, ++ "minecraft:leaves2": { ++ "fallback": "minecraft:acacia_leaves", ++ "kind": "word", ++ "values": [ ++ "minecraft:acacia_leaves", ++ "minecraft:dark_oak_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:dark_oak_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:dark_oak_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:acacia_leaves", ++ "minecraft:dark_oak_leaves" ++ ] ++ }, ++ "minecraft:light_block": { ++ "kind": "members" ++ }, ++ "minecraft:log": { ++ "fallback": "minecraft:oak_log", ++ "kind": "word", ++ "values": [ ++ "minecraft:oak_log", ++ "minecraft:spruce_log", ++ "minecraft:birch_log", ++ "minecraft:jungle_log", ++ "minecraft:oak_log", ++ "minecraft:spruce_log", ++ "minecraft:birch_log", ++ "minecraft:jungle_log", ++ "minecraft:oak_log", ++ "minecraft:spruce_log", ++ "minecraft:birch_log", ++ "minecraft:jungle_log" ++ ] ++ }, ++ "minecraft:log2": { ++ "fallback": "minecraft:acacia_log", ++ "kind": "word", ++ "values": [ ++ "minecraft:acacia_log", ++ "minecraft:dark_oak_log", ++ "minecraft:acacia_log", ++ "minecraft:acacia_log", ++ "minecraft:acacia_log", ++ "minecraft:dark_oak_log", ++ "minecraft:acacia_log", ++ "minecraft:acacia_log", ++ "minecraft:acacia_log", ++ "minecraft:dark_oak_log", ++ "minecraft:acacia_log", ++ "minecraft:acacia_log", ++ "minecraft:acacia_log", ++ "minecraft:dark_oak_log" ++ ] ++ }, ++ "minecraft:monster_egg": { ++ "kind": "members" ++ }, ++ "minecraft:planks": { ++ "kind": "members" ++ }, ++ "minecraft:prismarine": { ++ "kind": "members" ++ }, ++ "minecraft:purpur_block": { ++ "fallback": "minecraft:purpur_block", ++ "guardLimit": 16, ++ "kind": "signed_remainder", ++ "values": [ ++ "minecraft:purpur_block", ++ "minecraft:deprecated_purpur_block_1", ++ "minecraft:purpur_pillar", ++ "minecraft:deprecated_purpur_block_2", ++ "minecraft:purpur_block", ++ "minecraft:deprecated_purpur_block_1", ++ "minecraft:purpur_pillar", ++ "minecraft:deprecated_purpur_block_2", ++ "minecraft:purpur_block", ++ "minecraft:deprecated_purpur_block_1", ++ "minecraft:purpur_pillar", ++ "minecraft:deprecated_purpur_block_2", ++ "minecraft:purpur_block", ++ "minecraft:deprecated_purpur_block_1", ++ "minecraft:purpur_pillar", ++ "minecraft:deprecated_purpur_block_2" ++ ] ++ }, ++ "minecraft:quartz_block": { ++ "fallback": "minecraft:quartz_block", ++ "guardLimit": 16, ++ "kind": "signed_remainder", ++ "values": [ ++ "minecraft:quartz_block", ++ "minecraft:chiseled_quartz_block", ++ "minecraft:quartz_pillar", ++ "minecraft:smooth_quartz", ++ "minecraft:quartz_block", ++ "minecraft:chiseled_quartz_block", ++ "minecraft:quartz_pillar", ++ "minecraft:smooth_quartz", ++ "minecraft:quartz_block", ++ "minecraft:chiseled_quartz_block", ++ "minecraft:quartz_pillar", ++ "minecraft:smooth_quartz", ++ "minecraft:quartz_block", ++ "minecraft:chiseled_quartz_block", ++ "minecraft:quartz_pillar", ++ "minecraft:smooth_quartz" ++ ] ++ }, ++ "minecraft:red_flower": { ++ "kind": "members" ++ }, ++ "minecraft:red_sandstone": { ++ "kind": "members" ++ }, ++ "minecraft:sand": { ++ "kind": "members" ++ }, ++ "minecraft:sandstone": { ++ "kind": "members" ++ }, ++ "minecraft:sapling": { ++ "fallback": "minecraft:oak_sapling", ++ "kind": "word", ++ "values": [ ++ "minecraft:oak_sapling", ++ "minecraft:spruce_sapling", ++ "minecraft:birch_sapling", ++ "minecraft:jungle_sapling", ++ "minecraft:acacia_sapling", ++ "minecraft:dark_oak_sapling", ++ "minecraft:oak_sapling", ++ "minecraft:oak_sapling", ++ "minecraft:oak_sapling", ++ "minecraft:spruce_sapling", ++ "minecraft:birch_sapling", ++ "minecraft:jungle_sapling", ++ "minecraft:acacia_sapling", ++ "minecraft:dark_oak_sapling" ++ ] ++ }, ++ "minecraft:shulker_box": { ++ "kind": "members" ++ }, ++ "minecraft:skull": { ++ "kind": "members" ++ }, ++ "minecraft:spawn_egg": { ++ "fallback": "", ++ "kind": "word", ++ "values": [ ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "minecraft:chicken_spawn_egg", ++ "minecraft:cow_spawn_egg", ++ "minecraft:pig_spawn_egg", ++ "minecraft:sheep_spawn_egg", ++ "minecraft:wolf_spawn_egg", ++ "minecraft:villager_spawn_egg", ++ "minecraft:mooshroom_spawn_egg", ++ "minecraft:squid_spawn_egg", ++ "minecraft:rabbit_spawn_egg", ++ "minecraft:bat_spawn_egg", ++ "minecraft:iron_golem_spawn_egg", ++ "minecraft:snow_golem_spawn_egg", ++ "minecraft:ocelot_spawn_egg", ++ "minecraft:horse_spawn_egg", ++ "minecraft:donkey_spawn_egg", ++ "minecraft:mule_spawn_egg", ++ "minecraft:skeleton_horse_spawn_egg", ++ "minecraft:zombie_horse_spawn_egg", ++ "minecraft:polar_bear_spawn_egg", ++ "minecraft:llama_spawn_egg", ++ "minecraft:parrot_spawn_egg", ++ "minecraft:dolphin_spawn_egg", ++ "minecraft:zombie_spawn_egg", ++ "minecraft:creeper_spawn_egg", ++ "minecraft:skeleton_spawn_egg", ++ "minecraft:spider_spawn_egg", ++ "minecraft:zombie_pigman_spawn_egg", ++ "minecraft:slime_spawn_egg", ++ "minecraft:enderman_spawn_egg", ++ "minecraft:silverfish_spawn_egg", ++ "minecraft:cave_spider_spawn_egg", ++ "minecraft:ghast_spawn_egg", ++ "minecraft:magma_cube_spawn_egg", ++ "minecraft:blaze_spawn_egg", ++ "minecraft:zombie_villager_spawn_egg", ++ "minecraft:witch_spawn_egg", ++ "minecraft:stray_spawn_egg", ++ "minecraft:husk_spawn_egg", ++ "minecraft:wither_skeleton_spawn_egg", ++ "minecraft:guardian_spawn_egg", ++ "minecraft:elder_guardian_spawn_egg", ++ "minecraft:npc_spawn_egg", ++ "minecraft:wither_spawn_egg", ++ "minecraft:ender_dragon_spawn_egg", ++ "minecraft:shulker_spawn_egg", ++ "minecraft:endermite_spawn_egg", ++ "minecraft:agent_spawn_egg", ++ "minecraft:vindicator_spawn_egg", ++ "minecraft:phantom_spawn_egg", ++ "minecraft:ravager_spawn_egg", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "minecraft:turtle_spawn_egg", ++ "minecraft:cat_spawn_egg", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "", ++ "minecraft:evoker_spawn_egg", ++ "minecraft:vex_spawn_egg", ++ "", ++ "", ++ "minecraft:pufferfish_spawn_egg", ++ "minecraft:salmon_spawn_egg", ++ "minecraft:drowned_spawn_egg", ++ "minecraft:tropical_fish_spawn_egg", ++ "minecraft:cod_spawn_egg", ++ "minecraft:panda_spawn_egg", ++ "minecraft:pillager_spawn_egg", ++ "minecraft:villager_spawn_egg", ++ "minecraft:zombie_villager_spawn_egg", ++ "", ++ "minecraft:wandering_trader_spawn_egg", ++ "", ++ "", ++ "minecraft:fox_spawn_egg", ++ "minecraft:bee_spawn_egg", ++ "minecraft:piglin_spawn_egg", ++ "minecraft:hoglin_spawn_egg", ++ "minecraft:strider_spawn_egg", ++ "minecraft:zoglin_spawn_egg", ++ "minecraft:piglin_brute_spawn_egg", ++ "minecraft:goat_spawn_egg", ++ "minecraft:glow_squid_spawn_egg", ++ "minecraft:axolotl_bucket", ++ "minecraft:warden_spawn_egg", ++ "minecraft:frog_spawn_egg", ++ "minecraft:tadpole_spawn_egg", ++ "minecraft:allay_spawn_egg", ++ "", ++ "", ++ "", ++ "minecraft:camel_spawn_egg", ++ "minecraft:sniffer_spawn_egg", ++ "minecraft:breeze_spawn_egg", ++ "", ++ "minecraft:armadillo_spawn_egg", ++ "", ++ "minecraft:bogged_spawn_egg", ++ "", ++ "minecraft:creaking_spawn_egg", ++ "minecraft:happy_ghast_spawn_egg", ++ "minecraft:copper_golem_spawn_egg", ++ "minecraft:nautilus_spawn_egg", ++ "minecraft:zombie_nautilus_spawn_egg", ++ "minecraft:parched_spawn_egg", ++ "minecraft:camel_husk_spawn_egg", ++ "minecraft:sulfur_cube_spawn_egg", ++ "", ++ "", ++ "", ++ "minecraft:trader_llama_spawn_egg" ++ ] ++ }, ++ "minecraft:sponge": { ++ "kind": "members" ++ }, ++ "minecraft:stained_glass": { ++ "kind": "members" ++ }, ++ "minecraft:stained_glass_pane": { ++ "kind": "members" ++ }, ++ "minecraft:stained_hardened_clay": { ++ "kind": "members" ++ }, ++ "minecraft:stone": { ++ "kind": "members" ++ }, ++ "minecraft:stone_block_slab": { ++ "kind": "members" ++ }, ++ "minecraft:stone_block_slab2": { ++ "kind": "members" ++ }, ++ "minecraft:stone_block_slab3": { ++ "kind": "members" ++ }, ++ "minecraft:stone_block_slab4": { ++ "kind": "members" ++ }, ++ "minecraft:stonebrick": { ++ "kind": "members" ++ }, ++ "minecraft:tallgrass": { ++ "kind": "members" ++ }, ++ "minecraft:tnt": { ++ "fallback": "minecraft:tnt", ++ "guardLimit": 4, ++ "kind": "guarded_word", ++ "values": [ ++ "minecraft:tnt", ++ "minecraft:tnt", ++ "minecraft:underwater_tnt", ++ "minecraft:underwater_tnt" ++ ] ++ }, ++ "minecraft:wood": { ++ "fallback": "minecraft:oak_wood", ++ "kind": "word", ++ "values": [ ++ "minecraft:oak_wood", ++ "minecraft:spruce_wood", ++ "minecraft:birch_wood", ++ "minecraft:jungle_wood", ++ "minecraft:acacia_wood", ++ "minecraft:dark_oak_wood", ++ "minecraft:oak_wood", ++ "minecraft:oak_wood", ++ "minecraft:stripped_oak_wood", ++ "minecraft:stripped_spruce_wood", ++ "minecraft:stripped_birch_wood", ++ "minecraft:stripped_jungle_wood", ++ "minecraft:stripped_acacia_wood", ++ "minecraft:stripped_dark_oak_wood", ++ "minecraft:stripped_oak_wood", ++ "minecraft:stripped_oak_wood" ++ ] ++ }, ++ "minecraft:wooden_slab": { ++ "kind": "members" ++ }, ++ "minecraft:wool": { ++ "kind": "members" ++ } ++ }, + "complexAliases": { + "minecraft:anvil": [ + "minecraft:anvil", @@ -6380,7 +7255,7 @@ index 000000000..cf23fac36 + "minecraft:black_wool" + ] + }, -+ "format": 1, ++ "format": 2, + "protocol": 2193, + "redirects": { + "minecraft:appleenchanted": "enchanted_golden_apple", @@ -8356,6 +9231,73 @@ index 000000000..a2cef8438 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/NativeItemNameTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/NativeItemNameTest.java +new file mode 100644 +index 000000000..44536259c +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/model/NativeItemNameTest.java +@@ -0,0 +1,61 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model; ++ ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import org.junit.jupiter.api.Test; ++ ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.zip.GZIPInputStream; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertThrows; ++import static org.junit.jupiter.api.Assumptions.assumeTrue; ++ ++final class NativeItemNameTest { ++ ++ @Test ++ void matchesOriginalNameParser() throws Exception { ++ for (final var value : reference().getAsJsonArray("parserRows")) { ++ final var row = value.getAsJsonObject(); ++ final NativeItemName actual = NativeItemName.parse(row.get("inputName").getAsString(), row.get("inputAux").getAsInt()); ++ final NativeItemName expected = row.get("success").getAsBoolean() ++ ? new NativeItemName(row.get("namespace").getAsString() + ':' + row.get("name").getAsString(), row.get("aux").getAsInt()) : null; ++ assertEquals(expected, actual, row::toString); ++ } ++ } ++ ++ @Test ++ void matchesOriginalRegistryNameRewrites() throws Exception { ++ final var registry = new ItemRegistry(new ItemEntry[0]); ++ final var reference = reference(); ++ for (final var value : reference.getAsJsonArray("rewriteRows")) { ++ final var row = value.getAsJsonObject(); ++ assertEquals(new NativeItemName(row.get("name").getAsString(), row.get("aux").getAsInt()), ++ registry.rewriteLegacyName(new NativeItemName(row.get("inputName").getAsString(), row.get("inputAux").getAsInt())), row::toString); ++ } ++ // These metadata combinations index outside the original callback's vectors. ++ // They require caller admission rather than a fabricated native lookup result. ++ for (final var value : reference.getAsJsonArray("unsafeCallbackInputs")) { ++ final var row = value.getAsJsonObject(); ++ assertThrows(IllegalArgumentException.class, () -> registry.rewriteLegacyName( ++ new NativeItemName(row.get("name").getAsString(), row.get("aux").getAsInt())), row::toString); ++ } ++ } ++ ++ private static JsonObject reference() throws Exception { ++ final String path = System.getenv("STACKANVIL_NATIVE_ITEM_NAME_RESOLUTION"); ++ assumeTrue(path != null && !path.isBlank(), "Native ItemRegistry name resolution corpus is optional"); ++ try (var reader = new InputStreamReader(new GZIPInputStream(Files.newInputStream(Path.of(path))), StandardCharsets.UTF_8)) { ++ return JsonParser.parseReader(reader).getAsJsonObject().getAsJsonObject("nativeNameResolution"); ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/recipe/MolangTagQueryTest.java new file mode 100644 index 000000000..284e08781 diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index ed0fc6cb..6e328371 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -536,3 +536,28 @@ Three deliberate changes fail value comparisons: ignored redirects, reversed mem The retained descriptor, counter, source matcher, search, unlock and progressive-model comparisons also pass. All four builds pass with 2,860 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. All 1,495 core classes and the alias asset match both embedded copies; all 1,191 client classes match the tested add-on. + + +## Native legacy names and metadata callbacks + +Parse names with native ASCII case folding, colon/C-string suffix rules, tile-prefix removal and auxiliary acceptance. +Expose registry fallback rewriting separately from component-definition precedence and live Item ownership. +After one redirect and target parsing, execute the target's metadata callback and clear its auxiliary. +The pinned data requires all 60 callbacks: 38 member-indexed definitions and 22 explicit mappings with 439 values. +Preserve signed-short arguments, byte guards, signed remainder behavior and default targets. +Reject metadata that indexes outside native callback storage; complete caller admission still needs implementation. + +Complete original parser and registry rewriter provide 2,501 parser controls and 22,917 rewrites after native block/state and ItemRegistry initialization. +The harness supplies read-only UTF-8 caller backing, allocation and ordinary Windows CRT imports. +Native game parsing, conversion dispatch/range checks, redirects, callbacks and owned string retirement execute. +All original input, registry, TLS, static and CRT cleanup completes with no native allocations or held locks. +Forty-six out-of-storage input combinations remain outside the normal-result corpus. +See the [name-resolution reference](../../../docs/native-player-inventory.md#native-name-parsing-and-metadata-rewrites) for exact scope and limits. + +Live component/member/representative definitions, underlying definition lookup, Item construction, full registry replacement, recipe admission and installed availability remain unfinished. + +The packaged core matches all 2,501 parser controls, 22,917 rewrites and 46 explicit unsupported-index guards. +Nine deliberate parser or callback changes fail native value comparisons. +The 2,127 alias getter controls and three retained alias regressions also pass, along with the earlier descriptor, counter, search, unlock and progressive comparisons. +All four builds pass with 2,862 tests, 163 optional skips and no failures or errors. Checkstyle passes after full replay. +All 1,498 core classes and the alias asset match both embedded copies; all 1,191 client classes match the tested add-on.