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"); + } +}