Merge pull request 'feat(platform): support Purpur 26.3' (#17) from ticket-407-support-purpur-26-3 into main
This commit was merged in pull request #17.
This commit is contained in:
commit
7ea6e254eb
20 files changed
+91
-50
No files matched your search
@@ -7,7 +7,7 @@ Fantasy Stick is the proposed transferable-character RPG plugin for Social Minec
|
||||
## Build and verify
|
||||
|
||||
- Java 25 toolchain (Gradle can resolve it), Gradle wrapper 9.6.1.
|
||||
- Purpur API/runtime `26.2.build.2618-stable`; `plugin.yml` declares API `26.2`.
|
||||
- Purpur API/runtime `26.3.build.2651-stable`; `plugin.yml` declares API `26.3`.
|
||||
- Node.js 20 or later and npm for the TypeScript release-tool tests.
|
||||
- ProtocolLib **5.4.0** is required at runtime and declared as a compile-only dependency; it is not bundled. Install the official [server distribution](https://github.com/dmulloy2/ProtocolLib/releases/tag/5.4.0), not the Maven library JAR (which lacks bundled runtime dependencies).
|
||||
|
||||
@@ -81,7 +81,7 @@ This is **not a transaction commit**. Eleven native planner/composition tests us
|
||||
|
||||
## Fixed-destination craft delivery
|
||||
|
||||
`ManualCraftDelivery` can apply a validated plan and exact-sized saved batch to the cursor, selected hotbar/offhand slot or bulk storage destinations. It rechecks current contents before claiming delivery, constructs all sheets first, then debits fresh native input mirrors and places all outputs before triggering one matrix recomputation. This deliberately relies on the pinned Purpur 2618 mirror/count/setter behavior. No further adapter-planned writes occur after the prepare-callback boundary. Existing native setters may send packets; this is not packet-silent or exception/crash-atomic.
|
||||
`ManualCraftDelivery` can apply a validated plan and exact-sized saved batch to the cursor, selected hotbar/offhand slot or bulk storage destinations. It rechecks current contents before claiming delivery, constructs all sheets first, then debits fresh native input mirrors and places all outputs before triggering one matrix recomputation. This deliberately relies on the pinned Purpur 2651 mirror/count/setter behavior. No further adapter-planned writes occur after the prepare-callback boundary. Existing native setters may send packets; this is not packet-silent or exception/crash-atomic.
|
||||
|
||||
Nine native delivery tests cover normal/right-click, bulk and exact-slot delivery, partial ingredient metadata, empty first matrix cells, stale context refusal, mismatched/abandoned/reused batches, reentrancy, callback item changes, new-menu cursor safety and post-callback exceptions. The fixture uses actual containers/cursor stacks and intercepts `slotsChanged`; it does not execute a listening server's recipe manager or event dispatch. A mismatch refuses before consuming cleanup authority; an exception after claiming delivery conservatively retains records rather than replaying or rolling back.
|
||||
|
||||
@@ -211,7 +211,7 @@ JSONL `effect.applied`/`effect.ended` records use `feature_id=dirt_path_speed` w
|
||||
|
||||
Speed I and Low-Light Vision retain their exact holder/session, native effect-instance identity and displaced-effect evidence while native removal is cancelled or unconfirmed. Bounded current-lifetime sweeps retry after downed containment ends; legitimate rescue preserves the same current lease. Equal-looking foreign instances and stronger/unrelated effects are not cleanup authority. A prior snapshot can be restored only after confirmed owned removal into an empty destination and within its remaining lifetime; fixed native tiers and replacement policies are unchanged.
|
||||
|
||||
A read-only adapter for **pinned Purpur 2618** checks native potion ticking and instance identity; bounded stack inspection refuses reentrant mutation during event/native-effect dispatch. This prevents queued by-type requests from later deleting a replacement and avoids treating mutable, in-flight event flags as completed mutation. Native maps, flags and event cancellation are never overridden. Runtime upgrades require renewed compatibility tests; an unavailable phase/identity is not permission for an unsafe fallback. No potion-event listener is needed, including for partial installation cleanup.
|
||||
A read-only adapter for **pinned Purpur 2651** checks native potion ticking and instance identity; bounded stack inspection refuses reentrant mutation during event/native-effect dispatch. This prevents queued by-type requests from later deleting a replacement and avoids treating mutable, in-flight event flags as completed mutation. Native maps, flags and event cancellation are never overridden. Runtime upgrades require renewed compatibility tests; an unavailable phase/identity is not permission for an unsafe fallback. No potion-event listener is needed, including for partial installation cleanup.
|
||||
|
||||
Owner close cancels its sweep timer and attempts only safe cleanup. Closing during native dispatch can schedule one post-dispatch attempt while the plugin is enabled, not an endless shutdown retry. Cancelled/unreadable/unsafe cleanup remains unresolved in the owner, with no false effect-end acknowledgment; shutdown or permanently denied removal does not guarantee cleanup. There is no cross-restart effect-ownership persistence, automatic recovery command or forced removal policy. Audit availability never authorizes native mutation or discards ownership evidence.
|
||||
|
||||
@@ -219,7 +219,7 @@ Owner close cancels its sweep timer and attempts only safe cleanup. Closing duri
|
||||
|
||||
Offhand entry applies actual native maximum health `min(10 + CON, native health limit)`, resumes saved remaining character health, and supplies the recurring blue active-character aura to native viewers who opt in with `/fstick particles players on`. The account-owned aura preference defaults to off; `/fstick particles players` reports it and `/fstick particles players off` hides only this recurring aura. The independent, default-on `/fstick particles all [on|off]` preference durably controls every explicit Fantasy Stick particle for that account without changing nearby players' delivery, sounds or gameplay. Turning `all` off temporarily takes precedence without overwriting the saved `players` choice. A fresh character's saved health starts full. CON has no gameplay cap; high values do not reject entry. Native damage/healing rules remain in effect, with the existing party/mixed-mode protection. The book reports current native health rather than waiting for an exit save.
|
||||
|
||||
`NativeMaximumHealth` owns only the effective-value cache/dirty callback of that player's existing native maximum-health instance. Base values, modifiers and native potion expiry remain owned by their original sources; nothing is copied back from an old modifier snapshot. The native player health methods and client attribute snapshot are tested. This adapter uses **pinned Purpur 2618 internals**, including private fields; a detached startup probe exercises installation, recalculation and removal before gameplay is registered. Runtime upgrades require renewed native compatibility verification, not a guessed fallback or global attribute-limit changes. Matching native health scaling displays the real fantasy maximum, not a separate virtual health pool; a later unrelated display-scale change is preserved on exit.
|
||||
`NativeMaximumHealth` owns only the effective-value cache/dirty callback of that player's existing native maximum-health instance. Base values, modifiers and native potion expiry remain owned by their original sources; nothing is copied back from an old modifier snapshot. The native player health methods and client attribute snapshot are tested. This adapter uses **pinned Purpur 2651 internals**, including private fields; a detached startup probe exercises installation, recalculation and removal before gameplay is registered. Runtime upgrades require renewed native compatibility verification, not a guessed fallback or global attribute-limit changes. Matching native health scaling displays the real fantasy maximum, not a separate virtual health pool; a later unrelated display-scale change is preserved on exit.
|
||||
|
||||
Ordinary health, food, saturation, exhaustion and health-display state belong to the holder/session, not the character sheet. The session receipt acknowledges this snapshot before native mutation; versioned player PDC records its application/restoration phase. Voluntary exit restores ordinary gear/vitals at the current location, with ordinary health capped by the current legitimate maximum. Character damage is saved on the worker, and the character claim/receipt cannot complete ahead of that acknowledgment. Quit and disable perform the same best-effort cleanup. Failed or uncertain saves retain the receipt and acknowledged character record instead of permitting a stale-health reentry. Missing/ambiguous native proof never authorizes a snapshot heal; process-owned maximum overrides are still retired independently of equipment/client-write failures.
|
||||
|
||||
|
||||
+7
-7
@@ -26,10 +26,10 @@ tasks.withType<JavaCompile>().configureEach {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable")
|
||||
compileOnly("net.dmulloy2:ProtocolLib:5.4.0")
|
||||
testImplementation("net.dmulloy2:ProtocolLib:5.4.0")
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-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")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
@@ -77,18 +77,18 @@ tasks.test {
|
||||
// Patch the exact supported runtime for non-listening native API tests, as in Purpur Base.
|
||||
val nativeRuntime = layout.buildDirectory.dir("native-runtime")
|
||||
val downloadNativeRuntime = tasks.register("downloadNativeRuntime") {
|
||||
val launcher = nativeRuntime.map { it.file("purpur-26.2-2618.jar") }
|
||||
val launcher = nativeRuntime.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)
|
||||
}
|
||||
@@ -96,7 +96,7 @@ val downloadNativeRuntime = tasks.register("downloadNativeRuntime") {
|
||||
val prepareNativeRuntime = tasks.register<JavaExec>("prepareNativeRuntime") {
|
||||
dependsOn(downloadNativeRuntime)
|
||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||
classpath = files(nativeRuntime.map { it.file("purpur-26.2-2618.jar") })
|
||||
classpath = files(nativeRuntime.map { it.file("purpur-26.3-2651.jar") })
|
||||
mainClass = "io.papermc.paperclip.Main"
|
||||
jvmArgs("-Dpaperclip.patchonly=true")
|
||||
workingDir(nativeRuntime)
|
||||
|
||||
@@ -25,7 +25,7 @@ public final class ManualCraftDelivery {
|
||||
var outputs = records.orElseThrow().stream().map(sheets::create).toList();
|
||||
var grid = (CraftingInventory) current.getTopInventory();
|
||||
var inputs = grid.getMatrix();
|
||||
// Purpur 2618: these are native mirrors; count changes do not call slotsChanged.
|
||||
// Purpur 2651: these are native mirrors; count changes do not call slotsChanged.
|
||||
for (var item : inputs) if (item != null && !item.isEmpty()) item.setAmount(item.getAmount() - plan.quantity());
|
||||
if (plan.route() == ManualCraftPlan.Route.CURSOR) current.setCursor(outputs.getFirst());
|
||||
else for (int index = 0; index < outputs.size(); index++) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.bukkit.craftbukkit.potion.CraftPotionEffectType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
/** Read-only Purpur 2618 phase/instance proof. Never changes native maps, events or mutation flags. */
|
||||
/** Read-only Purpur 2651 phase/instance proof. Never changes native maps, events or mutation flags. */
|
||||
final class NativePotionState {
|
||||
private static final Field TICKING = tickingField();
|
||||
private static final StackWalker STACK = StackWalker.getInstance();
|
||||
|
||||
@@ -25,7 +25,7 @@ import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Display-only codec pinned to Purpur 26.2 build 2618. No packet delivery, world insertion,
|
||||
* Display-only codec pinned to Purpur 26.3 build 2651. No packet delivery, world insertion,
|
||||
* profile/team changes, or authoritative player mutations belong here.
|
||||
*
|
||||
* <p>Player reads and detached-label creation/updates belong on the server thread. Pure
|
||||
|
||||
@@ -445,7 +445,7 @@ public final class ProtocolPresentation implements CharacterPresentation.Output
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ClientboundRemoveEntitiesPacket removal
|
||||
&& removal.getEntityIds().contains(decision.entity())) { mask |= UNTRACKED; }
|
||||
&& removal.entityIds().contains(decision.entity())) { mask |= UNTRACKED; }
|
||||
else if (packet instanceof ClientboundPlayerInfoRemovePacket removal
|
||||
&& removal.profileIds().contains(view.holder())) { mask |= TAB_REMOVED; }
|
||||
else if (packet instanceof ClientboundRespawnPacket) { mask |= UNTRACKED; }
|
||||
|
||||
@@ -176,7 +176,7 @@ public final class EquipmentSessionEvents implements Listener, AutoCloseable {
|
||||
int slot = player.getInventory().getHeldItemSlot();
|
||||
if (reservedExchange(player, slot)) {
|
||||
event.setCancelled(true);
|
||||
// Purpur 2618 snapshots the proposed hand stacks BEFORE dispatch. Never let its
|
||||
// Purpur 2651 snapshots the proposed hand stacks BEFORE dispatch. Never let its
|
||||
// continuation write the old fantasy control over the restored ordinary stack.
|
||||
if (Objects.equals(event.getMainHandItem(), player.getInventory().getItemInOffHand())
|
||||
&& Objects.equals(event.getOffHandItem(), player.getInventory().getItem(slot))) {
|
||||
@@ -408,7 +408,7 @@ public final class EquipmentSessionEvents implements Listener, AutoCloseable {
|
||||
try {
|
||||
boolean matching = draw != null && draw.item().equals(event.getItem())
|
||||
&& draw.item().equals(player.getActiveItem()) && event.getTicksHeldFor() >= 0;
|
||||
// Purpur 2618 reloads LivingEntity.useItem AFTER this event, then calls releaseUsing.
|
||||
// Purpur 2651 reloads LivingEntity.useItem AFTER this event, then calls releaseUsing.
|
||||
// Clear it first: cancelling EntityShootBowEvent alone is too late to own ammo draw.
|
||||
if (special(event.getItem()) && event.getItem().equals(player.getActiveItem())) player.clearActiveItem();
|
||||
if (matching) emit(player, draw.item(), draw.selected(), ControlInput.Gesture.DRAW_RELEASE,
|
||||
@@ -542,7 +542,7 @@ public final class EquipmentSessionEvents implements Listener, AutoCloseable {
|
||||
|| !Arrays.equals(BOX_SLOT_MAP, item.getPersistentDataContainer().get(BOX_SLOTS, PersistentDataType.INTEGER_ARRAY))
|
||||
|| !(entity instanceof Player player) || !box.owner().equals(player.getUniqueId())
|
||||
|| player.getInventory().getMaxStackSize() < 1) return true;
|
||||
// Purpur 2618 Inventory.add(-1, stack): factory nonstackable boxes cannot merge;
|
||||
// Purpur 2651 Inventory.add(-1, stack): factory nonstackable boxes cannot merge;
|
||||
// getFreeSlot scans 0..35, NOT the selected slot or only main storage. Read only:
|
||||
// leave native insertion to the caller, and never invent a recovery destination.
|
||||
for (int slot = 0; slot < 36; slot++) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.util.Objects;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
|
||||
/**
|
||||
* Purpur 2618 main-storage-only exchange, called on the server thread after revalidation.
|
||||
* Purpur 2651 main-storage-only exchange, called on the server thread after revalidation.
|
||||
* Public Craft/native accessors (as with NativeHealthLimit), no private-field access.
|
||||
* Native NonNullList.set only replaces a list reference: no item copying, packet emission,
|
||||
* equip notification, menu recomputation or plugin callback between the two writes.
|
||||
|
||||
@@ -6,7 +6,7 @@ import java.util.function.Consumer;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
|
||||
/**
|
||||
* Purpur 2618 instance-local effective-value lease. Base/modifiers remain in the native
|
||||
* Purpur 2651 instance-local effective-value lease. Base/modifiers remain in the native
|
||||
* instance, so additions, removals, potion expiry and serialization keep their normal owners.
|
||||
* The pinned dirty callback is synchronous: no polling interval can expose an external maximum.
|
||||
* No global attribute/range is changed. Native compatibility tests must gate runtime upgrades.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: FantasyStick
|
||||
version: ${version}
|
||||
main: games.dmg.fantasystick.FantasyStickPlugin
|
||||
api-version: "26.2"
|
||||
api-version: "26.3"
|
||||
depend: [ProtocolLib]
|
||||
description: Transferable characters, native fantasy health, recoverable equipment, character books and parties.
|
||||
author: dmg.games
|
||||
|
||||
@@ -776,10 +776,15 @@ class FantasyParticlesNativeTest {
|
||||
|
||||
private java.util.List<com.google.gson.JsonObject> preferenceRecords() throws Exception {
|
||||
var records = new java.util.ArrayList<com.google.gson.JsonObject>();
|
||||
try (var files = java.nio.file.Files.walk(data.resolve("events"))) {
|
||||
for (var path : files.filter(p -> p.getFileName().toString().endsWith(".jsonl")).toList())
|
||||
for (var line : java.nio.file.Files.readAllLines(path))
|
||||
if (!line.isBlank()) records.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
|
||||
try (var days = java.nio.file.Files.list(data.resolve("events"))) {
|
||||
for (var day : days.filter(path -> path.getFileName().toString()
|
||||
.matches("\\d{4}-\\d{2}-\\d{2}")).toList()) {
|
||||
try (var files = java.nio.file.Files.list(day)) {
|
||||
for (var path : files.filter(p -> p.getFileName().toString().endsWith(".jsonl")).toList())
|
||||
for (var line : java.nio.file.Files.readAllLines(path))
|
||||
if (!line.isBlank()) records.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -1325,7 +1325,7 @@ class FantasyStickLifecycleNativeTest {
|
||||
private static void assertRestored(Fixture f, Actor actor, Actor viewer) {
|
||||
var spawn = f.packets(viewer.player, net.minecraft.network.protocol.game.ClientboundAddEntityPacket.class).getLast();
|
||||
assertTrue(f.packets(viewer.player, net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket.class).stream()
|
||||
.anyMatch(packet -> packet.getEntityIds().contains(spawn.getId())));
|
||||
.anyMatch(packet -> packet.entityIds().contains(spawn.getId())));
|
||||
var tab = f.packets(viewer.player, net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.class).getLast();
|
||||
assertEquals(net.minecraft.network.chat.Component.literal("Ordinary tab"), tab.entries().getFirst().displayName());
|
||||
var attributes = f.packets(viewer.player, net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket.class).getLast();
|
||||
@@ -1432,9 +1432,9 @@ class FantasyStickLifecycleNativeTest {
|
||||
platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> nativeServer;
|
||||
case "isPrimaryThread" -> Thread.currentThread() == main;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
});
|
||||
var config = new io.papermc.paper.configuration.GlobalConfiguration();
|
||||
@@ -1497,8 +1497,8 @@ class FantasyStickLifecycleNativeTest {
|
||||
case "getLogger" -> Logger.getLogger("FantasyStick lifecycle fixture");
|
||||
case "getWarningState" -> org.bukkit.Warning.WarningState.DEFAULT;
|
||||
case "getName" -> "Purpur";
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "isPrimaryThread" -> Thread.currentThread() == main;
|
||||
case "getOnlinePlayers" -> List.copyOf(players.values());
|
||||
case "getWorlds" -> players.values().stream().map(org.bukkit.entity.Player::getWorld).distinct().toList();
|
||||
|
||||
@@ -204,7 +204,7 @@ class ManualCraftDeliveryNativeTest {
|
||||
assertEquals(ManualCraftDelivery.Result.DELIVERED, ManualCraftDelivery.commit(plan, batch, fixture.view));
|
||||
assertEquals(org.bukkit.Material.DIAMOND, fixture.view.getCursor().getType());
|
||||
assertEquals(record.id(), new CharacterSheets().identify(
|
||||
org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(fixture.menu.getCarried())).orElseThrow());
|
||||
org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitMirror(fixture.menu.getCarried())).orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ class ManualCraftPlanNativeTest {
|
||||
return human;
|
||||
}
|
||||
@Override public InventoryType getType() { return grid.getType(); }
|
||||
@Override public ItemStack getCursor() { return CraftItemStack.asCraftMirror(activeMenu.getCarried()); }
|
||||
@Override public ItemStack getCursor() { return CraftItemStack.asBukkitMirror(activeMenu.getCarried()); }
|
||||
@Override public void setCursor(ItemStack item) { activeMenu.setCarried(CraftItemStack.asNMSCopy(item)); }
|
||||
@Override public String getTitle() { return "Crafting fixture"; }
|
||||
@Override public String getOriginalTitle() { return getTitle(); }
|
||||
|
||||
@@ -15,7 +15,7 @@ class NativeCompatibilityTest {
|
||||
try (var jar = new JarFile(Path.of(System.getProperty("distribution.jar")).toFile());
|
||||
var descriptor = jar.getInputStream(jar.getJarEntry("plugin.yml"))) {
|
||||
var metadata = new PluginDescriptionFile(descriptor);
|
||||
assertEquals("26.2", metadata.getAPIVersion());
|
||||
assertEquals("26.3", metadata.getAPIVersion());
|
||||
assertEquals(ApiVersion.CURRENT, ApiVersion.getOrCreateVersion(metadata.getAPIVersion()));
|
||||
assertEquals("games.dmg.fantasystick.FantasyStickPlugin", metadata.getMain());
|
||||
assertTrue(org.bukkit.plugin.java.JavaPlugin.class.isAssignableFrom(FantasyStickPlugin.class));
|
||||
|
||||
@@ -284,8 +284,8 @@ class NativePresentationPacketsTest {
|
||||
assertEquals(Component.literal("Old"), originalMetadata.packedItems().stream()
|
||||
.filter(value -> value.id() == changed.getFirst().id()).findFirst().orElseThrow().value());
|
||||
var removal = wire(net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket.STREAM_CODEC, label.remove());
|
||||
assertArrayEquals(new int[] {id}, removal.getEntityIds().toIntArray());
|
||||
assertArrayEquals(new int[] {id}, label.remove().getEntityIds().toIntArray());
|
||||
assertArrayEquals(new int[] {id}, removal.entityIds().toIntArray());
|
||||
assertArrayEquals(new int[] {id}, label.remove().entityIds().toIntArray());
|
||||
assertEquals(id, label.id());
|
||||
assertEquals(uuid, label.uuid());
|
||||
assertEquals(allocated, player.world.chunks.checks, "Updates and removals never allocate replacement IDs");
|
||||
@@ -392,7 +392,7 @@ class NativePresentationPacketsTest {
|
||||
private static final class ReadChunks extends net.minecraft.server.level.ServerChunkCache {
|
||||
private int checks;
|
||||
private boolean rejectNext;
|
||||
private ReadChunks() { super(null, null, null, null, null, null, 0, 0, false, null, null, null); }
|
||||
private ReadChunks() { super(null, null, null, null, null, null, 0, 0, false, null, null); }
|
||||
@Override public boolean hasEntityWithId(int id) {
|
||||
checks++;
|
||||
boolean reject = rejectNext;
|
||||
|
||||
@@ -40,12 +40,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"); // Initialize the fixed API registry before loading data packs.
|
||||
|
||||
@@ -319,7 +319,7 @@ class ProtocolPresentationNativeTest {
|
||||
assertInstanceOf(ClientboundBundlePacket.class, event.getPacket().getHandle()).subPackets().forEach(packets::add);
|
||||
assertEquals(2, packets.size());
|
||||
assertSame(original, packets.getFirst()); assertSame(unrelated, packets.getLast());
|
||||
assertArrayEquals(new int[] {label}, f.packets(f.viewer, ClientboundRemoveEntitiesPacket.class).getLast().getEntityIds().toIntArray());
|
||||
assertArrayEquals(new int[] {label}, f.packets(f.viewer, ClientboundRemoveEntitiesPacket.class).getLast().entityIds().toIntArray());
|
||||
assertEquals(1, f.packets(f.viewer, ClientboundRemoveEntitiesPacket.class).size());
|
||||
assertTrue(packets.stream().noneMatch(ClientboundBundlePacket.class::isInstance));
|
||||
verifyNoInteractions(errors);
|
||||
@@ -484,7 +484,7 @@ class ProtocolPresentationNativeTest {
|
||||
var sent = f.sent.get(i);
|
||||
if (sent.viewer() != f.viewer) { continue; }
|
||||
if (sent.packet() instanceof ClientboundAddEntityPacket spawn && spawn.getId() == oldId) { oldSpawn = i; }
|
||||
if (sent.packet() instanceof ClientboundRemoveEntitiesPacket remove && remove.getEntityIds().contains(oldId)) { oldRemove = i; }
|
||||
if (sent.packet() instanceof ClientboundRemoveEntitiesPacket remove && remove.entityIds().contains(oldId)) { oldRemove = i; }
|
||||
}
|
||||
assertTrue(oldSpawn >= 0 && oldRemove > oldSpawn, "No old label may escape after its cleanup");
|
||||
assertEquals(2, f.packets(f.viewer, ClientboundAddEntityPacket.class).size());
|
||||
@@ -528,7 +528,7 @@ class ProtocolPresentationNativeTest {
|
||||
f.tick.run(); f.snapshot.set(Map.of()); f.output.clear(newer);
|
||||
var removed = new java.util.HashSet<Integer>();
|
||||
f.packets(f.viewer, ClientboundRemoveEntitiesPacket.class).forEach(packet -> {
|
||||
for (int id : packet.getEntityIds().toIntArray()) { removed.add(id); }
|
||||
for (int id : packet.entityIds().toIntArray()) { removed.add(id); }
|
||||
});
|
||||
for (var spawn : f.packets(f.viewer, ClientboundAddEntityPacket.class)) {
|
||||
assertTrue(removed.contains(spawn.getId()), "Every escaped label remains owned until cleanup: " + spawn.getId());
|
||||
@@ -866,7 +866,7 @@ class ProtocolPresentationNativeTest {
|
||||
assertSame(original, event.getPacket().getHandle(), "Retain native removal/concealment packet and every field");
|
||||
var removed = wire(ClientboundRemoveEntitiesPacket.STREAM_CODEC,
|
||||
fixture.packets(fixture.viewer, ClientboundRemoveEntitiesPacket.class).getLast());
|
||||
assertArrayEquals(new int[] {label}, removed.getEntityIds().toIntArray());
|
||||
assertArrayEquals(new int[] {label}, removed.entityIds().toIntArray());
|
||||
assertFalse(event.isCancelled());
|
||||
}
|
||||
|
||||
@@ -900,9 +900,9 @@ class ProtocolPresentationNativeTest {
|
||||
platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> server;
|
||||
case "isPrimaryThread" -> true;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
});
|
||||
var config = new io.papermc.paper.configuration.GlobalConfiguration();
|
||||
|
||||
@@ -27,7 +27,7 @@ class DistributionTest {
|
||||
assertEquals(version, metadata.getVersion());
|
||||
assertFalse(metadata.getVersion().contains("${"));
|
||||
assertEquals("games.dmg.fantasystick.FantasyStickPlugin", metadata.getMain());
|
||||
assertNotNull(metadata.getAPIVersion(), "Declare the native API compatibility contract");
|
||||
assertEquals("26.3", metadata.getAPIVersion(), "Declare the exact native API compatibility contract");
|
||||
assertEquals(java.util.List.of("ProtocolLib"), metadata.getDepend());
|
||||
assertEquals(java.util.Set.of("fstick", "fstickadmin"), metadata.getCommands().keySet());
|
||||
assertNull(metadata.getCommands().get("fstick").get("permission"),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PurpurDistributionTest {
|
||||
private static final String API = "org.purpurmc.purpur:purpur-api:26.3.build.2651-stable";
|
||||
private static final String RUNTIME = "purpur-26.3-2651.jar";
|
||||
private static final String DOWNLOAD = "https://api.purpurmc.org/v2/purpur/26.3/2651/download";
|
||||
private static final String SHA256 = "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598";
|
||||
|
||||
@Test
|
||||
void pinsTheExactApprovedApiAndRuntime() throws Exception {
|
||||
String build = Files.readString(Path.of("build.gradle.kts"));
|
||||
assertTrue(build.contains("compileOnly(\"" + API + "\")"));
|
||||
assertTrue(build.contains("testImplementation(\"" + API + "\")"));
|
||||
assertTrue(build.contains(RUNTIME));
|
||||
assertTrue(build.contains(DOWNLOAD));
|
||||
assertTrue(build.contains(SHA256));
|
||||
assertFalse(build.contains("26.2.build.2618-stable"));
|
||||
assertFalse(build.contains("purpur-26.2-2618.jar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retainsTheApprovedProtocolLibDependency() throws Exception {
|
||||
String build = Files.readString(Path.of("build.gradle.kts"));
|
||||
String descriptor = Files.readString(Path.of("src/main/resources/plugin.yml"));
|
||||
assertTrue(build.contains("compileOnly(\"net.dmulloy2:ProtocolLib:5.4.0\")"));
|
||||
assertTrue(build.contains("testImplementation(\"net.dmulloy2:ProtocolLib:5.4.0\")"));
|
||||
assertTrue(descriptor.contains("depend: [ProtocolLib]"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user