diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index dabc94e..19a7b60 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -12,6 +12,8 @@ object Versions { const val SPIGOT_API = "1.21.4-R0.1-SNAPSHOT" + const val PACKETEVENTS = "2.9.5" + const val JETBRAINS_ANNOTATIONS = "26.0.2-1" } diff --git a/buildSrc/src/main/kotlin/multification-repositories.gradle.kts b/buildSrc/src/main/kotlin/multification-repositories.gradle.kts index 3ee91b7..118ed87 100644 --- a/buildSrc/src/main/kotlin/multification-repositories.gradle.kts +++ b/buildSrc/src/main/kotlin/multification-repositories.gradle.kts @@ -5,10 +5,11 @@ plugins { repositories { mavenCentral() - maven("https://papermc.io/repo/repository/maven-public/") // paper, adventure, velocity maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") // spigot - maven("https://repo.panda-lang.org/releases/") // expressible - maven("https://repo.stellardrift.ca/repository/snapshots/") - maven("https://storehouse.okaeri.eu/repository/maven-public/") // okaeri configs maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // adventure snapshots + maven("https://storehouse.okaeri.eu/repository/maven-public/") // okaeri configs + maven("https://repo.stellardrift.ca/repository/snapshots/") // ? xd + maven("https://repo.codemc.io/repository/maven-releases/") // packetevents + maven("https://papermc.io/repo/repository/maven-public/") // paper, adventure, velocity + maven("https://repo.panda-lang.org/releases/") // expressible } \ No newline at end of file diff --git a/examples/bukkit/build.gradle.kts b/examples/bukkit/build.gradle.kts index dd0e387..3fb343f 100644 --- a/examples/bukkit/build.gradle.kts +++ b/examples/bukkit/build.gradle.kts @@ -11,6 +11,7 @@ repositories { mavenCentral() maven("https://repo.panda-lang.org/releases/") maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://repo.codemc.io/repository/maven-releases/") // packetevents maven("https://repo.stellardrift.ca/repository/snapshots/") } @@ -23,8 +24,10 @@ dependencies { // implementation("com.eternalcode:multification-bukkit:1.0.3") // <-- uncomment in your project // implementation("com.eternalcode:multification-cdn:1.0.3") // <-- uncomment in your project - implementation(project(":multification-bukkit")) // don't use this line in your build.gradle - implementation(project(":multification-cdn")) // don't use this line in your build.gradle + implementation(project(":multification-bukkit")) + implementation(project(":multification-cdn")) + + compileOnly("com.github.retrooper:packetevents-spigot:${Versions.PACKETEVENTS}") } val pluginName = "ExamplePlugin" @@ -56,5 +59,9 @@ sourceSets.test { } tasks.runServer { - minecraftVersion("1.21.4") + minecraftVersion("1.21.8") + + downloadPlugins { + downloadPlugins.url("https://cdn.modrinth.com/data/HYKaKraK/versions/Kee6pozk/packetevents-spigot-2.9.5.jar") + } } diff --git a/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/ExamplePlugin.java b/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/ExamplePlugin.java index 5939c19..bc74185 100644 --- a/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/ExamplePlugin.java +++ b/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/ExamplePlugin.java @@ -1,5 +1,6 @@ package com.eternalcode.example.bukkit; +import com.eternalcode.example.bukkit.command.AdvancementCommand; import com.eternalcode.example.bukkit.command.GiveCommand; import com.eternalcode.example.bukkit.command.ReloadCommand; import com.eternalcode.example.bukkit.command.TeleportCommand; @@ -41,6 +42,7 @@ public void onEnable() { new FlyCommand(multification), new GiveCommand(multification), new ReloadCommand(configurationManager, multification), + new AdvancementCommand(multification, this), new TimerCommand(new TimerManager(this.getServer().getScheduler(), this, multification)) ) .build(); diff --git a/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/command/AdvancementCommand.java b/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/command/AdvancementCommand.java new file mode 100644 index 0000000..e151f3b --- /dev/null +++ b/examples/bukkit/src/main/java/com/eternalcode/example/bukkit/command/AdvancementCommand.java @@ -0,0 +1,171 @@ +package com.eternalcode.example.bukkit.command; + +import com.eternalcode.example.bukkit.multification.YourMultification; +import com.eternalcode.multification.notice.Notice; +import com.eternalcode.multification.bukkit.notice.resolver.advancement.PacketEventsNotice; +import com.eternalcode.multification.bukkit.notice.resolver.advancement.AdvancementFrameType; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.permission.Permission; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.time.Duration; + +@Command(name = "testadvancements") +@Permission("example.testadvancements") +public class AdvancementCommand { + + private final YourMultification multification; + private final Plugin plugin; + + public AdvancementCommand(YourMultification multification, Plugin plugin) { + this.multification = multification; + this.plugin = plugin; + } + + @Execute(name = "simple") + void executeSimple(@Context Player player) { + // Using simple helper method + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.advancement( + "Simple Achievement", + "You triggered a test advancement!" + )) + .send(); + } + + @Execute(name = "withicon") + void executeWithIcon(@Context Player player) { + // Using helper method with icon + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.advancement( + "Achievement Unlocked", + "You found a diamond!", + "DIAMOND" + )) + .send(); + } + + @Execute(name = "challenge") + void executeChallenge(@Context Player player) { + // Using helper method with frame type + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.advancement( + "Epic Challenge", + "Defeat the Ender Dragon", + "DRAGON_HEAD", + AdvancementFrameType.CHALLENGE + )) + .send(); + } + + @Execute(name = "goal") + void executeGoal(@Context Player player) { + // Using builder for more customization + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.builder() + .title("Reach the Goal") + .description("Plant 100 trees") + .icon("OAK_SAPLING") + .frameType(AdvancementFrameType.GOAL) + .background("minecraft:textures/gui/advancements/backgrounds/stone.png") + .buildAdvancement()) + .send(); + } + + @Execute(name = "timed") + void executeTimed(@Context Player player) { + // Using builder with show time + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.builder() + .title("Quick Message") + .description("This will disappear in 3 seconds") + .icon("CLOCK") + .frameType(AdvancementFrameType.TASK) + .showTime(Duration.ofSeconds(3)) + .buildAdvancement()) + .send(); + } + + @Execute(name = "custom") + void executeCustom(@Context Player player) { + // Using builder with full customization + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.builder() + .title("Custom Toast") + .description("With all options configured") + .icon("GOLD_INGOT") + .frameType(AdvancementFrameType.TASK) + .background("minecraft:textures/gui/advancements/backgrounds/adventure.png") + .position(0.0f, 0.0f) + .showTime(Duration.ofSeconds(5)) + .showToast(true) + .hidden(true) + .buildAdvancement()) + .send(); + } + + @Execute(name = "positioned") + void executePositioned(@Context Player player) { + // Custom position example + this.multification.create() + .viewer(player) + .notice(PacketEventsNotice.builder() + .title("Centered Message") + .description("This appears in the center") + .icon("COMPASS") + .position(0.5f, 0.5f) + .buildAdvancement()) + .send(); + } + + @Execute(name = "translated") + void executeTranslated(@Context Player player) { + // Chat message with placeholder + this.multification.create() + .viewer(player) + .notice(Notice.chat("Hello, {player}! This is a translated message.")) + .placeholder("{player}", player.getName()) + .send(); + } + + @Execute(name = "all") + void executeAll(@Context Player player) { + // Show simple first + executeSimple(player); + + // Schedule more advancements + Bukkit.getScheduler().runTaskLater( + this.plugin, + () -> executeWithIcon(player), + 40L // 2 seconds + ); + + Bukkit.getScheduler().runTaskLater( + this.plugin, + () -> executeChallenge(player), + 80L // 4 seconds + ); + + Bukkit.getScheduler().runTaskLater( + this.plugin, + () -> executeGoal(player), + 120L // 6 seconds + ); + + Bukkit.getScheduler().runTaskLater( + this.plugin, + () -> executeCustom(player), + 160L // 8 seconds + ); + } +} \ No newline at end of file diff --git a/multification-bukkit/build.gradle.kts b/multification-bukkit/build.gradle.kts index 9b0e0d5..b8cfddb 100644 --- a/multification-bukkit/build.gradle.kts +++ b/multification-bukkit/build.gradle.kts @@ -10,4 +10,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") + + compileOnly("com.github.retrooper:packetevents-spigot:${Versions.PACKETEVENTS}") + testImplementation("com.github.retrooper:packetevents-spigot:${Versions.PACKETEVENTS}") } \ No newline at end of file diff --git a/multification-bukkit/src/com/eternalcode/multification/bukkit/BukkitMultification.java b/multification-bukkit/src/com/eternalcode/multification/bukkit/BukkitMultification.java index 90f6568..1e2c328 100644 --- a/multification-bukkit/src/com/eternalcode/multification/bukkit/BukkitMultification.java +++ b/multification-bukkit/src/com/eternalcode/multification/bukkit/BukkitMultification.java @@ -1,7 +1,9 @@ package com.eternalcode.multification.bukkit; import com.eternalcode.multification.Multification; +import com.eternalcode.multification.bukkit.notice.resolver.advancement.AdvancementResolver; import com.eternalcode.multification.bukkit.notice.resolver.sound.SoundBukkitResolver; +import com.eternalcode.multification.bukkit.util.PacketEventsUtil; import com.eternalcode.multification.executor.AsyncExecutor; import com.eternalcode.multification.locate.LocaleProvider; import com.eternalcode.multification.viewer.ViewerProvider; @@ -17,7 +19,13 @@ public abstract class BukkitMultification extends Multification contents() { + return List.of(this.title, this.description); + } + + public String iconOrDefault() { + return this.icon != null ? this.icon : DEFAULT_ICON; + } + + public AdvancementFrameType frameTypeOrDefault() { + return this.frameType != null ? this.frameType : DEFAULT_FRAME; + } + + public Duration showTimeOrDefault() { + return this.showTime != null ? this.showTime : DEFAULT_SHOW_TIME; + } +} \ No newline at end of file diff --git a/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/AdvancementFrameType.java b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/AdvancementFrameType.java new file mode 100644 index 0000000..cff4eff --- /dev/null +++ b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/AdvancementFrameType.java @@ -0,0 +1,15 @@ +package com.eternalcode.multification.bukkit.notice.resolver.advancement; + +import com.github.retrooper.packetevents.protocol.advancements.AdvancementType; + +public enum AdvancementFrameType { + + TASK, + CHALLENGE, + GOAL; + + public AdvancementType toPacketEventsType() { + return AdvancementType.valueOf(this.name()); + } +} + diff --git a/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/AdvancementResolver.java b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/AdvancementResolver.java new file mode 100644 index 0000000..d0647a5 --- /dev/null +++ b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/AdvancementResolver.java @@ -0,0 +1,244 @@ +package com.eternalcode.multification.bukkit.notice.resolver.advancement; + +import com.eternalcode.multification.notice.NoticeKey; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult; +import com.eternalcode.multification.notice.resolver.text.TextContentResolver; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.advancements.Advancement; +import com.github.retrooper.packetevents.protocol.advancements.AdvancementDisplay; +import com.github.retrooper.packetevents.protocol.advancements.AdvancementHolder; +import com.github.retrooper.packetevents.protocol.advancements.AdvancementProgress; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.item.type.ItemType; +import com.github.retrooper.packetevents.protocol.item.type.ItemTypes; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.resources.ResourceLocation; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAdvancements; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.ComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.UnaryOperator; + +public class AdvancementResolver implements TextContentResolver { + + private static final String FORMAT = "%s|%s|%s|%s|%s|%b|%b|%f|%f|%s"; + + private final Plugin plugin = JavaPlugin.getProvidingPlugin(AdvancementResolver.class); + + @Override + public NoticeKey noticeKey() { + return PacketEventsNoticeKey.ADVANCEMENT; + } + + @Override + public void send(Audience audience, ComponentSerializer serializer, AdvancementContent content) { + // TODO: handle player from this audience shit + Player player = null; + + User user = PacketEvents.getAPI().getPlayerManager().getUser(player); + + if (user == null) { + System.out.println("PacketEvents user is null for player: " + audience); + return; + } + String advancementKeyString = "toast_" + UUID.randomUUID().toString().replace("-", ""); + Key advancementKey = Key.key(this.plugin.getName().toLowerCase(), advancementKeyString); + + Component titleComponent = serializer.deserialize(content.title()); + Component descComponent = serializer.deserialize(content.description()); + + ItemStack icon = this.createIcon(content.iconOrDefault()); + ResourceLocation background = this.parseBackground(content.background()); + + AdvancementDisplay display = new AdvancementDisplay( + titleComponent, + descComponent, + icon, + content.frameTypeOrDefault().toPacketEventsType(), + background, + content.showToast(), + content.hidden(), + content.x(), + content.y() + ); + + Advancement advancement = new Advancement( + background, + display, + Collections.emptyList(), + false + ); + + List holders = new ArrayList<>(); + ResourceLocation advancementLocation = new ResourceLocation(advancementKey.namespace(), advancementKey.value()); + holders.add(new AdvancementHolder(advancementLocation, advancement)); + + WrapperPlayServerUpdateAdvancements addPacket = new WrapperPlayServerUpdateAdvancements( + false, + holders, + Collections.emptySet(), + Collections.emptyMap(), + true + ); + user.sendPacket(addPacket); + + Map progressMap = new HashMap<>(); + progressMap.put(advancementLocation, new AdvancementProgress(new HashMap<>())); + + WrapperPlayServerUpdateAdvancements grantPacket = new WrapperPlayServerUpdateAdvancements( + false, + Collections.emptyList(), + Collections.emptySet(), + progressMap, + false + ); + user.sendPacket(grantPacket); + + System.out.println(content); + + long delayTicks = content.showTimeOrDefault().toMillis() / 50; + + Bukkit.getScheduler().runTaskLater( + this.plugin, + () -> { + Set removeKeys = new HashSet<>(); + removeKeys.add(new ResourceLocation(advancementKey.namespace(), advancementKey.value())); + + WrapperPlayServerUpdateAdvancements removePacket = new WrapperPlayServerUpdateAdvancements( + false, + Collections.emptyList(), + removeKeys, + Collections.emptyMap(), + false + ); + user.sendPacket(removePacket); + }, + delayTicks + ); + } + + @Override + public NoticeSerdesResult serialize(AdvancementContent content) { + return new NoticeSerdesResult.Single(String.format(FORMAT, + content.title(), + content.description(), + content.iconOrDefault(), + content.frameTypeOrDefault().name(), + content.background() != null ? content.background() : "", + content.showToast(), + content.hidden(), + content.x(), + content.y(), + content.showTimeOrDefault().toMillis() + )); + } + + @Override + public Optional deserialize(NoticeSerdesResult result) { + return result.firstElement().map(value -> { + String[] parts = value.split("\\|"); + + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid advancement format: " + value); + } + + String title = parts[0]; + String description = parts[1]; + String icon = parts.length > 2 && !parts[2].isEmpty() ? parts[2] : null; + AdvancementFrameType frameType = parts.length > 3 && !parts[3].isEmpty() + ? AdvancementFrameType.valueOf(parts[3]) + : null; + String background = parts.length > 4 && !parts[4].isEmpty() ? parts[4] : null; + boolean showToast = parts.length > 5 ? Boolean.parseBoolean(parts[5]) : AdvancementContent.DEFAULT_SHOW_TOAST; + boolean hidden = parts.length > 6 ? Boolean.parseBoolean(parts[6]) : AdvancementContent.DEFAULT_HIDDEN; + float x = parts.length > 7 ? Float.parseFloat(parts[7]) : AdvancementContent.DEFAULT_X; + float y = parts.length > 8 ? Float.parseFloat(parts[8]) : AdvancementContent.DEFAULT_Y; + Duration showTime = parts.length > 9 && !parts[9].isEmpty() + ? Duration.ofMillis(Long.parseLong(parts[9])) + : null; + + return new AdvancementContent(title, description, icon, frameType, background, showToast, hidden, x, y, showTime); + }); + } + + @Override + public AdvancementContent createFromText(List contents) { + if (contents.isEmpty()) { + return new AdvancementContent("", "", null, null); + } + + if (contents.size() == 1) { + return new AdvancementContent(contents.get(0), "", null, null); + } + + return new AdvancementContent(contents.get(0), contents.get(1), null, null); + } + + @Override + public AdvancementContent applyText(AdvancementContent content, UnaryOperator function) { + return new AdvancementContent( + function.apply(content.title()), + function.apply(content.description()), + content.icon(), + content.frameType(), + content.background(), + content.showToast(), + content.hidden(), + content.x(), + content.y(), + content.showTime() + ); + } + + private ItemStack createIcon(@NotNull String materialName) { + try { + ItemType materialType = ItemTypes.getByName(materialName); + + if (materialType == null) { + throw new IllegalArgumentException("Invalid material: " + materialName); + } + + return ItemStack.builder() + .type(materialType) + .build(); + } + catch (IllegalArgumentException e) { + return ItemStack.builder() + .type(ItemTypes.GRASS_BLOCK) + .amount(1) + .build(); + } + } + + private ResourceLocation parseBackground(@Nullable String backgroundString) { + if (backgroundString == null || backgroundString.isEmpty()) { + return null; + } + + String[] parts = backgroundString.split(":", 2); + + if (parts.length != 2) { + return new ResourceLocation("minecraft", backgroundString); + } + + return new ResourceLocation(parts[0], parts[1]); + } +} \ No newline at end of file diff --git a/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/PacketEventsNotice.java b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/PacketEventsNotice.java new file mode 100644 index 0000000..5790d18 --- /dev/null +++ b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/PacketEventsNotice.java @@ -0,0 +1,225 @@ +package com.eternalcode.multification.bukkit.notice.resolver.advancement; + +import com.eternalcode.multification.notice.Notice; +import java.time.Duration; + +public class PacketEventsNotice { + + /** + * Creates a simple advancement with title and description + * + * @param title Advancement title + * @param description Advancement description + * @return Notice instance + */ + public static Notice advancement(String title, String description) { + return builder() + .title(title) + .description(description) + .buildAdvancement(); + } + + /** + * Creates an advancement with title, description and icon + * + * @param title Advancement title + * @param description Advancement description + * @param icon Icon material/item name + * @return Notice instance + */ + public static Notice advancement(String title, String description, String icon) { + return builder() + .title(title) + .description(description) + .icon(icon) + .buildAdvancement(); + } + + /** + * Creates an advancement with title, description, icon and frame type + * + * @param title Advancement title + * @param description Advancement description + * @param icon Icon material/item name + * @param frameType Frame type + * @return Notice instance + */ + public static Notice advancement(String title, String description, String icon, AdvancementFrameType frameType) { + return builder() + .title(title) + .description(description) + .icon(icon) + .frameType(frameType) + .buildAdvancement(); + } + + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Notice.BaseBuilder { + + private String title; + private String description; + private String icon; + private AdvancementFrameType frameType; + private String background; + private boolean showToast = true; + private boolean hidden = false; + private float x = 0.0f; + private float y = 0.0f; + private Duration showTime; + + /** + * Sets the title of the advancement + * + * @param title Advancement title + * @return Builder instance + */ + public Builder title(String title) { + this.title = title; + return this; + } + + /** + * Sets the description of the advancement + * + * @param description Advancement description + * @return Builder instance + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the icon for the advancement + * + * @param icon Icon material/item name + * @return Builder instance + */ + public Builder icon(String icon) { + this.icon = icon; + return this; + } + + /** + * Sets the frame type of the advancement + * + * @param frameType Type of advancement frame + * @return Builder instance + */ + public Builder frameType(AdvancementFrameType frameType) { + this.frameType = frameType; + return this; + } + + /** + * Sets the background texture + * + * @param background Background texture path + * @return Builder instance + */ + public Builder background(String background) { + this.background = background; + return this; + } + + /** + * Sets whether to show the toast notification + * + * @param showToast Show toast notification + * @return Builder instance + */ + public Builder showToast(boolean showToast) { + this.showToast = showToast; + return this; + } + + /** + * Sets whether the advancement is hidden + * + * @param hidden Hidden status + * @return Builder instance + */ + public Builder hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + /** + * Sets the X position (0.0 = left, 1.0 = right) + * + * @param x X coordinate + * @return Builder instance + */ + public Builder x(float x) { + this.x = x; + return this; + } + + /** + * Sets the Y position (0.0 = top, 1.0 = bottom) + * + * @param y Y coordinate + * @return Builder instance + */ + public Builder y(float y) { + this.y = y; + return this; + } + + /** + * Sets the position of the advancement + * + * @param x X coordinate (0.0 = left, 1.0 = right) + * @param y Y coordinate (0.0 = top, 1.0 = bottom) + * @return Builder instance + */ + public Builder position(float x, float y) { + this.x = x; + this.y = y; + return this; + } + + /** + * Sets how long the advancement should be shown + * + * @param showTime Duration to show the advancement + * @return Builder instance + */ + public Builder showTime(Duration showTime) { + this.showTime = showTime; + return this; + } + + /** + * Builds the advancement notice + * + * @return Notice with advancement content + */ + public Notice buildAdvancement() { + return this.withPart( + PacketEventsNoticeKey.ADVANCEMENT, + new AdvancementContent( + title, + description, + icon, + frameType, + background, + showToast, + hidden, + x, + y, + showTime + ) + ).build(); + } + + @Override + protected Builder getThis() { + return this; + } + } +} \ No newline at end of file diff --git a/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/PacketEventsNoticeKey.java b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/PacketEventsNoticeKey.java new file mode 100644 index 0000000..41a1216 --- /dev/null +++ b/multification-bukkit/src/com/eternalcode/multification/bukkit/notice/resolver/advancement/PacketEventsNoticeKey.java @@ -0,0 +1,9 @@ +package com.eternalcode.multification.bukkit.notice.resolver.advancement; + +import com.eternalcode.multification.notice.NoticeKey; +import com.eternalcode.multification.notice.resolver.NoticeContent; + +public interface PacketEventsNoticeKey extends NoticeKey { + + NoticeKey ADVANCEMENT = NoticeKey.of("advancement", AdvancementContent.class); +} diff --git a/multification-bukkit/src/com/eternalcode/multification/bukkit/util/PacketEventsUtil.java b/multification-bukkit/src/com/eternalcode/multification/bukkit/util/PacketEventsUtil.java new file mode 100644 index 0000000..fad8ec4 --- /dev/null +++ b/multification-bukkit/src/com/eternalcode/multification/bukkit/util/PacketEventsUtil.java @@ -0,0 +1,42 @@ +package com.eternalcode.multification.bukkit.util; + +public class PacketEventsUtil { + + private static Boolean packetEventsLoaded = null; + + private static final String[] PACKETEVENTS_CLASSES = { + "com.github.retrooper.packetevents.PacketEvents", + "io.github.retrooper.packetevents.PacketEvents", + "com.github.retrooper.packetevents.PacketEventsAPI", + "io.github.retrooper.packetevents.PacketEventsAPI" + }; + + /** + * Checks if PacketEvents is loaded via Class.forName() + * Handles different relocations + * + * @return true if PacketEvents is available, false otherwise + */ + public static boolean isPacketEventsLoaded() { + if (packetEventsLoaded == null) { + packetEventsLoaded = checkPacketEventsClass(); + } + return packetEventsLoaded; + } + + /** + * Checks if any PacketEvents class exists + * + * @return true if PacketEvents class was found + */ + private static boolean checkPacketEventsClass() { + for (String className : PACKETEVENTS_CLASSES) { + try { + Class.forName(className); + return true; + } + catch (ClassNotFoundException ignored) {} + } + return false; + } +} diff --git a/multification-core/build.gradle.kts b/multification-core/build.gradle.kts index a4a20f7..98b6196 100644 --- a/multification-core/build.gradle.kts +++ b/multification-core/build.gradle.kts @@ -8,6 +8,7 @@ plugins { dependencies { compileOnlyApi("net.kyori:adventure-api:${Versions.ADVENTURE_API}") + compileOnlyApi("net.kyori:adventure-platform-bukkit:${Versions.ADVENTURE_PLATFORM_BUKKIT}") api("org.jetbrains:annotations:${Versions.JETBRAINS_ANNOTATIONS}") testImplementation("net.kyori:adventure-api:${Versions.ADVENTURE_API}") diff --git a/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolver.java b/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolver.java index 1e9c820..bbed4d7 100644 --- a/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolver.java +++ b/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolver.java @@ -2,6 +2,7 @@ import com.eternalcode.multification.notice.NoticeKey; import java.util.Optional; +import java.util.UUID; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.ComponentSerializer; diff --git a/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolverRegistry.java b/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolverRegistry.java index cfb9ac0..be1fcb9 100644 --- a/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolverRegistry.java +++ b/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeResolverRegistry.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.function.UnaryOperator; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; diff --git a/multification-okaeri/src/com/eternalcode/multification/okaeri/AdvancementResolver.java b/multification-okaeri/src/com/eternalcode/multification/okaeri/AdvancementResolver.java new file mode 100644 index 0000000..9702d6b --- /dev/null +++ b/multification-okaeri/src/com/eternalcode/multification/okaeri/AdvancementResolver.java @@ -0,0 +1,217 @@ +package com.eternalcode.multification.okaeri; + +import com.eternalcode.multification.notice.NoticeKey; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult; +import com.eternalcode.multification.notice.resolver.text.TextContentResolver; +import com.eternalcode.multification.packetevents.notice.PacketEventsNoticeKey; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.advancements.*; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.item.type.ItemType; +import com.github.retrooper.packetevents.protocol.item.type.ItemTypes; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerAdvancementsUpdate; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.ComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.UnaryOperator; + +public class AdvancementResolver implements TextContentResolver { + + private static final String FORMAT = "%s|%s|%s|%s"; + private static final long CLEANUP_DELAY_TICKS = 20L; + + private final NoticeKey key; + private final Plugin plugin; + + public AdvancementResolver(Plugin plugin) { + this.key = PacketEventsNoticeKey.ADVANCEMENT; + this.plugin = plugin; + } + + @Override + public NoticeKey noticeKey() { + return this.key; + } + + @Override + public void send(Audience audience, ComponentSerializer serializer, AdvancementContent content) { + if (!(audience instanceof Player player)) { + return; + } + + User user = PacketEvents.getAPI().getPlayerManager().getUser(player); + if (user == null) { + return; + } + + // Generate unique advancement key for this toast + String advancementKey = "minecraft:toast_" + UUID.randomUUID().toString().replace("-", ""); + + Component titleComponent = serializer.deserialize(content.title()); + Component descComponent = serializer.deserialize(content.description()); + + ItemStack icon = this.createIcon(content.iconOrDefault()); + + // Create advancement display with toast flag + AdvancementDisplay display = new AdvancementDisplay( + titleComponent, + descComponent, + icon, + this.toFrameType(content.frameTypeOrDefault()), + true, // showToast + false, // announceToChat + true // hidden + ); + + // Create advancement with display + Advancement advancement = new Advancement( + null, // parent + display, + Collections.emptyList(), // criteria + Collections.emptyList() // requirements + ); + + Map advancements = new HashMap<>(); + advancements.put(advancementKey, advancement); + + // Packet 1: Add advancement to client + WrapperPlayServerAdvancementsUpdate addPacket = new WrapperPlayServerAdvancementsUpdate( + false, // reset + advancements, + Collections.emptyList(), // identifiersToRemove + Collections.emptyMap() // progressMap + ); + user.sendPacket(addPacket); + + // Packet 2: Grant criteria to trigger toast display + Map progressMap = new HashMap<>(); + Map criteria = new HashMap<>(); + criteria.put("trigger", System.currentTimeMillis()); + + progressMap.put(advancementKey, new AdvancementProgress(criteria)); + + WrapperPlayServerAdvancementsUpdate grantPacket = new WrapperPlayServerAdvancementsUpdate( + false, + Collections.emptyMap(), + Collections.emptyList(), + progressMap + ); + user.sendPacket(grantPacket); + + // Packet 3: Remove advancement after delay (cleanup) + Bukkit.getScheduler().runTaskLater( + this.plugin, + () -> { + WrapperPlayServerAdvancementsUpdate removePacket = new WrapperPlayServerAdvancementsUpdate( + false, + Collections.emptyMap(), + Collections.singletonList(advancementKey), + Collections.emptyMap() + ); + user.sendPacket(removePacket); + }, + CLEANUP_DELAY_TICKS + ); + } + + @Override + public NoticeSerdesResult serialize(AdvancementContent content) { + return new NoticeSerdesResult.Single(String.format(FORMAT, + content.title(), + content.description(), + content.iconOrDefault(), + content.frameTypeOrDefault().name() + )); + } + + @Override + public Optional deserialize(NoticeSerdesResult result) { + return result.firstElement().map(value -> { + String[] parts = value.split("\\|"); + + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid advancement format: " + value); + } + + String title = parts[0]; + String description = parts[1]; + String icon = parts.length > 2 ? parts[2] : null; + AdvancementFrameType frameType = parts.length > 3 + ? AdvancementFrameType.valueOf(parts[3]) + : null; + + return new AdvancementContent(title, description, icon, frameType); + }); + } + + @Override + public AdvancementContent createFromText(List contents) { + if (contents.isEmpty()) { + return new AdvancementContent("", "", null, null); + } + + if (contents.size() == 1) { + return new AdvancementContent(contents.get(0), "", null, null); + } + + return new AdvancementContent(contents.get(0), contents.get(1), null, null); + } + + @Override + public AdvancementContent applyText(AdvancementContent content, UnaryOperator function) { + return new AdvancementContent( + function.apply(content.title()), + function.apply(content.description()), + content.icon(), + content.frameType() + ); + } + + /** + * Creates an ItemStack icon from material name string. + * Falls back to GRASS_BLOCK if material is invalid. + * + * @param materialName the material name (e.g., "OAK_SAPLING", "DIAMOND") + * @return ItemStack with the specified material type + */ + private ItemStack createIcon(@NotNull String materialName) { + try { + ItemType materialType = ItemTypes.getByName(materialName); + + if (materialType == null) { + throw new IllegalArgumentException("Invalid material: " + materialName); + } + + return ItemStack.builder() + .type(materialType) + .build(); + } + catch (IllegalArgumentException e) { + return ItemStack.builder() + .type(ItemTypes.GRASS_BLOCK) + .amount(1) + .build(); + } + } + + /** + * Converts AdvancementFrameType enum to PacketEvents FrameType. + * + * @param frameType the frame type enum + * @return PacketEvents FrameType + */ + private FrameType toFrameType(AdvancementFrameType frameType) { + return switch (frameType) { + case TASK -> FrameType.TASK; // Yellow frame (normal achievement) + case CHALLENGE -> FrameType.CHALLENGE; // Purple frame (challenge) + case GOAL -> FrameType.GOAL; // Rounded frame (goal) + }; + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f85e277..4a3d69f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,4 @@ include("multification-cdn") include("multification-okaeri") include("multification-bukkit") -include("examples:bukkit") +include("examples:bukkit") \ No newline at end of file