fix(heights): support Purpur 26.3
CI / build (push) Successful in 1m10s
CI / build (pull_request) Successful in 1m14s

This commit is contained in:
dmg committed 2026-10-16 22:44:55 -04:00
1 parent a6c80e7ac9
commit 68d4a854c3
9 files changed
+139 -48

No files matched your search

+6 -6
View File
@@ -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-<version>.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
+7 -7
View File
@@ -24,8 +24,8 @@ tasks.withType<JavaCompile>().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<JavaExec>("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)
@@ -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<? super ItemStack> 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();
@@ -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));
}
}
@@ -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();
if (removeIdentity) {
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");
"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;
}
@@ -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;
@@ -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<RegistryLayer> layers = RegistryLayer.createRegistryAccess();
List<Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC));
List<HolderLookup.RegistryLookup<?>> 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<HolderLookup.RegistryLookup<?>> 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;
}
}
@@ -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)));
}
@@ -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));