diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index 52763250f..f19ca2b32 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -1,5 +1,7 @@ package net.minestom.demo; +import net.kyori.adventure.resource.ResourcePackInfo; +import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.text.Component; import net.minestom.server.MinecraftServer; import net.minestom.server.adventure.MinestomAdventure; @@ -18,7 +20,10 @@ import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.PickupItemEvent; import net.minestom.server.event.player.*; import net.minestom.server.event.server.ServerTickMonitorEvent; -import net.minestom.server.instance.*; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.InstanceManager; +import net.minestom.server.instance.LightingChunk; import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; @@ -32,8 +37,11 @@ import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.world.DimensionType; +import java.net.URI; import java.time.Duration; -import java.util.*; +import java.util.Random; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicReference; @@ -120,9 +128,9 @@ public class PlayerInit { var itemStack = event.getItemStack(); var block = event.getInstance().getBlock(event.getPosition()); - if ("false".equals(block.getProperty("waterlogged")) && itemStack.material().equals(Material.WATER_BUCKET)) { + if ("false" .equals(block.getProperty("waterlogged")) && itemStack.material().equals(Material.WATER_BUCKET)) { block = block.withProperty("waterlogged", "true"); - } else if ("true".equals(block.getProperty("waterlogged")) && itemStack.material().equals(Material.BUCKET)) { + } else if ("true" .equals(block.getProperty("waterlogged")) && itemStack.material().equals(Material.BUCKET)) { block = block.withProperty("waterlogged", "false"); } else return; diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index fa1649fc8..cba2e0d4a 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -8,6 +8,9 @@ import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.inventory.Book; import net.kyori.adventure.pointer.Pointers; +import net.kyori.adventure.resource.ResourcePackCallback; +import net.kyori.adventure.resource.ResourcePackRequest; +import net.kyori.adventure.resource.ResourcePackStatus; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.text.Component; @@ -58,10 +61,7 @@ import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.packet.client.ClientPacket; import net.minestom.server.network.packet.server.SendablePacket; import net.minestom.server.network.packet.server.ServerPacket; -import net.minestom.server.network.packet.server.common.DisconnectPacket; -import net.minestom.server.network.packet.server.common.KeepAlivePacket; -import net.minestom.server.network.packet.server.common.PluginMessagePacket; -import net.minestom.server.network.packet.server.common.ResourcePackPushPacket; +import net.minestom.server.network.packet.server.common.*; import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.packet.server.play.data.DeathLocation; @@ -70,7 +70,6 @@ import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; import net.minestom.server.recipe.Recipe; import net.minestom.server.recipe.RecipeManager; -import net.minestom.server.resourcepack.ResourcePack; import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Team; import net.minestom.server.snapshot.EntitySnapshot; @@ -233,6 +232,11 @@ public class Player extends LivingEntity implements CommandSender, Localizable, private Identity identity; private final Pointers pointers; + // Resource packs + private final Map resourcePackCallbacks = new HashMap<>(); + // The future is non-null when a resource pack is in-flight, and completed when all statuses have been received. + private CompletableFuture resourcePackFuture = null; + public Player(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection playerConnection) { super(EntityType.PLAYER, uuid); this.username = username; @@ -1274,13 +1278,56 @@ public class Player extends LivingEntity implements CommandSender, Localizable, return !itemDropEvent.isCancelled(); } + @Override + public void sendResourcePacks(@NotNull ResourcePackRequest request) { + if (request.replace()) clearResourcePacks(); + + for (var pack : request.packs()) { + sendPacket(new ResourcePackPushPacket(pack, request.required(), request.prompt())); + resourcePackCallbacks.put(pack.id(), request.callback()); + if (resourcePackFuture == null) { + resourcePackFuture = new CompletableFuture<>(); + } + } + } + + @Override + public void removeResourcePacks(@NotNull UUID id, @NotNull UUID @NotNull ... others) { + sendPacket(new ResourcePackPopPacket(id)); + for (var other : others) { + sendPacket(new ResourcePackPopPacket(other)); + } + } + + @Override + public void clearResourcePacks() { + sendPacket(new ResourcePackPopPacket((UUID) null)); + } + /** - * Sets the player resource pack. - * - * @param resourcePack the resource pack + * If there are resource packs in-flight, a future is returned which will be completed when + * all resource packs have been responded to by the client. Otherwise null is returned. */ - public void setResourcePack(@NotNull ResourcePack resourcePack) { - sendPacket(new ResourcePackPushPacket(resourcePack)); + @ApiStatus.Internal + public @Nullable CompletableFuture getResourcePackFuture() { + return resourcePackFuture; + } + + @ApiStatus.Internal + public void onResourcePackStatus(@NotNull UUID id, @NotNull ResourcePackStatus status) { + var callback = resourcePackCallbacks.get(id); + if (callback == null) return; + + callback.packEventReceived(id, status, this); + if (!status.intermediate()) { + // Remove the callback and finish the future if relevant + resourcePackCallbacks.remove(id); + + if (resourcePackCallbacks.isEmpty() && resourcePackFuture != null) { + resourcePackFuture.complete(null); + resourcePackFuture = null; + } + } } /** @@ -2138,7 +2185,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, @Override public @NotNull HoverEvent asHoverEvent(@NotNull UnaryOperator op) { - return HoverEvent.showEntity(ShowEntity.of(EntityType.PLAYER, this.uuid, this.displayName)); + return HoverEvent.showEntity(ShowEntity.showEntity(EntityType.PLAYER, this.uuid, this.displayName)); } /** diff --git a/src/main/java/net/minestom/server/event/player/PlayerResourcePackStatusEvent.java b/src/main/java/net/minestom/server/event/player/PlayerResourcePackStatusEvent.java index 81c200c28..fb873d83d 100644 --- a/src/main/java/net/minestom/server/event/player/PlayerResourcePackStatusEvent.java +++ b/src/main/java/net/minestom/server/event/player/PlayerResourcePackStatusEvent.java @@ -1,8 +1,8 @@ package net.minestom.server.event.player; +import net.kyori.adventure.resource.ResourcePackStatus; import net.minestom.server.entity.Player; import net.minestom.server.event.trait.PlayerEvent; -import net.minestom.server.resourcepack.ResourcePackStatus; import org.jetbrains.annotations.NotNull; /** diff --git a/src/main/java/net/minestom/server/listener/common/ResourcePackListener.java b/src/main/java/net/minestom/server/listener/common/ResourcePackListener.java index 71946b799..5cf0c328d 100644 --- a/src/main/java/net/minestom/server/listener/common/ResourcePackListener.java +++ b/src/main/java/net/minestom/server/listener/common/ResourcePackListener.java @@ -9,5 +9,8 @@ public class ResourcePackListener { public static void listener(ClientResourcePackStatusPacket packet, Player player) { EventDispatcher.call(new PlayerResourcePackStatusEvent(player, packet.status())); + + // Run adventure callbacks for the resource pack + player.onResourcePackStatus(packet.id(), packet.status()); } } diff --git a/src/main/java/net/minestom/server/network/ConnectionManager.java b/src/main/java/net/minestom/server/network/ConnectionManager.java index b0a91b2e1..b5b699d3d 100644 --- a/src/main/java/net/minestom/server/network/ConnectionManager.java +++ b/src/main/java/net/minestom/server/network/ConnectionManager.java @@ -272,6 +272,10 @@ public final class ConnectionManager { player.sendPacket(TagsPacket.DEFAULT_TAGS); } + // Wait for pending resource packs if any + var packFuture = player.getResourcePackFuture(); + if (packFuture != null) packFuture.join(); + player.setPendingInstance(spawningInstance); player.sendPacket(new FinishConfigurationPacket()); }); diff --git a/src/main/java/net/minestom/server/network/packet/client/common/ClientResourcePackStatusPacket.java b/src/main/java/net/minestom/server/network/packet/client/common/ClientResourcePackStatusPacket.java index a28b9dd72..5caa2cb86 100644 --- a/src/main/java/net/minestom/server/network/packet/client/common/ClientResourcePackStatusPacket.java +++ b/src/main/java/net/minestom/server/network/packet/client/common/ClientResourcePackStatusPacket.java @@ -1,8 +1,8 @@ package net.minestom.server.network.packet.client.common; +import net.kyori.adventure.resource.ResourcePackStatus; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.client.ClientPacket; -import net.minestom.server.resourcepack.ResourcePackStatus; import org.jetbrains.annotations.NotNull; import java.util.UUID; @@ -12,7 +12,7 @@ public record ClientResourcePackStatusPacket( @NotNull ResourcePackStatus status ) implements ClientPacket { public ClientResourcePackStatusPacket(@NotNull NetworkBuffer reader) { - this(reader.read(NetworkBuffer.UUID), reader.readEnum(ResourcePackStatus.class)); + this(reader.read(NetworkBuffer.UUID), readStatus(reader)); } @Override @@ -20,4 +20,19 @@ public record ClientResourcePackStatusPacket( writer.write(NetworkBuffer.UUID, id); writer.writeEnum(ResourcePackStatus.class, status); } + + private static @NotNull ResourcePackStatus readStatus(@NotNull NetworkBuffer reader) { + var ordinal = reader.read(NetworkBuffer.VAR_INT); + return switch (ordinal) { + case 0 -> ResourcePackStatus.SUCCESSFULLY_LOADED; + case 1 -> ResourcePackStatus.DECLINED; + case 2 -> ResourcePackStatus.FAILED_DOWNLOAD; + case 3 -> ResourcePackStatus.ACCEPTED; + case 4 -> ResourcePackStatus.DOWNLOADED; + case 5 -> ResourcePackStatus.INVALID_URL; + case 6 -> ResourcePackStatus.FAILED_RELOAD; + case 7 -> ResourcePackStatus.DISCARDED; + default -> throw new IllegalStateException("Unexpected resource pack status: " + ordinal); + }; + } } diff --git a/src/main/java/net/minestom/server/network/packet/server/common/ResourcePackPushPacket.java b/src/main/java/net/minestom/server/network/packet/server/common/ResourcePackPushPacket.java index 4ea4f9507..d31d1883e 100644 --- a/src/main/java/net/minestom/server/network/packet/server/common/ResourcePackPushPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/common/ResourcePackPushPacket.java @@ -1,12 +1,12 @@ package net.minestom.server.network.packet.server.common; +import net.kyori.adventure.resource.ResourcePackInfo; import net.kyori.adventure.text.Component; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.server.ComponentHoldingServerPacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; -import net.minestom.server.resourcepack.ResourcePack; import net.minestom.server.utils.PacketUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,9 +30,8 @@ public record ResourcePackPushPacket( reader.read(BOOLEAN), reader.readOptional(COMPONENT)); } - public ResourcePackPushPacket(@NotNull ResourcePack resourcePack) { - this(resourcePack.getId(), resourcePack.getUrl(), resourcePack.getHash(), - resourcePack.isForced(), resourcePack.getPrompt()); + public ResourcePackPushPacket(@NotNull ResourcePackInfo resourcePackInfo, boolean required, @Nullable Component prompt) { + this(resourcePackInfo.id(), resourcePackInfo.uri().toString(), resourcePackInfo.hash(), required, prompt); } @Override diff --git a/src/main/java/net/minestom/server/resourcepack/ResourcePack.java b/src/main/java/net/minestom/server/resourcepack/ResourcePack.java deleted file mode 100644 index ebc6c131f..000000000 --- a/src/main/java/net/minestom/server/resourcepack/ResourcePack.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.minestom.server.resourcepack; - -import net.kyori.adventure.text.Component; -import net.minestom.server.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.UUID; - -/** - * Represents a resource pack which can be sent with {@link Player#setResourcePack(ResourcePack)}. - */ -public class ResourcePack { - - private final UUID id; - private final String url; - private final String hash; - private final boolean forced; - private final Component prompt; - - private ResourcePack(@NotNull UUID id, @NotNull String url, @Nullable String hash, boolean forced, @Nullable Component prompt) { - this.id = id; - this.url = url; - // Optional, set to empty if null - this.hash = hash == null ? "" : hash; - this.forced = forced; - this.prompt = prompt; - } - - public static ResourcePack optional(@NotNull UUID id, @NotNull String url, @Nullable String hash, @Nullable Component prompt) { - return new ResourcePack(id, url, hash, false, prompt); - } - - public static ResourcePack optional(@NotNull UUID id, @NotNull String url, @Nullable String hash) { - return optional(id, url, hash, null); - } - - public static ResourcePack forced(@NotNull UUID id, @NotNull String url, @Nullable String hash, @Nullable Component prompt) { - return new ResourcePack(id, url, hash, true, prompt); - } - - public static ResourcePack forced(@NotNull UUID id, @NotNull String url, @Nullable String hash) { - return forced(id, url, hash, null); - } - - public @NotNull UUID getId() { - return id; - } - - /** - * Gets the resource pack URL. - * - * @return the resource pack URL - */ - public @NotNull String getUrl() { - return url; - } - - /** - * Gets the resource pack hash. - *

- * WARNING: if null or empty, the player will probably waste bandwidth by re-downloading - * the resource pack. - * - * @return the resource pack hash, can be empty - */ - public @NotNull String getHash() { - return hash; - } - - public boolean isForced() { - return forced; - } - - public @Nullable Component getPrompt() { - return prompt; - } - -} diff --git a/src/main/java/net/minestom/server/resourcepack/ResourcePackStatus.java b/src/main/java/net/minestom/server/resourcepack/ResourcePackStatus.java deleted file mode 100644 index 482a3a552..000000000 --- a/src/main/java/net/minestom/server/resourcepack/ResourcePackStatus.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.minestom.server.resourcepack; - -import net.minestom.server.entity.Player; - -/** - * Represents the result of {@link Player#setResourcePack(ResourcePack)} in - * {@link net.minestom.server.event.player.PlayerResourcePackStatusEvent}. - */ -public enum ResourcePackStatus { - - SUCCESSFULLY_LOADED, - DECLINED, - FAILED_DOWNLOAD, - ACCEPTED, - DOWNLOADED, - INVALID_URL, - FAILED_RELOAD, - DISCARDED, -}