diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 692a6449..34d79054 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: @@ -12,9 +14,13 @@ 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: write + contents: read steps: - name: Checkout repository @@ -75,11 +81,14 @@ 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 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 @@ -90,6 +99,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 @@ -98,9 +109,9 @@ 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 !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/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..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.2.build.55-alpha") + 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/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. + diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts index e43c1c18..7ee87f4c 100644 --- a/paper/build.gradle.kts +++ b/paper/build.gradle.kts @@ -1,5 +1,24 @@ +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.2.build.55-alpha") + 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 +} + +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/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java b/paper/src/main/java/cn/superiormc/ultimateshop/paper/methods/BuildItemPaper.java index 4700403e..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; @@ -880,19 +881,21 @@ 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. + SwingAnimationResolver.setUnified(item, animation); + } else { + ConfigurationSection attackAnimationKey = section.getConfigurationSection("attack-animation"); + if (attackAnimationKey != null) { + SwingAnimationResolver.setAttack(item, buildSwingAnimation(attackAnimationKey)); } - int duration = swingAnimationKey.getInt("duration", -1); - if (duration > 0) { - builder.duration(duration); + ConfigurationSection interactAnimationKey = section.getConfigurationSection("interact-animation"); + if (interactAnimationKey != null) { + SwingAnimationResolver.setInteract(item, 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..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; @@ -639,13 +640,19 @@ 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 = SwingAnimationResolver.attack(item); + SwingAnimation interactAnimation = SwingAnimationResolver.interact(item); + 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 +685,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/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..f74a8269 --- /dev/null +++ b/paper/src/main/java/cn/superiormc/ultimateshop/paper/utils/SwingAnimationResolver.java @@ -0,0 +1,126 @@ +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. + * 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 volatile RuntimeTypes runtimeTypes; + + 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<>(); + for (String name : fieldNames) { + names.add(name); + } + return new ComponentNames(names.contains("SWING_ANIMATION"), + 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 runtime().names(); + } + + public static void setUnified(ItemStack item, SwingAnimation 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) { + 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) { + 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) { + RuntimeTypes types = runtime(); + return types.names().split() && types.attack() != null + ? value(item, types.attack()) : value(item, types.legacy()); + } + + public static SwingAnimation interact(ItemStack item) { + 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 { + 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/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"); + } +} 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..2404f029 --- /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"); + + // 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) { + if (!condition) { + throw new AssertionError(message); + } + } +} diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 878ffdc2..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.2.build.55-alpha") + compileOnly("io.papermc.paper:paper-api:$paperVersion") } tasks.shadowJar {