Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
branches:
- 'feature/new-branch'
- 'chore/minecraft-26.3'
pull_request:
workflow_dispatch:

env:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.gradle/
build/
**/build/
*.iml
.idea/
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
3 changes: 3 additions & 0 deletions docs/format/itemformat-tm/component-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

21 changes: 20 additions & 1 deletion paper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<JavaExec>("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<JavaExec>("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
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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<DamageType> damageTypes) {
if (damageTypes instanceof Tag<?> tag) {
return tag.tagKey().key().asString();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>
*/
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<SwingAnimation> legacy,
DataComponentType.Valued<SwingAnimation> attack,
DataComponentType.Valued<SwingAnimation> interact) {
}

/** Pure resolver used by unit tests with fake old/split field sets. */
public static ComponentNames classify(Iterable<String> fieldNames) {
Set<String> 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<SwingAnimation> 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<String> 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<SwingAnimation> resolve(String fieldName) {
try {
Field field = DataComponentTypes.class.getField(fieldName);
return (DataComponentType.Valued<SwingAnimation>) field.get(null);
} catch (ReflectiveOperationException | ClassCastException ignored) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading
Loading