From 68d4a854c328d6aa6bc7af880248c29d5ce7532a Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Fri, 16 Oct 2026 22:44:55 -0400 Subject: [PATCH] fix(heights): support Purpur 26.3 --- README.md | 12 +-- build.gradle.kts | 14 +-- .../dmg/spigotheights/PotionRecipes.java | 25 +++++- .../dmg/spigotheights/StatureBrewing.java | 6 +- .../dmg/spigotheights/NativeBrewingTest.java | 85 ++++++++++++++----- .../spigotheights/NativeCarryRoutingTest.java | 2 +- .../dmg/spigotheights/NativeRuntime.java | 21 +++-- .../dmg/spigotheights/PotionRecipesTest.java | 7 +- .../spigotheights/PurpurDistributionTest.java | 17 +++- 9 files changed, 140 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 40f697b..19e1af1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Purpur Heights -A Purpur 26.2 plugin adding craftable player-scaling potions and dispenser launch tubes for tiny players. +A Purpur 26.3 plugin adding craftable player-scaling potions and dispenser launch tubes for tiny players. Approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-heights/index.md). @@ -8,7 +8,7 @@ New releases use the `purpur-heights` repository and `purpur-heights-.j ## Requirements -- Purpur 26.2 build 2618 +- Purpur 26.3 build 2651 - Java 25 or newer ## Potions @@ -57,9 +57,9 @@ Drinking a permanent stature potion ends the temporary sequence, using the curre Heights tracks only its loaded clouds and checks them every five ticks, dispatching `AreaEffectCloudApplyEvent` and honoring cancellation and recipient filtering. This supports effectless stature clouds without adding a vanilla status effect. Online expiry is checked every second. Durable receipts survive cloud unload and are retired on observed permanent entity removal; abrupt process termination can leave harmless orphan receipts. -Heights corrects Purpur 2618's no-op conversion of custom potions without a vanilla base potion type inside `BrewEvent`. This covers existing items without changing metadata or crafting recipes. It detaches aliased results at LOWEST priority before normal result modifiers and validates/corrects at HIGHEST. Conflicting/missing results or item-construction failures cancel the brew; valid converted results and native ingredient/fuel accounting are retained. Cancellation remains authoritative. As with other Bukkit listeners, this cannot control plugins that directly mutate inventories or run conflicting handlers outside the normal event-priority contract. +Purpur 26.3's data-driven brewing engine natively converts authenticated custom potions without a vanilla base potion type. Heights supplies representative recipe choices so the server's brewing input/reagent indexes include those recipes, then validates results at the `BrewEvent` transaction boundary. The build-2618 no-op correction is not needed for normal build-2651 conversion; the defensive listener preserves cancellation and rejects conflicting, missing or identity-stripped results. Valid native results, compatible result modifiers, ingredient/fuel accounting, and dragon's-breath bottle return remain server-owned. Cancellation remains authoritative. As with other Bukkit listeners, this cannot control plugins that directly mutate inventories or run conflicting handlers outside the normal event-priority contract. -**Verification:** `nativeBrewingTest` executes real Purpur 2618 brewing cycles with native inventories, recipes, metadata and Paper event dispatch. It covers all four splash/lingering conversions, ingredient safety and cancellation, mixed/ordinary potions, and output compatibility with the stature adapters. World and entity boundaries are test doubles; these checks do not claim native projectile/cloud spawning, client appearance, or live logout/restart gameplay. Those remain supplementary live checks. +**Verification:** `nativeBrewingTest` executes real Purpur 26.3 build 2651 brewing cycles with native data-driven recipes, inventories, metadata and Paper event dispatch. It proves all four base-less potion kinds convert without the old correction, then covers splash/lingering conversion, representative recipe indexing, ingredient safety and cancellation, mixed/ordinary potions, immediate second-stage brewing, bottle return, and output compatibility with the stature adapters. World and entity boundaries are test doubles; these checks do not claim native projectile/cloud spawning, client appearance, or live logout/restart gameplay. Those remain supplementary live checks. ## Ride larger players (US-010) @@ -89,7 +89,7 @@ Eligibility is rechecked every five ticks, including temporary potion applicatio Server/plugin cancellation remains authoritative: a refused dismount leaves accurate tracking and is reported, not bypassed or called successful. A cancelled voluntary stop can be retried by either player; automatic eligibility cleanup continues checking and reports refusal once per ride rather than spamming. Lifecycle/disable refusals are logged. Native dismount events are reconciled after dispatch so a later cancellation is honored. An unrelated vehicle is never ejected during stale-pair cleanup. **Verification:** API/domain tests preserve native cleanup and reject rescue/motion-reset calls. -`NativeCarryRoutingTest` registers the production listener through Purpur 2618's actual Paper +`NativeCarryRoutingTest` registers the production listener through Purpur 26.3 build 2651's actual Paper plugin manager and dispatches both `PlayerInteractEntityEvent` and `PlayerInteractAtEntityEvent`. Same-tick duplicate rider/target interactions are coalesced, including refused mounts; a fresh interaction next tick can retry without a gameplay cooldown. Registered mount/dismount cancellation @@ -161,7 +161,7 @@ Player names must match an online player exactly (case-insensitive); partial nam The plugin JAR is written to `build/libs/`. -`check` includes `nativeBrewingTest`. It downloads the exact Purpur 26.2 build 2618 launcher (SHA-256 verified) and prepares its runtime dependencies with Paperclip's **patch-only** mode under `build/brewing-runtime/`. It does not start a server, open ports, create a world or accept the EULA. Initial/clean builds need network access; the native test JVM permits up to 1 GiB heap. No runtime implementation classes or test dependencies are packaged in the plugin. Run `./gradlew test` for the fast API/domain suite alone, or `./gradlew nativeBrewingTest` for native brewing and registered carry-event routing tests. Full verification remains `./gradlew clean check jar`. +`check` includes `nativeBrewingTest`. It downloads the exact Purpur 26.3 build 2651 launcher (SHA-256 `0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598`) and prepares its runtime dependencies with Paperclip's **patch-only** mode under `build/brewing-runtime/`. It does not start a server, open ports, create a world or accept the EULA. Initial/clean builds need network access; the native test JVM permits up to 1 GiB heap. No runtime implementation classes or test dependencies are packaged in the plugin. Run `./gradlew test` for the fast API/domain suite alone, or `./gradlew nativeBrewingTest` for native brewing and registered carry-event routing tests. Full verification remains `./gradlew clean check jar`. ## Releases diff --git a/build.gradle.kts b/build.gradle.kts index c76428f..512b043 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,8 +24,8 @@ tasks.withType().configureEach { } dependencies { - compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") - testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") + compileOnly("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable") + testImplementation("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.yaml:snakeyaml:2.4") @@ -43,19 +43,19 @@ tasks.test { // Paperclip only extracts/patches dependencies: no server, world, port or EULA acceptance. val brewingRuntime = layout.buildDirectory.dir("brewing-runtime") val downloadBrewingRuntime = tasks.register("downloadBrewingRuntime") { - val launcher = brewingRuntime.map { it.file("purpur-26.2-2618.jar") } + val launcher = brewingRuntime.map { it.file("purpur-26.3-2651.jar") } outputs.file(launcher) doLast { val file = launcher.get().asFile file.parentFile.mkdirs() - val connection = URI("https://api.purpurmc.org/v2/purpur/26.2/2618/download").toURL().openConnection() + val connection = URI("https://api.purpurmc.org/v2/purpur/26.3/2651/download").toURL().openConnection() connection.connectTimeout = 30_000 connection.readTimeout = 120_000 val bytes = connection.getInputStream().use { it.readBytes() } val digest = MessageDigest.getInstance("SHA-256").digest(bytes) .joinToString("") { "%02x".format(it) } - check(digest == "4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31") { - "Purpur 2618 checksum mismatch" + check(digest == "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598") { + "Purpur 2651 checksum mismatch" } file.writeBytes(bytes) } @@ -63,7 +63,7 @@ val downloadBrewingRuntime = tasks.register("downloadBrewingRuntime") { val prepareBrewingRuntime = tasks.register("prepareBrewingRuntime") { dependsOn(downloadBrewingRuntime) javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) } - classpath = files(brewingRuntime.map { it.file("purpur-26.2-2618.jar") }) + classpath = files(brewingRuntime.map { it.file("purpur-26.3-2651.jar") }) mainClass = "io.papermc.paperclip.Main" jvmArgs("-Dpaperclip.patchonly=true") workingDir(brewingRuntime) diff --git a/src/main/java/games/dmg/spigotheights/PotionRecipes.java b/src/main/java/games/dmg/spigotheights/PotionRecipes.java index 7b115e7..fb6a970 100644 --- a/src/main/java/games/dmg/spigotheights/PotionRecipes.java +++ b/src/main/java/games/dmg/spigotheights/PotionRecipes.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import io.papermc.paper.potion.PotionMix; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -98,12 +99,32 @@ public final class PotionRecipes { NamespacedKey mixKey = mixKey(kind, target); brewer.removePotionMix(mixKey); brewer.addPotionMix(new PotionMix(mixKey, create(kind, target), - PotionMix.createPredicateChoice(item -> item != null && item.getType() == source + predicateChoice(create(kind, source), item -> item != null && item.getType() == source && identify(item) == kind), - PotionMix.createPredicateChoice(item -> item != null && item.getType() == ingredient))); + predicateChoice(items.apply(ingredient), item -> item != null && item.getType() == ingredient))); ownedMixKeys.add(mixKey); } + private static RecipeChoice.PredicateChoice predicateChoice( + ItemStack example, Predicate predicate) { + return new RecipeChoice.PredicateChoice() { + @Override + public ItemStack getItemStack() { + return example.clone(); + } + + @Override + public RecipeChoice.PredicateChoice clone() { + return this; + } + + @Override + public boolean test(ItemStack item) { + return predicate.test(item); + } + }; + } + public void unregister() { ownedRecipeKeys.forEach(server::removeRecipe); ownedRecipeKeys.clear(); diff --git a/src/main/java/games/dmg/spigotheights/StatureBrewing.java b/src/main/java/games/dmg/spigotheights/StatureBrewing.java index 6453c91..103ee26 100644 --- a/src/main/java/games/dmg/spigotheights/StatureBrewing.java +++ b/src/main/java/games/dmg/spigotheights/StatureBrewing.java @@ -7,7 +7,7 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.inventory.BrewEvent; -/** Completes authenticated stature conversions at the native brewing transaction boundary. */ +/** Validates authenticated stature conversions at the native brewing transaction boundary. */ public final class StatureBrewing implements Listener { private final PotionRecipes potions; @@ -80,8 +80,8 @@ public final class StatureBrewing implements Listener { event.setCancelled(true); return; } - // 2618 recognizes custom recipes but mix() returns base-less potions unchanged. - // Correct only that no-op; leave inventory and ingredient accounting to the server. + // Correct only an unexpected native no-op; leave inventory and ingredient accounting + // to the server. Build 2651 normally returns the registered native result directly. event.getResults().set(slot, potions.create(kind, target)); } } diff --git a/src/nativeTest/java/games/dmg/spigotheights/NativeBrewingTest.java b/src/nativeTest/java/games/dmg/spigotheights/NativeBrewingTest.java index 7f2db92..be2a3e3 100644 --- a/src/nativeTest/java/games/dmg/spigotheights/NativeBrewingTest.java +++ b/src/nativeTest/java/games/dmg/spigotheights/NativeBrewingTest.java @@ -15,9 +15,10 @@ import java.util.UUID; import java.util.logging.Logger; import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.flag.FeatureFlags; -import net.minecraft.world.item.alchemy.PotionBrewing; -import net.minecraft.world.level.Level; +import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; import org.bukkit.Bukkit; @@ -25,10 +26,11 @@ import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Server; import org.bukkit.command.SimpleCommandMap; +import org.bukkit.craftbukkit.inventory.CraftBrewingRecipe; import org.bukkit.craftbukkit.inventory.CraftInventoryBrewer; import org.bukkit.craftbukkit.inventory.CraftItemFactory; -import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.event.HandlerList; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -55,12 +57,12 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.Arguments; -/** Real 2618 brewing ticks, items, metadata, recipes, event dispatch and ingredient consumption. */ +/** Real build-2651 brewing ticks, items, metadata, recipes, event dispatch and ingredient consumption. */ class NativeBrewingTest { private static PluginManager events; private static Plugin plugin; private PotionRecipes recipes; - private PotionBrewing engine; + private RecipeManager engine; @BeforeAll static void bootstrap() throws Exception { @@ -70,8 +72,8 @@ class NativeBrewingTest { when(server.getUnsafe()).thenReturn(CraftMagicNumbers.INSTANCE); when(server.getLogger()).thenReturn(Logger.getLogger("NativeBrewingTest")); when(server.getName()).thenReturn("NativeBrewingTest"); - when(server.getVersion()).thenReturn("26.2-2618"); - when(server.getBukkitVersion()).thenReturn("26.2-2618"); + when(server.getVersion()).thenReturn("26.3-2651"); + when(server.getBukkitVersion()).thenReturn("26.3-2651"); when(server.isPrimaryThread()).thenReturn(true); events = new PaperPluginManagerImpl(server, new SimpleCommandMap(server, new HashMap<>()), null); when(server.getPluginManager()).thenReturn(events); @@ -88,13 +90,23 @@ class NativeBrewingTest { @BeforeEach void registerActualRecipesAndListener() { HandlerList.unregisterAll(); - engine = PotionBrewing.bootstrap(FeatureFlags.VANILLA_SET); + engine = NativeRuntime.recipes(); recipes = new PotionRecipes(null, new NamespacedKey("spigotheights", "stature_potion"), ItemStack::new); PotionBrewer brewer = (PotionBrewer) Proxy.newProxyInstance(PotionBrewer.class.getClassLoader(), new Class[] {PotionBrewer.class}, (proxy, method, args) -> { switch (method.getName()) { - case "addPotionMix" -> engine.addPotionMix((io.papermc.paper.potion.PotionMix) args[0]); - case "removePotionMix" -> engine.removePotionMix((NamespacedKey) args[0]); + case "addPotionMix" -> { + var mix = (io.papermc.paper.potion.PotionMix) args[0]; + var recipe = new org.bukkit.inventory.BrewingRecipe( + mix.getKey(), mix.getResult(), mix.getInput(), mix.getIngredient()); + engine.recipes.addRecipe(CraftBrewingRecipe.fromBukkitRecipe(recipe).toMinecraftRecipe()); + engine.finalizeRecipeLoading(FeatureFlags.VANILLA_SET); + } + case "removePotionMix" -> { + engine.recipes.removeRecipe(CraftNamespacedKey.toResourceKey( + Registries.RECIPE, (NamespacedKey) args[0])); + engine.finalizeRecipeLoading(FeatureFlags.VANILLA_SET); + } default -> throw new UnsupportedOperationException(method.getName()); } return null; @@ -103,6 +115,20 @@ class NativeBrewingTest { events.registerEvents(new StatureBrewing(recipes), plugin); } + @ParameterizedTest + @EnumSource(StaturePotion.class) + void purpur26_3ConvertsBaseLessStaturePotionsWithoutTheBuild2618Correction(StaturePotion kind) { + HandlerList.unregisterAll(); + NativeStand stand = new NativeStand(); + stand.inventory.setItem(0, recipes.create(kind)); + stand.inventory.setIngredient(new ItemStack(Material.GUNPOWDER)); + stand.completeCycle(); + ItemStack result = stand.inventory.getItem(0); + assertEquals(Material.SPLASH_POTION, result.getType()); + assertEquals(kind, recipes.identify(result)); + assertTrue(stand.getItem(3).isEmpty()); + } + @ParameterizedTest @EnumSource(StaturePotion.class) void gunpowderActuallyConvertsEveryAuthenticatedDrinkable(StaturePotion kind) { @@ -127,7 +153,8 @@ class NativeBrewingTest { ItemStack result = stand.inventory.getItem(0); assertEquals(Material.LINGERING_POTION, result.getType()); assertEquals(kind, recipes.identify(result)); - assertTrue(stand.getItem(3).isEmpty(), "one successful conversion spends one dragon's breath"); + assertEquals(new ItemStack(Material.GLASS_BOTTLE), stand.inventory.getIngredient(), + "one successful conversion spends one dragon's breath and retains its bottle"); assertLingeringApplies(result, kind); } @@ -143,7 +170,7 @@ class NativeBrewingTest { stand.completeCycle(); assertEquals(Material.LINGERING_POTION, stand.inventory.getItem(0).getType()); assertEquals(kind, recipes.identify(stand.inventory.getItem(0))); - assertTrue(stand.getItem(3).isEmpty()); + assertEquals(new ItemStack(Material.GLASS_BOTTLE), stand.inventory.getIngredient()); assertEquals(18, stand.fuel); } @@ -271,7 +298,8 @@ class NativeBrewingTest { @ParameterizedTest @MethodSource("conversionEdits") - void inPlaceResultEditsCannotMutateInputOrBypassIngredientSafety(Material ingredient, boolean removeIdentity) { + void nativeResultEditsRemainDetachedAndIdentityRemovalCannotSpendIngredients( + Material ingredient, boolean removeIdentity) { events.registerEvent(BrewEvent.class, new Listener() {}, EventPriority.NORMAL, (listener, event) -> { ItemStack result = ((BrewEvent) event).getResults().get(0); result.editMeta(meta -> { @@ -288,13 +316,27 @@ class NativeBrewingTest { stand.inventory.setItem(0, original); stand.inventory.setIngredient(new ItemStack(ingredient)); stand.completeCycle(); - assertEquals(new ItemStack(ingredient), stand.inventory.getIngredient(), - "in-place output conflicts cannot spend ingredients"); - assertEquals(original, stand.inventory.getItem(0), "cancelled result edits must not mutate the input inventory"); + if (removeIdentity) { + assertEquals(new ItemStack(ingredient), stand.inventory.getIngredient(), + "identity removal cannot spend ingredients"); + assertEquals(original, stand.inventory.getItem(0), + "cancelled identity removal must not mutate the input inventory"); + } else { + assertEquals(ingredient == Material.GUNPOWDER ? null : new ItemStack(Material.GLASS_BOTTLE), + stand.inventory.getIngredient()); + ItemStack result = stand.inventory.getItem(0); + assertEquals(ingredient == Material.GUNPOWDER ? Material.SPLASH_POTION : Material.LINGERING_POTION, + result.getType()); + assertEquals(StaturePotion.GROWTH, recipes.identify(result)); + assertEquals(net.kyori.adventure.text.Component.text("Other plugin's conflicting edit"), + result.getItemMeta().displayName()); + assertEquals(StaturePotion.GROWTH, recipes.identify(original), + "native result edits must stay detached from the input object"); + } } @Test - void resultConstructionFailureDoesNotSpendTheIngredient() { + void purpur26_3NativeOutputDoesNotNeedTheCorrectionFactory() { HandlerList.unregisterAll(); PotionRecipes brokenFactory = new PotionRecipes(null, new NamespacedKey("spigotheights", "stature_potion"), material -> { throw new IllegalStateException("simulated item factory failure"); }); @@ -304,8 +346,9 @@ class NativeBrewingTest { stand.inventory.setItem(0, original); stand.inventory.setIngredient(new ItemStack(Material.GUNPOWDER)); stand.completeCycle(); - assertEquals(original, stand.inventory.getItem(0)); - assertEquals(new ItemStack(Material.GUNPOWDER), stand.inventory.getIngredient()); + assertEquals(Material.SPLASH_POTION, stand.inventory.getItem(0).getType()); + assertEquals(StaturePotion.GROWTH, recipes.identify(stand.inventory.getItem(0))); + assertTrue(stand.getItem(3).isEmpty()); } private void assertSplashApplies(ItemStack result, StaturePotion expected) { @@ -354,11 +397,11 @@ class NativeBrewingTest { private final class NativeStand extends BrewingStandBlockEntity { private final CraftInventoryBrewer inventory = new CraftInventoryBrewer(this); - private final Level level = mock(Level.class); + private final ServerLevel level = mock(ServerLevel.class); NativeStand() { super(BlockPos.ZERO, Blocks.BREWING_STAND.defaultBlockState()); - when(level.potionBrewing()).thenReturn(engine); + when(level.recipeAccess()).thenReturn(engine); fuel = 20; } diff --git a/src/nativeTest/java/games/dmg/spigotheights/NativeCarryRoutingTest.java b/src/nativeTest/java/games/dmg/spigotheights/NativeCarryRoutingTest.java index 3eb1335..eed920d 100644 --- a/src/nativeTest/java/games/dmg/spigotheights/NativeCarryRoutingTest.java +++ b/src/nativeTest/java/games/dmg/spigotheights/NativeCarryRoutingTest.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -/** Actual Purpur 2618 listener discovery, distinct handler lists and Paper event dispatch. +/** Actual Purpur build-2651 listener discovery, distinct handler lists and Paper event dispatch. * Only server/player boundaries are doubled; no listening server or client packets. */ class NativeCarryRoutingTest { private static Server previousServer; diff --git a/src/nativeTest/java/games/dmg/spigotheights/NativeRuntime.java b/src/nativeTest/java/games/dmg/spigotheights/NativeRuntime.java index 3365703..d8a0fd8 100644 --- a/src/nativeTest/java/games/dmg/spigotheights/NativeRuntime.java +++ b/src/nativeTest/java/games/dmg/spigotheights/NativeRuntime.java @@ -21,6 +21,7 @@ import net.minecraft.server.permissions.LevelBasedPermissionSet; import net.minecraft.tags.TagLoader; import net.minecraft.util.Util; import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.level.DataPackConfig; import net.minecraft.world.level.WorldDataConfiguration; import org.bukkit.craftbukkit.CraftRegistry; @@ -28,6 +29,7 @@ import org.bukkit.craftbukkit.CraftRegistry; /** Loads vanilla registries/tags/components using the same path as the server's own tests. */ final class NativeRuntime { private static boolean bootstrapped; + private static RecipeManager recipes; private NativeRuntime() {} @@ -44,12 +46,12 @@ final class NativeRuntime { try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, packs.openAllSelected())) { LayeredRegistryAccess layers = RegistryLayer.createRegistryAccess(); List> tags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC)); - List> lookups = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLDGEN), tags); - RegistryAccess.Frozen worldgen = RegistryDataLoader.load(resources, lookups, - RegistryDataLoader.WORLDGEN_REGISTRIES, Util.backgroundExecutor()).join(); - layers = layers.replaceFrom(RegistryLayer.WORLDGEN, worldgen); + List> lookups = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLD), tags); + RegistryAccess.Frozen world = RegistryDataLoader.load(resources, lookups, + RegistryDataLoader.WORLD_REGISTRIES, Util.backgroundExecutor()).join(); + layers = layers.replaceFrom(RegistryLayer.WORLD, world); RegistryAccess.Frozen dimensions = RegistryDataLoader.load(resources, - Stream.concat(lookups.stream(), worldgen.listRegistries()).toList(), + Stream.concat(lookups.stream(), world.listRegistries()).toList(), RegistryDataLoader.DIMENSION_REGISTRIES, Util.backgroundExecutor()).join(); layers = layers.replaceFrom(RegistryLayer.DIMENSIONS, dimensions); Class.forName(org.bukkit.Registry.class.getName()); @@ -58,7 +60,16 @@ final class NativeRuntime { Util.backgroundExecutor(), Runnable::run).join(); datapack.updateComponentsAndStaticRegistryTags(); CraftRegistry.setMinecraftRegistry(layers.compositeAccess().freeze()); + recipes = datapack.getRecipeManager(); + recipes.finalizeRecipeLoading(flags); bootstrapped = true; } } + + static RecipeManager recipes() { + if (!bootstrapped) { + throw new IllegalStateException("Native runtime has not been bootstrapped"); + } + return recipes; + } } diff --git a/src/test/java/games/dmg/spigotheights/PotionRecipesTest.java b/src/test/java/games/dmg/spigotheights/PotionRecipesTest.java index 1c3d0c5..8144d4f 100644 --- a/src/test/java/games/dmg/spigotheights/PotionRecipesTest.java +++ b/src/test/java/games/dmg/spigotheights/PotionRecipesTest.java @@ -64,7 +64,12 @@ class PotionRecipesTest { input.getItemMeta().getPersistentDataContainer().set(key, PersistentDataType.STRING, kind.name()); org.junit.jupiter.api.Assertions.assertTrue(mix.getInput().test(input)); org.junit.jupiter.api.Assertions.assertFalse(mix.getInput().test(mix.getResult())); - org.junit.jupiter.api.Assertions.assertTrue(mix.getIngredient().test(new BukkitDoubles.Item( + var ingredient = org.junit.jupiter.api.Assertions.assertInstanceOf( + org.bukkit.inventory.RecipeChoice.PredicateChoice.class, mix.getIngredient(), + "Purpur 26.3 builds brewing reagent indexes from representative recipe choices"); + assertEquals(splash ? Material.GUNPOWDER : Material.DRAGON_BREATH, + ingredient.getItemStack().getType()); + org.junit.jupiter.api.Assertions.assertTrue(ingredient.test(new BukkitDoubles.Item( splash ? Material.GUNPOWDER : Material.DRAGON_BREATH))); org.junit.jupiter.api.Assertions.assertFalse(mix.getIngredient().test(new BukkitDoubles.Item(Material.SUGAR))); } diff --git a/src/test/java/games/dmg/spigotheights/PurpurDistributionTest.java b/src/test/java/games/dmg/spigotheights/PurpurDistributionTest.java index f36ab65..c1d625f 100644 --- a/src/test/java/games/dmg/spigotheights/PurpurDistributionTest.java +++ b/src/test/java/games/dmg/spigotheights/PurpurDistributionTest.java @@ -22,9 +22,16 @@ class PurpurDistributionTest { assertEquals(version, metadata.get("version")); assertEquals("SpigotHeights", metadata.get("name"), "Retain the data directory and persistent namespace"); assertEquals("games.dmg.spigotheights.SpigotHeightsPlugin", metadata.get("main")); + assertEquals("1.21", metadata.get("api-version")); + assertTrue(((Map) metadata.get("commands")).containsKey("heights")); assertTrue(((Map) metadata.get("permissions")).containsKey("spigotheights.admin")); assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/") - || entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class"))); + || entry.getName().startsWith("org/purpurmc/") + || entry.getName().startsWith("net/minecraft/") + || entry.getName().startsWith("com/comphenix/protocol/") + || entry.getName().startsWith("org/junit/") + || entry.getName().startsWith("org/mockito/") + || entry.getName().endsWith("Test.class"))); } } @@ -64,9 +71,13 @@ class PurpurDistributionTest { } @Test - void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception { + void releaseAndCiAgreeWithThePurpur26_3DistributionAndToolchain() throws Exception { String build = Files.readString(Path.of("build.gradle.kts")); - assertTrue(build.contains("purpur-api:26.2.build.2618-stable")); + assertTrue(build.contains("purpur-api:26.3.build.2651-stable")); + assertTrue(build.contains("purpur-26.3-2651.jar")); + assertTrue(build.contains("https://api.purpurmc.org/v2/purpur/26.3/2651/download")); + assertTrue(build.contains("0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598")); + assertFalse(build.contains("26.2") || build.contains("2618")); assertFalse(build.contains("org.spigotmc:spigot-api")); for (String workflow : new String[] {"ci.yml", "release.yml"}) { String content = Files.readString(Path.of(".gitea/workflows", workflow)); -- 2.54.0