From 365d1e9bed6e8d09610087d40eba8107295440d8 Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 18:57:27 +0800
Subject: [PATCH 1/7] chore: target Paper 26.3 API
---
build.gradle.kts | 2 +-
core/build.gradle.kts | 2 +-
paper/build.gradle.kts | 2 +-
.../paper/methods/BuildItemPaper.java | 34 ++++++++++++++-----
.../paper/methods/DebuildItemPaper.java | 32 +++++++++++++----
plugin/build.gradle.kts | 2 +-
6 files changed, 54 insertions(+), 20 deletions(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index d67afde8..701e5921 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -15,7 +15,7 @@ subprojects {
configurations.configureEach {
if (isCanBeResolved) {
- // Upstream Paper 26.2 publishes Java 25 metadata. Resolve against JDK 25
+ // Paper 26.3 publishes Java 25 metadata. Resolve against JDK 25
// while JavaCompile below still emits Java 21-compatible bytecode.
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 58520f95..b9a163b2 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,7 +1,7 @@
dependencies {
compileOnly(files("lib/LegacyCodesCompatibility.jar"))
compileOnly("net.kyori:adventure-api:5.2.0")
- compileOnly("io.papermc.paper:paper-api:26.2.build.55-alpha")
+ compileOnly("io.papermc.paper:paper-api:26.3.build.157-beta")
implementation("org.bstats:bstats-bukkit:3.1.0")
compileOnly("org.geysermc.floodgate:api:2.2.2-SNAPSHOT")
implementation("com.cronutils:cron-utils:9.2.0")
diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts
index e43c1c18..4302fdd7 100644
--- a/paper/build.gradle.kts
+++ b/paper/build.gradle.kts
@@ -1,5 +1,5 @@
dependencies {
compileOnly("net.kyori:adventure-api:5.2.0")
- compileOnly("io.papermc.paper:paper-api:26.2.build.55-alpha")
+ compileOnly("io.papermc.paper:paper-api:26.3.build.157-beta")
compileOnly(project(":core"))
}
diff --git a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java
index 4700403e..76976a20 100644
--- a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java
+++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java
@@ -880,19 +880,22 @@ public static ItemStack editItemStack(ItemStack item,
item.setData(DataComponentTypes.PIERCING_WEAPON, builder.build());
}
- // Swing Animation
+ // Swing Animation (renamed in Minecraft 26.3)
ConfigurationSection swingAnimationKey = section.getConfigurationSection("swing-animation");
if (swingAnimationKey != null) {
- SwingAnimation.Builder builder = SwingAnimation.swingAnimation();
- String typeStr = swingAnimationKey.getString("type");
- if (typeStr != null) {
- builder.type(Enums.getIfPresent(SwingAnimation.Animation.class, typeStr).or(SwingAnimation.Animation.NONE));
+ SwingAnimation animation = buildSwingAnimation(swingAnimationKey);
+ // Preserve the old component's behavior: it applied to both attacks and interactions.
+ item.setData(DataComponentTypes.ATTACK_ANIMATION, animation);
+ item.setData(DataComponentTypes.INTERACT_ANIMATION, animation);
+ } else {
+ ConfigurationSection attackAnimationKey = section.getConfigurationSection("attack-animation");
+ if (attackAnimationKey != null) {
+ item.setData(DataComponentTypes.ATTACK_ANIMATION, buildSwingAnimation(attackAnimationKey));
}
- int duration = swingAnimationKey.getInt("duration", -1);
- if (duration > 0) {
- builder.duration(duration);
+ ConfigurationSection interactAnimationKey = section.getConfigurationSection("interact-animation");
+ if (interactAnimationKey != null) {
+ item.setData(DataComponentTypes.INTERACT_ANIMATION, buildSwingAnimation(interactAnimationKey));
}
- item.setData(DataComponentTypes.SWING_ANIMATION, builder.build());
}
// Use Effects
@@ -946,6 +949,19 @@ public static ItemStack editItemStack(ItemStack item,
return item;
}
+ private static SwingAnimation buildSwingAnimation(ConfigurationSection section) {
+ SwingAnimation.Builder builder = SwingAnimation.swingAnimation();
+ String type = section.getString("type");
+ if (type != null) {
+ builder.type(Enums.getIfPresent(SwingAnimation.Animation.class, type).or(SwingAnimation.Animation.NONE));
+ }
+ int duration = section.getInt("duration", -1);
+ if (duration >= 0) {
+ builder.duration(duration);
+ }
+ return builder.build();
+ }
+
private static KineticWeapon.Condition parseKineticWeaponCondition(
ConfigurationSection parent,
String key
diff --git a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java
index f5544c55..18d3e0d0 100644
--- a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java
+++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java
@@ -639,13 +639,21 @@ public static ConfigurationSection serializeItemStack(ItemStack item) {
}
}
- // Swing Animation
- if (item.isDataOverridden(DataComponentTypes.SWING_ANIMATION)) {
- SwingAnimation sa = item.getData(DataComponentTypes.SWING_ANIMATION);
- if (sa != null) {
- ConfigurationSection saSection = section.createSection("swing-animation");
- saSection.set("type", sa.type().name());
- saSection.set("duration", sa.duration());
+ // Swing Animation (renamed in Minecraft 26.3)
+ SwingAnimation attackAnimation = item.isDataOverridden(DataComponentTypes.ATTACK_ANIMATION)
+ ? item.getData(DataComponentTypes.ATTACK_ANIMATION) : null;
+ SwingAnimation interactAnimation = item.isDataOverridden(DataComponentTypes.INTERACT_ANIMATION)
+ ? item.getData(DataComponentTypes.INTERACT_ANIMATION) : null;
+ if (attackAnimation != null && interactAnimation != null
+ && attackAnimation.type() == interactAnimation.type()
+ && attackAnimation.duration() == interactAnimation.duration()) {
+ serializeSwingAnimation(section, "swing-animation", attackAnimation);
+ } else {
+ if (attackAnimation != null) {
+ serializeSwingAnimation(section, "attack-animation", attackAnimation);
+ }
+ if (interactAnimation != null) {
+ serializeSwingAnimation(section, "interact-animation", interactAnimation);
}
}
@@ -678,6 +686,16 @@ public static ConfigurationSection serializeItemStack(ItemStack item) {
return section;
}
+ private static void serializeSwingAnimation(
+ ConfigurationSection parent,
+ String key,
+ SwingAnimation animation
+ ) {
+ ConfigurationSection animationSection = parent.createSection(key);
+ animationSection.set("type", animation.type().name());
+ animationSection.set("duration", animation.duration());
+ }
+
private static Object serializeDamageTypes(RegistryKeySet damageTypes) {
if (damageTypes instanceof Tag> tag) {
return tag.tagKey().key().asString();
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
index 878ffdc2..b7739bf8 100644
--- a/plugin/build.gradle.kts
+++ b/plugin/build.gradle.kts
@@ -6,7 +6,7 @@ dependencies {
implementation(project(":core"))
implementation(project(":spigot"))
implementation(project(":paper"))
- compileOnly("io.papermc.paper:paper-api:26.2.build.55-alpha")
+ compileOnly("io.papermc.paper:paper-api:26.3.build.157-beta")
}
tasks.shadowJar {
From 6abd6389fc491c14ccbd98d34a8a2c419557e7c1 Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 19:43:06 +0800
Subject: [PATCH 2/7] docs: describe Paper 26.3 item component migration
---
docs/format/itemformat-tm/component-format.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/format/itemformat-tm/component-format.md b/docs/format/itemformat-tm/component-format.md
index 32dc396d..746eca57 100644
--- a/docs/format/itemformat-tm/component-format.md
+++ b/docs/format/itemformat-tm/component-format.md
@@ -261,3 +261,6 @@ component:
speed-multiplier: 0.4
```
+
+> Paper/Minecraft 26.3 removed the vanilla `swing_animation` component and replaced it with `attack_animation` and `interact_animation`. UltimateShop keeps `swing-animation` as a compatibility key and writes the same value to both new components. Use `attack-animation` or `interact-animation` separately when different attack and interaction animations are required.
+
From b3ec5375460064e41a60eecb37ca12796bd1735f Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 20:11:15 +0800
Subject: [PATCH 3/7] ci: validate the 26.3 upgrade branch
---
.github/workflows/build.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 692a6449..c232ec5a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- 'feature/new-branch'
+ - 'chore/minecraft-26.3'
+ pull_request:
workflow_dispatch:
env:
@@ -14,7 +16,7 @@ jobs:
build:
runs-on: ubuntu-latest
permissions:
- contents: write
+ contents: read
steps:
- name: Checkout repository
From 1c27532a78ff37c64bc59f22bc29d9733adc6d39 Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 23:18:22 +0800
Subject: [PATCH 4/7] fix: adapt swing animation components across Paper
versions
---
.github/workflows/build.yml | 12 ++-
.gitignore | 5 +
core/build.gradle.kts | 4 +-
paper/build.gradle.kts | 13 ++-
.../paper/methods/BuildItemPaper.java | 8 +-
.../paper/methods/DebuildItemPaper.java | 7 +-
.../paper/utils/SwingAnimationResolver.java | 99 +++++++++++++++++++
.../utils/SwingAnimationResolverTest.java | 29 ++++++
plugin/build.gradle.kts | 4 +-
9 files changed, 168 insertions(+), 13 deletions(-)
create mode 100644 .gitignore
create mode 100644 paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java
create mode 100644 paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c232ec5a..19ddda31 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,6 +14,10 @@ env:
jobs:
build:
+ strategy:
+ fail-fast: false
+ matrix:
+ paper-version: ['26.2.build.130-stable', '26.3.build.157-beta']
runs-on: ubuntu-latest
permissions:
contents: read
@@ -77,11 +81,13 @@ jobs:
- name: Build with Gradle
run: |
+ gradle :paper:testSwingAnimationResolver --no-daemon -PpaperVersion=${{ matrix.paper-version }}
gradle :core:testMath --no-daemon
gradle :core:testDecay --no-daemon
gradle :core:testDecayExtended --no-daemon
gradle :core:testPureUtils --no-daemon
- gradle clean shadowJar --no-daemon
+ gradle clean shadowJar --no-daemon -PpaperVersion=${{ matrix.paper-version }}
+ echo "Paper API matrix: ${{ matrix.paper-version }}"
if ! javap -verbose -classpath core/build/classes/java/main \
cn.superiormc.ultimateshop.hooks.economy.EconomyExcellentEconomyHook \
| grep "major version: 65" >/dev/null; then
@@ -92,6 +98,8 @@ jobs:
if [ -n "$JAR_FILE" ]; then
echo "Build successful!"
ls -lh "$JAR_FILE"
+ unzip -tq "$JAR_FILE" >/dev/null
+ unzip -p "$JAR_FILE" plugin.yml | grep -E '^main:|^api-version:'
else
echo "Build failed - no JAR file found"
exit 1
@@ -105,4 +113,4 @@ jobs:
plugin/build/libs/UltimateShop-*.jar
!plugin/build/libs/*-sources.jar
!plugin/build/libs/*-javadoc.jar
- retention-days: 30
+ retention-days: 30
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..d476ee15
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.gradle/
+build/
+**/build/
+*.iml
+.idea/
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index b9a163b2..cece0726 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,7 +1,9 @@
+val paperVersion = providers.gradleProperty("paperVersion").getOrElse("26.3.build.157-beta")
+
dependencies {
compileOnly(files("lib/LegacyCodesCompatibility.jar"))
compileOnly("net.kyori:adventure-api:5.2.0")
- compileOnly("io.papermc.paper:paper-api:26.3.build.157-beta")
+ compileOnly("io.papermc.paper:paper-api:$paperVersion")
implementation("org.bstats:bstats-bukkit:3.1.0")
compileOnly("org.geysermc.floodgate:api:2.2.2-SNAPSHOT")
implementation("com.cronutils:cron-utils:9.2.0")
diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts
index 4302fdd7..6480074a 100644
--- a/paper/build.gradle.kts
+++ b/paper/build.gradle.kts
@@ -1,5 +1,16 @@
+val paperVersion = providers.gradleProperty("paperVersion").getOrElse("26.3.build.157-beta")
+
dependencies {
compileOnly("net.kyori:adventure-api:5.2.0")
- compileOnly("io.papermc.paper:paper-api:26.3.build.157-beta")
+ compileOnly("io.papermc.paper:paper-api:$paperVersion")
+ testRuntimeOnly("io.papermc.paper:paper-api:$paperVersion")
compileOnly(project(":core"))
}
+
+tasks.register("testSwingAnimationResolver") {
+ group = "verification"
+ description = "Verify old and split Paper animation component resolution"
+ mainClass.set("cn.superiormc.ultimateshop.paper.utils.SwingAnimationResolverTest")
+ dependsOn(tasks.named("testClasses"))
+ classpath = sourceSets["test"].runtimeClasspath
+}
diff --git a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java
index 76976a20..0583d9e8 100644
--- a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java
+++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java
@@ -1,6 +1,7 @@
package cn.superiormc.ultimateshop.paper.methods;
import cn.superiormc.ultimateshop.paper.utils.PaperTextUtil;
+import cn.superiormc.ultimateshop.paper.utils.SwingAnimationResolver;
import cn.superiormc.ultimateshop.utils.CommonUtil;
import cn.superiormc.ultimateshop.utils.TextUtil;
import com.destroystokyo.paper.profile.PlayerProfile;
@@ -885,16 +886,15 @@ public static ItemStack editItemStack(ItemStack item,
if (swingAnimationKey != null) {
SwingAnimation animation = buildSwingAnimation(swingAnimationKey);
// Preserve the old component's behavior: it applied to both attacks and interactions.
- item.setData(DataComponentTypes.ATTACK_ANIMATION, animation);
- item.setData(DataComponentTypes.INTERACT_ANIMATION, animation);
+ SwingAnimationResolver.setUnified(item, animation);
} else {
ConfigurationSection attackAnimationKey = section.getConfigurationSection("attack-animation");
if (attackAnimationKey != null) {
- item.setData(DataComponentTypes.ATTACK_ANIMATION, buildSwingAnimation(attackAnimationKey));
+ SwingAnimationResolver.setAttack(item, buildSwingAnimation(attackAnimationKey));
}
ConfigurationSection interactAnimationKey = section.getConfigurationSection("interact-animation");
if (interactAnimationKey != null) {
- item.setData(DataComponentTypes.INTERACT_ANIMATION, buildSwingAnimation(interactAnimationKey));
+ SwingAnimationResolver.setInteract(item, buildSwingAnimation(interactAnimationKey));
}
}
diff --git a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java
index 18d3e0d0..6132eeb9 100644
--- a/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java
+++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/DebuildItemPaper.java
@@ -2,6 +2,7 @@
import cn.superiormc.ultimateshop.methods.Items.DebuildItem;
import cn.superiormc.ultimateshop.paper.utils.PaperTextUtil;
+import cn.superiormc.ultimateshop.paper.utils.SwingAnimationResolver;
import cn.superiormc.ultimateshop.utils.CommonUtil;
import com.destroystokyo.paper.profile.ProfileProperty;
import io.papermc.paper.datacomponent.DataComponentType;
@@ -640,10 +641,8 @@ public static ConfigurationSection serializeItemStack(ItemStack item) {
}
// Swing Animation (renamed in Minecraft 26.3)
- SwingAnimation attackAnimation = item.isDataOverridden(DataComponentTypes.ATTACK_ANIMATION)
- ? item.getData(DataComponentTypes.ATTACK_ANIMATION) : null;
- SwingAnimation interactAnimation = item.isDataOverridden(DataComponentTypes.INTERACT_ANIMATION)
- ? item.getData(DataComponentTypes.INTERACT_ANIMATION) : null;
+ SwingAnimation attackAnimation = SwingAnimationResolver.attack(item);
+ SwingAnimation interactAnimation = SwingAnimationResolver.interact(item);
if (attackAnimation != null && interactAnimation != null
&& attackAnimation.type() == interactAnimation.type()
&& attackAnimation.duration() == interactAnimation.duration()) {
diff --git a/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java
new file mode 100644
index 00000000..e62566eb
--- /dev/null
+++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java
@@ -0,0 +1,99 @@
+package cn.superiormc.ultimateshop.paper.utils;
+
+import io.papermc.paper.datacomponent.DataComponentType;
+import io.papermc.paper.datacomponent.DataComponentTypes;
+import io.papermc.paper.datacomponent.item.SwingAnimation;
+import org.bukkit.inventory.ItemStack;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Runtime-safe bridge for Minecraft 26.2's SWING_ANIMATION and 26.3's
+ * ATTACK_ANIMATION/INTERACT_ANIMATION component split.
+ *
+ * Do not reference the version-specific fields directly: JVM field
+ * resolution would throw NoSuchFieldError before a version check could run.
+ */
+public final class SwingAnimationResolver {
+ private static final ComponentNames NAMES = classify(
+ Arrays.stream(DataComponentTypes.class.getFields()).map(Field::getName).toList());
+ private static final DataComponentType.Valued LEGACY = resolve("SWING_ANIMATION");
+ private static final DataComponentType.Valued ATTACK = resolve("ATTACK_ANIMATION");
+ private static final DataComponentType.Valued INTERACT = resolve("INTERACT_ANIMATION");
+
+ private SwingAnimationResolver() {
+ }
+
+ public record ComponentNames(boolean legacy, boolean split) {
+ }
+
+ /** Pure resolver used by unit tests with fake old/split field sets. */
+ public static ComponentNames classify(Iterable fieldNames) {
+ Set names = new LinkedHashSet<>();
+ for (String name : fieldNames) {
+ names.add(name);
+ }
+ return new ComponentNames(names.contains("SWING_ANIMATION"),
+ names.contains("ATTACK_ANIMATION") && names.contains("INTERACT_ANIMATION"));
+ }
+
+ public static ComponentNames componentNames() {
+ return NAMES;
+ }
+
+ public static void setUnified(ItemStack item, SwingAnimation animation) {
+ if (NAMES.split() && ATTACK != null && INTERACT != null) {
+ item.setData(ATTACK, animation);
+ item.setData(INTERACT, animation);
+ } else if (LEGACY != null) {
+ item.setData(LEGACY, animation);
+ }
+ }
+
+ public static void setAttack(ItemStack item, SwingAnimation animation) {
+ if (NAMES.split() && ATTACK != null) {
+ item.setData(ATTACK, animation);
+ } else if (LEGACY != null) {
+ item.setData(LEGACY, animation);
+ }
+ }
+
+ public static void setInteract(ItemStack item, SwingAnimation animation) {
+ if (NAMES.split() && INTERACT != null) {
+ item.setData(INTERACT, animation);
+ } else if (LEGACY != null) {
+ item.setData(LEGACY, animation);
+ }
+ }
+
+ public static SwingAnimation attack(ItemStack item) {
+ if (NAMES.split() && ATTACK != null) {
+ return value(item, ATTACK);
+ }
+ return value(item, LEGACY);
+ }
+
+ public static SwingAnimation interact(ItemStack item) {
+ if (NAMES.split() && INTERACT != null) {
+ return value(item, INTERACT);
+ }
+ return value(item, LEGACY);
+ }
+
+ private static SwingAnimation value(ItemStack item, DataComponentType.Valued type) {
+ return type != null && item.isDataOverridden(type) ? item.getData(type) : null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static DataComponentType.Valued resolve(String fieldName) {
+ try {
+ Field field = DataComponentTypes.class.getField(fieldName);
+ return (DataComponentType.Valued) field.get(null);
+ } catch (ReflectiveOperationException | ClassCastException ignored) {
+ return null;
+ }
+ }
+}
diff --git a/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java b/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java
new file mode 100644
index 00000000..15d3f703
--- /dev/null
+++ b/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java
@@ -0,0 +1,29 @@
+package cn.superiormc.ultimateshop.paper.utils;
+
+import java.util.Set;
+
+/** Small no-framework regression test for old/split component field resolution. */
+public final class SwingAnimationResolverTest {
+ public static void main(String[] args) {
+ SwingAnimationResolver.ComponentNames old = SwingAnimationResolver.classify(Set.of("SWING_ANIMATION"));
+ require(old.legacy() && !old.split(), "old SWING_ANIMATION layout must be detected");
+
+ SwingAnimationResolver.ComponentNames split = SwingAnimationResolver.classify(
+ Set.of("ATTACK_ANIMATION", "INTERACT_ANIMATION"));
+ require(!split.legacy() && split.split(), "26.3 split animation layout must be detected");
+
+ SwingAnimationResolver.ComponentNames partial = SwingAnimationResolver.classify(
+ Set.of("ATTACK_ANIMATION"));
+ require(!partial.split(), "one split field must not activate split mode");
+
+ SwingAnimationResolver.ComponentNames runtime = SwingAnimationResolver.componentNames();
+ require(runtime.legacy() || runtime.split(), "Paper runtime must expose an animation layout");
+ System.out.println("SwingAnimationResolverTest passed: " + runtime);
+ }
+
+ private static void require(boolean condition, String message) {
+ if (!condition) {
+ throw new AssertionError(message);
+ }
+ }
+}
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
index b7739bf8..b8df6e91 100644
--- a/plugin/build.gradle.kts
+++ b/plugin/build.gradle.kts
@@ -1,3 +1,5 @@
+val paperVersion = providers.gradleProperty("paperVersion").getOrElse("26.3.build.157-beta")
+
plugins {
id("com.gradleup.shadow")
}
@@ -6,7 +8,7 @@ dependencies {
implementation(project(":core"))
implementation(project(":spigot"))
implementation(project(":paper"))
- compileOnly("io.papermc.paper:paper-api:26.3.build.157-beta")
+ compileOnly("io.papermc.paper:paper-api:$paperVersion")
}
tasks.shadowJar {
From fcc3abb49fe32a1ed368b4e5200d6baa8e2ebdd7 Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 23:21:19 +0800
Subject: [PATCH 5/7] ci: separate API matrix artifacts
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 19ddda31..280ca1cf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -108,7 +108,7 @@ jobs:
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
- name: UltimateShop-Build
+ name: UltimateShop-Build-${{ matrix.paper-version }}
path: |
plugin/build/libs/UltimateShop-*.jar
!plugin/build/libs/*-sources.jar
From 9920abc6e2e157b7e2bdb95f7e2d5f6332984b6b Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 23:24:07 +0800
Subject: [PATCH 6/7] test: avoid Paper registry init in animation resolver
test
---
.../paper/utils/SwingAnimationResolver.java | 83 ++++++++++++-------
.../utils/SwingAnimationResolverTest.java | 6 +-
2 files changed, 58 insertions(+), 31 deletions(-)
diff --git a/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java
index e62566eb..f74a8269 100644
--- a/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java
+++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java
@@ -15,14 +15,12 @@
* ATTACK_ANIMATION/INTERACT_ANIMATION component split.
*
* Do not reference the version-specific fields directly: JVM field
- * resolution would throw NoSuchFieldError before a version check could run.
+ * resolution would throw NoSuchFieldError before a version check could run.
+ * Resolution is lazy because the Paper API's DataComponentTypes class needs a
+ * live registry implementation when it initializes outside a server.
*/
public final class SwingAnimationResolver {
- private static final ComponentNames NAMES = classify(
- Arrays.stream(DataComponentTypes.class.getFields()).map(Field::getName).toList());
- private static final DataComponentType.Valued LEGACY = resolve("SWING_ANIMATION");
- private static final DataComponentType.Valued ATTACK = resolve("ATTACK_ANIMATION");
- private static final DataComponentType.Valued INTERACT = resolve("INTERACT_ANIMATION");
+ private static volatile RuntimeTypes runtimeTypes;
private SwingAnimationResolver() {
}
@@ -30,6 +28,13 @@ private SwingAnimationResolver() {
public record ComponentNames(boolean legacy, boolean split) {
}
+ private record RuntimeTypes(
+ ComponentNames names,
+ DataComponentType.Valued legacy,
+ DataComponentType.Valued attack,
+ DataComponentType.Valued interact) {
+ }
+
/** Pure resolver used by unit tests with fake old/split field sets. */
public static ComponentNames classify(Iterable fieldNames) {
Set names = new LinkedHashSet<>();
@@ -40,53 +45,75 @@ public static ComponentNames classify(Iterable fieldNames) {
names.contains("ATTACK_ANIMATION") && names.contains("INTERACT_ANIMATION"));
}
+ /** Returns the detected layout; call this only while a Paper server is running. */
public static ComponentNames componentNames() {
- return NAMES;
+ return runtime().names();
}
public static void setUnified(ItemStack item, SwingAnimation animation) {
- if (NAMES.split() && ATTACK != null && INTERACT != null) {
- item.setData(ATTACK, animation);
- item.setData(INTERACT, animation);
- } else if (LEGACY != null) {
- item.setData(LEGACY, animation);
+ RuntimeTypes types = runtime();
+ if (types.names().split() && types.attack() != null && types.interact() != null) {
+ item.setData(types.attack(), animation);
+ item.setData(types.interact(), animation);
+ } else if (types.legacy() != null) {
+ item.setData(types.legacy(), animation);
}
}
public static void setAttack(ItemStack item, SwingAnimation animation) {
- if (NAMES.split() && ATTACK != null) {
- item.setData(ATTACK, animation);
- } else if (LEGACY != null) {
- item.setData(LEGACY, animation);
+ RuntimeTypes types = runtime();
+ if (types.names().split() && types.attack() != null) {
+ item.setData(types.attack(), animation);
+ } else if (types.legacy() != null) {
+ item.setData(types.legacy(), animation);
}
}
public static void setInteract(ItemStack item, SwingAnimation animation) {
- if (NAMES.split() && INTERACT != null) {
- item.setData(INTERACT, animation);
- } else if (LEGACY != null) {
- item.setData(LEGACY, animation);
+ RuntimeTypes types = runtime();
+ if (types.names().split() && types.interact() != null) {
+ item.setData(types.interact(), animation);
+ } else if (types.legacy() != null) {
+ item.setData(types.legacy(), animation);
}
}
public static SwingAnimation attack(ItemStack item) {
- if (NAMES.split() && ATTACK != null) {
- return value(item, ATTACK);
- }
- return value(item, LEGACY);
+ RuntimeTypes types = runtime();
+ return types.names().split() && types.attack() != null
+ ? value(item, types.attack()) : value(item, types.legacy());
}
public static SwingAnimation interact(ItemStack item) {
- if (NAMES.split() && INTERACT != null) {
- return value(item, INTERACT);
- }
- return value(item, LEGACY);
+ RuntimeTypes types = runtime();
+ return types.names().split() && types.interact() != null
+ ? value(item, types.interact()) : value(item, types.legacy());
}
private static SwingAnimation value(ItemStack item, DataComponentType.Valued type) {
return type != null && item.isDataOverridden(type) ? item.getData(type) : null;
}
+ private static RuntimeTypes runtime() {
+ RuntimeTypes result = runtimeTypes;
+ if (result == null) {
+ synchronized (SwingAnimationResolver.class) {
+ result = runtimeTypes;
+ if (result == null) {
+ Set names = new LinkedHashSet<>(Arrays.stream(DataComponentTypes.class.getFields())
+ .map(Field::getName).toList());
+ result = new RuntimeTypes(
+ classify(names),
+ resolve("SWING_ANIMATION"),
+ resolve("ATTACK_ANIMATION"),
+ resolve("INTERACT_ANIMATION"));
+ runtimeTypes = result;
+ }
+ }
+ }
+ return result;
+ }
+
@SuppressWarnings("unchecked")
private static DataComponentType.Valued resolve(String fieldName) {
try {
diff --git a/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java b/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java
index 15d3f703..2404f029 100644
--- a/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java
+++ b/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolverTest.java
@@ -16,9 +16,9 @@ public static void main(String[] args) {
Set.of("ATTACK_ANIMATION"));
require(!partial.split(), "one split field must not activate split mode");
- SwingAnimationResolver.ComponentNames runtime = SwingAnimationResolver.componentNames();
- require(runtime.legacy() || runtime.split(), "Paper runtime must expose an animation layout");
- System.out.println("SwingAnimationResolverTest passed: " + runtime);
+ // Runtime field access is intentionally not performed here: Paper's
+ // DataComponentTypes needs a live registry implementation outside a server.
+ System.out.println("SwingAnimationResolverTest passed: fake old/split layouts");
}
private static void require(boolean condition, String message) {
From 178ed6c1851f4b35d85d565ccd7f84e77eb05b87 Mon Sep 17 00:00:00 2001
From: Arbousier1
Date: Tue, 6 Oct 2026 23:30:00 +0800
Subject: [PATCH 7/7] test: add Paper adapter startup smoke
---
.github/workflows/build.yml | 1 +
paper/build.gradle.kts | 8 ++++++++
.../paper/utils/PaperAdapterStartupSmoke.java | 14 ++++++++++++++
3 files changed, 23 insertions(+)
create mode 100644 paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/PaperAdapterStartupSmoke.java
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 280ca1cf..34d79054 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -82,6 +82,7 @@ jobs:
- name: Build with Gradle
run: |
gradle :paper:testSwingAnimationResolver --no-daemon -PpaperVersion=${{ matrix.paper-version }}
+ gradle :paper:smokePaperAdapter --no-daemon -PpaperVersion=${{ matrix.paper-version }}
gradle :core:testMath --no-daemon
gradle :core:testDecay --no-daemon
gradle :core:testDecayExtended --no-daemon
diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts
index 6480074a..7ee87f4c 100644
--- a/paper/build.gradle.kts
+++ b/paper/build.gradle.kts
@@ -14,3 +14,11 @@ tasks.register("testSwingAnimationResolver") {
dependsOn(tasks.named("testClasses"))
classpath = sourceSets["test"].runtimeClasspath
}
+
+tasks.register("smokePaperAdapter") {
+ group = "verification"
+ description = "Smoke-load Paper item adapters without resolving removed animation fields"
+ mainClass.set("cn.superiormc.ultimateshop.paper.utils.PaperAdapterStartupSmoke")
+ dependsOn(tasks.named("testClasses"))
+ classpath = sourceSets["test"].runtimeClasspath
+}
diff --git a/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/PaperAdapterStartupSmoke.java b/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/PaperAdapterStartupSmoke.java
new file mode 100644
index 00000000..559e0e1b
--- /dev/null
+++ b/paper/src/test/java/cn/superiormc/ultimateshop/paper/utils/PaperAdapterStartupSmoke.java
@@ -0,0 +1,14 @@
+package cn.superiormc.ultimateshop.paper.utils;
+
+/**
+ * Class-link smoke: loading the Paper adapters must not resolve a removed
+ * 26.2/26.3 DataComponentTypes field during plugin startup.
+ */
+public final class PaperAdapterStartupSmoke {
+ public static void main(String[] args) throws Exception {
+ Class.forName("cn.superiormc.ultimateshop.paper.methods.BuildItemPaper");
+ Class.forName("cn.superiormc.ultimateshop.paper.methods.DebuildItemPaper");
+ Class.forName(SwingAnimationResolver.class.getName());
+ System.out.println("PaperAdapterStartupSmoke passed");
+ }
+}