From e3ee41e09cb2eaf0cdb474a8510c13dbfae2570d Mon Sep 17 00:00:00 2001 From: Phillipp Glanz <6745190+TheMeinerLP@users.noreply.github.com> Date: Sun, 26 May 2024 20:15:06 +0200 Subject: [PATCH] [#34] Implement new notification system with tests --- .../server/notifications/BuilderImpl.java | 43 +++++++ .../server/notifications/Notification.java | 112 ++++++++++++++++++ .../notifications/NotificationImpl.java | 44 +++++++ .../NotificationIntegrationTest.java | 58 +++++++++ 4 files changed, 257 insertions(+) create mode 100644 src/main/java/net/minestom/server/notifications/BuilderImpl.java create mode 100644 src/main/java/net/minestom/server/notifications/Notification.java create mode 100644 src/main/java/net/minestom/server/notifications/NotificationImpl.java create mode 100644 src/test/java/net/minestom/server/notifications/NotificationIntegrationTest.java diff --git a/src/main/java/net/minestom/server/notifications/BuilderImpl.java b/src/main/java/net/minestom/server/notifications/BuilderImpl.java new file mode 100644 index 00000000000..b23c8885806 --- /dev/null +++ b/src/main/java/net/minestom/server/notifications/BuilderImpl.java @@ -0,0 +1,43 @@ +package net.minestom.server.notifications; + +import net.kyori.adventure.text.Component; +import net.minestom.server.advancements.FrameType; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +final class BuilderImpl implements Notification.Builder { + private Component title; + private FrameType type; + private ItemStack icon; + + + @Override + public Notification.Builder title(@NotNull Component component) { + this.title = component; + return this; + } + + @Override + public Notification.Builder frameType(@NotNull FrameType frameType) { + this.type = frameType; + return this; + } + + @Override + public Notification.Builder icon(@NotNull Material material) { + this.icon = ItemStack.of(material); + return this; + } + + @Override + public Notification.Builder icon(@NotNull ItemStack itemStack) { + this.icon = itemStack; + return this; + } + + @Override + public Notification build() { + return new NotificationImpl(title, type, icon); + } +} diff --git a/src/main/java/net/minestom/server/notifications/Notification.java b/src/main/java/net/minestom/server/notifications/Notification.java new file mode 100644 index 00000000000..bf4b8efc621 --- /dev/null +++ b/src/main/java/net/minestom/server/notifications/Notification.java @@ -0,0 +1,112 @@ +package net.minestom.server.notifications; + +import net.kyori.adventure.text.Component; +import net.minestom.server.advancements.FrameType; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.packet.server.play.AdvancementsPacket; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; + +/** + * Is used to send temporary advancements to the client, which are called notifications. + *
+ * Here is an example of its use: + *

+ * Notification notification = Notification.builder()
+ *  .frameType(FrameType.TASK)
+ *  .title(Component.text("Welcome!"))
+ *  .icon(Material.IRON_SWORD).build();
+ * notification.send(player);
+ * 
+ */ +public sealed interface Notification permits NotificationImpl { + + String IDENTIFIER = "minestom:notification"; + AdvancementsPacket REMOVE_PACKET = new AdvancementsPacket(false, List.of(), List.of(IDENTIFIER), List.of()); + + /** + * Creates a new builder instance + * @return + */ + @Contract(pure = true) + static @NotNull Builder builder() { + return new BuilderImpl(); + } + + /** + * Send the notification to the client + * @param player to get be sent + */ + void send(@NotNull Player player); + + /** + * Send the notification to a collection of clients + * @param players to get be sent + */ + void send(@NotNull Collection<@NotNull Player> players); + + + /** + * Gets the title of the notification as a {@link Component} + * @return the title {@link Component} + */ + @NotNull Component title(); + + /** + * Get the {@link FrameType} of the notification + * @return the type + */ + @NotNull FrameType type(); + + /** + * Get the displayed icon of the notification as {@link ItemStack} + * @return the {@link ItemStack} + */ + @NotNull ItemStack icon(); + + sealed interface Builder permits BuilderImpl { + /** + * Set the title for a notification as component. + * + * If you're using a resource pack you can use {@link Component#translatable(String)} + * + * @param component to get send to the client + * @return the builder + */ + Builder title(@NotNull Component component); + + /** + * Set the frame typ of the notification + * @param frameType to showed for the client + * @return the builder + */ + Builder frameType(@NotNull FrameType frameType); + + /** + * Set the {@link Material} for the icon + * @param material to be shown to the client + * @return the builder + */ + Builder icon(@NotNull Material material); + + /** + * Set the {@link ItemStack} for the icon + * @param itemStack to be shown to the client + * @return the builder + */ + Builder icon(@NotNull ItemStack itemStack); + + /** + * Returns an instance of the creation notification + * @return the instance + */ + Notification build(); + } + + +} diff --git a/src/main/java/net/minestom/server/notifications/NotificationImpl.java b/src/main/java/net/minestom/server/notifications/NotificationImpl.java new file mode 100644 index 00000000000..2d9ddef8eea --- /dev/null +++ b/src/main/java/net/minestom/server/notifications/NotificationImpl.java @@ -0,0 +1,44 @@ +package net.minestom.server.notifications; + +import net.kyori.adventure.text.Component; +import net.minestom.server.advancements.FrameType; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; +import net.minestom.server.network.packet.server.play.AdvancementsPacket; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; + +record NotificationImpl(@NotNull Component title, @NotNull FrameType type, + @NotNull ItemStack icon) implements Notification { + @Override + public void send(@NotNull Player player) { + player.sendPacket(createPacket()); + player.sendPacket(REMOVE_PACKET); + } + + @Override + public void send(@NotNull Collection<@NotNull Player> players) { + players.forEach(this::send); + } + + AdvancementsPacket createPacket() { + final var displayData = new AdvancementsPacket.DisplayData( + title(), Component.empty(), + icon(), type(), + 0x6, null, 0f, 0f); + + final var criteria = new AdvancementsPacket.Criteria("minestom:some_criteria", + new AdvancementsPacket.CriterionProgress(System.currentTimeMillis())); + + final var advancement = new AdvancementsPacket.Advancement(null, displayData, + List.of(new AdvancementsPacket.Requirement(List.of(criteria.criterionIdentifier()))), + false); + + final var mapping = new AdvancementsPacket.AdvancementMapping(IDENTIFIER, advancement); + final var progressMapping = new AdvancementsPacket.ProgressMapping(IDENTIFIER, + new AdvancementsPacket.AdvancementProgress(List.of(criteria))); + return new AdvancementsPacket(false, List.of(mapping), List.of(), List.of(progressMapping)); + } +} diff --git a/src/test/java/net/minestom/server/notifications/NotificationIntegrationTest.java b/src/test/java/net/minestom/server/notifications/NotificationIntegrationTest.java new file mode 100644 index 00000000000..01447aac5de --- /dev/null +++ b/src/test/java/net/minestom/server/notifications/NotificationIntegrationTest.java @@ -0,0 +1,58 @@ +package net.minestom.server.notifications; + +import net.kyori.adventure.text.Component; +import net.minestom.server.advancements.FrameType; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.packet.server.play.AdvancementsPacket; +import net.minestom.testing.Collector; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@EnvTest +class NotificationIntegrationTest { + + @Test + void testBuilder(Env env) { + var notification = Notification.builder() + .icon(Material.ITEM_FRAME) + .title(Component.text("unit test")) + .frameType(FrameType.TASK) + .build(); + assertEquals(notification.type(), FrameType.TASK); + assertEquals(notification.icon(), ItemStack.of(Material.ITEM_FRAME)); + assertEquals(notification.title(), Component.text("unit test")); + } + + @Test + void testSend(Env env) { + var instance = env.createFlatInstance(); + var connection = env.createConnection(); + Collector advancementsPacketCollector = connection.trackIncoming(AdvancementsPacket.class); + var player = connection.connect(instance, new Pos(0, 42, 0)).join(); + var notification = Notification.builder() + .icon(Material.ITEM_FRAME) + .title(Component.text("unit test")) + .frameType(FrameType.TASK) + .build(); + notification.send(player); + advancementsPacketCollector.assertCount(2); + AdvancementsPacket advancementsPacket = advancementsPacketCollector.collect().get(1); + assertNotNull(advancementsPacket); + Optional advancementMapping = advancementsPacket.advancementMappings().stream().findFirst(); + advancementMapping.ifPresent(advancementMapping1 -> { + AdvancementsPacket.Advancement advancement = advancementMapping1.value(); + assertFalse(advancement.sendTelemetryData()); + var displayData = advancement.displayData(); + assertEquals(displayData.icon(), ItemStack.of(Material.ITEM_FRAME)); + assertEquals(displayData.title(), Component.text("unit test")); + assertEquals(displayData.frameType(), FrameType.TASK); + }); + } +}