From faefd65d84e8128165716dcd826da1002432da73 Mon Sep 17 00:00:00 2001 From: fishwob <147350172+fishwob@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:28:39 -0700 Subject: [PATCH 1/4] Configuration phase support for Velocity --- velocity/build.gradle.kts | 4 ++ .../velocity/handler/PackHandler.java | 68 +++++++++++++++++-- .../listener/ResourcePackListener.java | 27 ++++++-- .../resourcepack/VelocityResourcePack.java | 3 +- velocity/src/main/resources/config.toml | 4 ++ 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 21a6fe9..c9f46c3 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -2,6 +2,10 @@ plugins { id("buildlogic.java-platform-conventions") } +tasks.compileJava { + options.release.set(17) +} + repositories { maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") maven("https://repo.papermc.io/repository/maven-public/") diff --git a/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java b/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java index cc1c0a4..3d193bd 100644 --- a/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java +++ b/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java @@ -8,6 +8,7 @@ import com.convallyria.forcepack.velocity.ForcePackVelocity; import com.convallyria.forcepack.velocity.config.VelocityConfig; import com.convallyria.forcepack.velocity.player.ForcePackVelocityPlayer; +import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; @@ -29,6 +30,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -41,18 +43,70 @@ public final class PackHandler { private final Map waiting; private final Map> pendingTasks; + private final Map> configurationPhaseCompletions; + private final Map configurationPhaseServers; + private final Set configurationPhaseHandled; + public PackHandler(final ForcePackVelocity plugin) { this.plugin = plugin; this.waiting = new ConcurrentHashMap<>(); this.pendingTasks = new ConcurrentHashMap<>(); + this.configurationPhaseCompletions = new ConcurrentHashMap<>(); + this.configurationPhaseServers = new ConcurrentHashMap<>(); + this.configurationPhaseHandled = ConcurrentHashMap.newKeySet(); plugin.getServer().getEventManager().register(plugin, DisconnectEvent.class, this::onDisconnect); } + public EventTask handleConfigurationPhase(final Player player, final ServerConnection server) { + final UUID uuid = player.getUniqueId(); + configurationPhaseHandled.add(uuid); + + if (plugin.temporaryExemptedPlayers.remove(uuid)) { + plugin.log("Ignoring player " + player.getUsername() + " as they have a one-off exemption."); + return null; + } + + configurationPhaseServers.put(uuid, server); + setPack(player, server); + + if (!isWaiting(player)) { + // No pack needs to be applied, allow the configuration phase to continue immediately. + configurationPhaseServers.remove(uuid); + return null; + } + + final CompletableFuture future = new CompletableFuture<>(); + configurationPhaseCompletions.put(uuid, future); + plugin.log("Holding configuration phase for %s until their resource pack(s) are resolved.", player.getUsername()); + return EventTask.resumeWhenComplete(future); + } + + public boolean takeConfigurationPhaseHandled(final Player player) { + return configurationPhaseHandled.remove(player.getUniqueId()); + } + + public Optional getConfigurationPhaseServer(final Player player) { + return Optional.ofNullable(configurationPhaseServers.get(player.getUniqueId())); + } + + private void completeConfigurationPhase(final UUID uuid) { + configurationPhaseServers.remove(uuid); + final CompletableFuture future = configurationPhaseCompletions.remove(uuid); + if (future != null) { + future.complete(null); + } + } + + private void discardConfigurationPhase(final UUID uuid) { + configurationPhaseServers.remove(uuid); + configurationPhaseCompletions.remove(uuid); + } + public void processWaitingResourcePack(Player player, UUID packId) { final UUID playerId = player.getUniqueId(); // If the player is on a version older than 1.20.3, they can only have one resource pack. if (player.getProtocolVersion().getProtocol() < ProtocolVersion.MINECRAFT_1_20_3.getProtocol()) { - removeFromWaiting(player); + removeFromWaiting(player, true); return; } @@ -62,7 +116,7 @@ public void processWaitingResourcePack(Player player, UUID packId) { }); if (newPlayer == null || newPlayer.getWaitingPacks().isEmpty()) { - removeFromWaiting(player); + removeFromWaiting(player, true); } } @@ -84,12 +138,18 @@ public boolean isWaitingFor(Player player, @Nullable UUID packId) { } private void onDisconnect(DisconnectEvent event) { - removeFromWaiting(event.getPlayer()); + removeFromWaiting(event.getPlayer(), false); pendingTasks.remove(event.getPlayer().getUniqueId()); + configurationPhaseHandled.remove(event.getPlayer().getUniqueId()); } - private void removeFromWaiting(Player player) { + private void removeFromWaiting(Player player, boolean resumeConfigurationPhase) { waiting.remove(player.getUniqueId()); + if (resumeConfigurationPhase) { + completeConfigurationPhase(player.getUniqueId()); + } else { + discardConfigurationPhase(player.getUniqueId()); + } } private void addToWaiting(Player player, @NonNull ResourcePack pack) { diff --git a/velocity/src/main/java/com/convallyria/forcepack/velocity/listener/ResourcePackListener.java b/velocity/src/main/java/com/convallyria/forcepack/velocity/listener/ResourcePackListener.java index 724acc4..fa7ff9e 100644 --- a/velocity/src/main/java/com/convallyria/forcepack/velocity/listener/ResourcePackListener.java +++ b/velocity/src/main/java/com/convallyria/forcepack/velocity/listener/ResourcePackListener.java @@ -10,11 +10,13 @@ import com.convallyria.forcepack.velocity.handler.PackHandler; import com.convallyria.forcepack.velocity.resourcepack.VelocityResourcePack; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.event.player.configuration.PlayerConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; @@ -37,8 +39,10 @@ public ResourcePackListener(final ForcePackVelocity plugin) { @Subscribe(order = PostOrder.EARLY) public void onPackStatus(PlayerResourcePackStatusEvent event) { final Player player = event.getPlayer(); - final Optional currentServer = player.getCurrentServer(); - if (currentServer.isEmpty()) { + final ServerConnection currentServer = player.getCurrentServer() + .or(() -> plugin.getPackHandler().getConfigurationPhaseServer(player)) + .orElse(null); + if (currentServer == null) { plugin.log(player.getUsername() + "'s server does not exist."); return; } @@ -46,7 +50,7 @@ public void onPackStatus(PlayerResourcePackStatusEvent event) { final PlayerResourcePackStatusEvent.Status status = event.getStatus(); // Check if the server they're on has a resource pack - final String serverName = currentServer.get().getServerInfo().getName(); + final String serverName = currentServer.getServerInfo().getName(); final ResourcePackInfo packInfo = event.getPackInfo(); // Returns null on < 1.20.3 clients, and if a UUID isn't provided I guess? final UUID id = packInfo == null ? null : packInfo.getId(); if (id != null) plugin.log(player.getUsername() + " sent response id '%s'", id.toString()); @@ -106,12 +110,12 @@ public void onPackStatus(PlayerResourcePackStatusEvent event) { // Declined/failed is valid and should be allowed, server owner decides whether they get kicked if (status != PlayerResourcePackStatusEvent.Status.ACCEPTED && status != PlayerResourcePackStatusEvent.Status.DOWNLOADED && !kick) { - plugin.log("Sent player '%s' plugin message downstream to '%s' for status '%s'", player.getUsername(), currentServer.get().getServerInfo().getName(), status.name()); + plugin.log("Sent player '%s' plugin message downstream to '%s' for status '%s'", player.getUsername(), currentServer.getServerInfo().getName(), status.name()); // No longer applying, remove them from the list plugin.getPackHandler().processWaitingResourcePack(player, packByServer.getUUID()); final String name = status == PlayerResourcePackStatusEvent.Status.SUCCESSFUL ? "SUCCESSFULLY_LOADED" : status.name(); final boolean waiting = plugin.getPackHandler().isWaiting(player); - currentServer.get().sendPluginMessage(PackHandler.FORCEPACK_STATUS_IDENTIFIER, (packByServer.getUUID().toString() + ";" + name + ";" + !waiting).getBytes(StandardCharsets.UTF_8)); + currentServer.sendPluginMessage(PackHandler.FORCEPACK_STATUS_IDENTIFIER, (packByServer.getUUID().toString() + ";" + name + ";" + !waiting).getBytes(StandardCharsets.UTF_8)); plugin.getPackHandler().getForcePackPlayer(player).ifPresentOrElse(forcePackPlayer -> { plugin.log("Current packs we are waiting for: %s", forcePackPlayer.getWaitingPacks()); }, () -> plugin.log("Waiting for? %s", waiting)); @@ -165,12 +169,25 @@ private boolean disconnectAction(Player player, VelocityConfig actions) { return true; } + @Subscribe(order = PostOrder.EARLY) + public EventTask onConfigure(PlayerConfigurationEvent event) { + if (!plugin.getConfig().getBoolean("use-configuration-phase", false)) return null; + final Player player = event.player(); + plugin.log("Handling resource pack for %s during the configuration phase.", player.getUsername()); + return plugin.getPackHandler().handleConfigurationPhase(player, event.server()); + } + @Subscribe(order = PostOrder.EARLY) public void onJoin(ServerPostConnectEvent event) { final Player player = event.getPlayer(); final Optional currentServer = player.getCurrentServer(); if (currentServer.isEmpty()) return; + if (plugin.getPackHandler().takeConfigurationPhaseHandled(player)) { + plugin.log("Not sending resource pack to %s on join as it was handled during the configuration phase.", player.getUsername()); + return; + } + if (plugin.temporaryExemptedPlayers.remove(player.getUniqueId())) { plugin.log("Ignoring player " + player.getUsername() + " as they have a one-off exemption."); return; diff --git a/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java b/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java index 8ce15fc..a9b8170 100644 --- a/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java +++ b/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java @@ -55,7 +55,8 @@ private void runSetResourcePack(UUID uuid) { if (server.contains(ForcePackVelocity.GLOBAL_SERVER_NAME)) { serverConfig = velocityPlugin.getConfig().getConfig("global-pack"); final List excluded = serverConfig.getStringList("exclude"); - final Optional currentServer = player.getCurrentServer(); + final Optional currentServer = player.getCurrentServer() + .or(() -> velocityPlugin.getPackHandler().getConfigurationPhaseServer(player)); if (currentServer.isPresent()) { if (excluded.contains(currentServer.get().getServerInfo().getName())) return; } else { diff --git a/velocity/src/main/resources/config.toml b/velocity/src/main/resources/config.toml index 14b9804..f3298ab 100644 --- a/velocity/src/main/resources/config.toml +++ b/velocity/src/main/resources/config.toml @@ -44,6 +44,10 @@ enable-mc-164316-fix = true # Note that with this true, the custom disconnect message will not work because the client forcefully kicks itself use-new-force-pack-screen = true +# Whether to use the configuration phase. +# When enabled, players on 1.20.2+ receive and load the pack before spawning into the world instead of after joining. +use-configuration-phase = false + # Should we try and prevent hacked clients sending fake resource pack accept packets? # Still bypassable, but some are stupid and we are able to detect them. try-to-stop-fake-accept-hacks = true From fee5f9d744f3441c3d3245b19de3e39d384d0a5e Mon Sep 17 00:00:00 2001 From: SamB440 Date: Tue, 23 Jun 2026 16:21:31 +0100 Subject: [PATCH 2/4] velocity: fix group prompts not working --- .../resourcepack/VelocityResourcePack.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java b/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java index a9b8170..329acc6 100644 --- a/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java +++ b/velocity/src/main/java/com/convallyria/forcepack/velocity/resourcepack/VelocityResourcePack.java @@ -45,7 +45,7 @@ private void runSetResourcePack(UUID uuid) { final Player player = velocityPlugin.getServer().getPlayer(uuid).orElse(null); if (player == null) return; - final ResourcePackInfo.Builder infoBuilder = velocityPlugin.getServer() + ResourcePackInfo.Builder infoBuilder = velocityPlugin.getServer() .createResourcePackBuilder(getURL()) .setHash(getHashSum()) .setId(this.uuid) @@ -63,13 +63,19 @@ private void runSetResourcePack(UUID uuid) { velocityPlugin.log("Unable to check global resource pack exclusion list as player is not in a server!?"); } } else { - serverConfig = velocityPlugin.getConfig().getConfig("servers").getConfig(server); + if (group != null) { + serverConfig = velocityPlugin.getConfig().getConfig("groups").getConfig(group); + } else { + serverConfig = velocityPlugin.getConfig().getConfig("servers").getConfig(server); + } } if (serverConfig != null) { final String promptText = serverConfig.getConfig("resourcepack").getString("prompt"); - final Component promptComponent = velocityPlugin.getMiniMessage().deserialize(promptText); - infoBuilder.setPrompt(promptComponent); + if (promptText != null) { + final Component promptComponent = velocityPlugin.getMiniMessage().deserialize(promptText); + infoBuilder = infoBuilder.setPrompt(promptComponent); + } } final ResourcePackInfo built = infoBuilder.build(); From 2d564e453408988857a4fe5671fe3fa91594cab0 Mon Sep 17 00:00:00 2001 From: SamB440 Date: Tue, 23 Jun 2026 16:24:38 +0100 Subject: [PATCH 3/4] sponge: support velocity proxy configuration-phase resource pack statuses --- .../forcepack/api/player/ForcePackPlayer.java | 3 + .../forcepack/paper/ForcePackPaper.java | 2 +- .../paper/player/ForcePackPaperPlayer.java | 14 +- .../forcepack/sponge/ForcePackSponge.java | 66 ++++--- .../sponge/command/ForcePackCommand.java | 23 +-- .../MultiVersionResourcePackStatusEvent.java | 30 +++- .../sponge/listener/ExemptionListener.java | 6 +- .../sponge/listener/PacketListener.java | 23 ++- .../sponge/listener/ResourcePackListener.java | 167 +++++++++++------- .../sponge/player/ForcePackSpongePlayer.java | 57 +++++- .../resourcepack/SpongeResourcePack.java | 4 +- .../forcepack/sponge/util/ProtocolUtil.java | 22 ++- .../velocity/handler/PackHandler.java | 2 +- .../player/ForcePackVelocityPlayer.java | 15 +- 14 files changed, 293 insertions(+), 141 deletions(-) diff --git a/api/src/main/java/com/convallyria/forcepack/api/player/ForcePackPlayer.java b/api/src/main/java/com/convallyria/forcepack/api/player/ForcePackPlayer.java index 4f4b5e8..7313051 100644 --- a/api/src/main/java/com/convallyria/forcepack/api/player/ForcePackPlayer.java +++ b/api/src/main/java/com/convallyria/forcepack/api/player/ForcePackPlayer.java @@ -5,9 +5,12 @@ import java.util.List; import java.util.Set; +import java.util.UUID; public interface ForcePackPlayer { + UUID uniqueId(); + Set getWaitingPacks(); List getChecks(); diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java b/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java index 82e4568..18a05f2 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java @@ -139,7 +139,7 @@ public void removeFromWaiting(Player player) { public void addToWaiting(UUID uuid, @NonNull Set packs) { waiting.compute(uuid, (a, existing) -> { - ForcePackPlayer newPlayer = existing != null ? existing : new ForcePackPaperPlayer(Bukkit.getPlayer(uuid)); + ForcePackPlayer newPlayer = existing != null ? existing : new ForcePackPaperPlayer(uuid); newPlayer.getWaitingPacks().addAll(packs); return newPlayer; }); diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java b/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java index e05b975..e7e5d1f 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java @@ -12,15 +12,23 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; public class ForcePackPaperPlayer implements ForcePackPlayer { + private final UUID uniqueId; private final Set waitingPacks = new HashSet<>(); private final List checks = new ArrayList<>(); - public ForcePackPaperPlayer(Player player) { - checks.add(new DelayedSuccessSpoofCheck(ForcePackPaper.getAPI(), player.getUniqueId())); - checks.add(new InvalidOrderSpoofCheck(ForcePackPaper.getAPI(), player.getUniqueId())); + public ForcePackPaperPlayer(UUID uuid) { + this.uniqueId = uuid; + checks.add(new DelayedSuccessSpoofCheck(ForcePackPaper.getAPI(), uuid)); + checks.add(new InvalidOrderSpoofCheck(ForcePackPaper.getAPI(), uuid)); + } + + @Override + public UUID uniqueId() { + return uniqueId; } @Override diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/ForcePackSponge.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/ForcePackSponge.java index 7e5b7df..950a19a 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/ForcePackSponge.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/ForcePackSponge.java @@ -40,14 +40,15 @@ import org.spongepowered.api.Sponge; import org.spongepowered.api.command.Command; import org.spongepowered.api.config.ConfigDir; -import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.event.EventManager; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.lifecycle.RegisterChannelEvent; import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; import org.spongepowered.api.event.lifecycle.StartingEngineEvent; import org.spongepowered.api.network.ServerConnectionState; +import org.spongepowered.api.network.channel.ChannelBuf; import org.spongepowered.api.network.channel.raw.RawDataChannel; +import org.spongepowered.api.profile.GameProfile; import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.loader.ConfigurationLoader; @@ -172,22 +173,21 @@ public void onServerStarting(final StartingEngineEvent event) { } } - public Set getPacksForVersion(ServerPlayer player) { + public Set getPacksForVersion(UUID player) { final int protocolVersion = ProtocolUtil.getProtocolVersion(player); return getPacksForVersion(protocolVersion); } - private final Map waiting = new HashMap<>(); + private final Map waiting = new HashMap<>(); - public void processWaitingResourcePack(ServerPlayer player, UUID packId) { - final UUID playerId = player.uniqueId(); + public void processWaitingResourcePack(UUID player, UUID packId) { // If the player is on a version older than 1.20.3, they can only have one resource pack. if (ProtocolUtil.getProtocolVersion(player) < 765) { removeFromWaiting(player); return; } - final ForcePackPlayer newPlayer = waiting.computeIfPresent(playerId, (a, forcePackPlayer) -> { + final ForcePackPlayer newPlayer = waiting.computeIfPresent(player, (a, forcePackPlayer) -> { final Set packs = forcePackPlayer.getWaitingPacks(); packs.removeIf(pack -> pack.getUUID().equals(packId)); return forcePackPlayer; @@ -198,15 +198,15 @@ public void processWaitingResourcePack(ServerPlayer player, UUID packId) { } } - public Optional getForcePackPlayer(ServerPlayer player) { - return Optional.ofNullable(waiting.get(player.uniqueId())); + public Optional getForcePackPlayer(UUID player) { + return Optional.ofNullable(waiting.get(player)); } - public boolean isWaiting(ServerPlayer player) { - return waiting.containsKey(player.uniqueId()); + public boolean isWaiting(UUID player) { + return waiting.containsKey(player); } - public boolean isWaitingFor(ServerPlayer player, UUID packId) { + public boolean isWaitingFor(UUID player, UUID packId) { if (!isWaiting(player)) return false; // If the player is on a version older than 1.20.3, they can only have one resource pack. @@ -214,17 +214,17 @@ public boolean isWaitingFor(ServerPlayer player, UUID packId) { return true; } - final Set waitingPacks = waiting.get(player.uniqueId()).getWaitingPacks(); + final Set waitingPacks = waiting.get(player).getWaitingPacks(); return waitingPacks.stream().anyMatch(pack -> pack.getUUID().equals(packId)); } - public void removeFromWaiting(ServerPlayer player) { - waiting.remove(player.uniqueId()); + public void removeFromWaiting(UUID uuid) { + waiting.remove(uuid); } - public void addToWaiting(UUID uuid, @NonNull Set packs) { - waiting.compute(uuid, (a, existing) -> { - ForcePackPlayer newPlayer = existing != null ? existing : new ForcePackSpongePlayer(Sponge.server().player(uuid).orElseThrow()); + public ForcePackSpongePlayer addToWaiting(UUID uuid, @NonNull Set packs) { + return waiting.compute(uuid, (a, existing) -> { + ForcePackSpongePlayer newPlayer = existing != null ? existing : new ForcePackSpongePlayer(uuid); newPlayer.getWaitingPacks().addAll(packs); return newPlayer; }); @@ -368,20 +368,34 @@ private boolean checkPack(@Nullable ResourcePackVersion version, String url, boo public void onRegisterChannels(RegisterChannelEvent event) { if (!getConfig().node("velocity-mode").getBoolean()) return; getLogger().info("Enabled velocity listener"); + final RawDataChannel channel = event.register(ResourceKey.of("forcepack", "status"), RawDataChannel.class); channel.play().addHandler(ServerConnectionState.Game.class, (message, state) -> { - final ServerPlayer player = state.player(); - final String data = new String(message.readBytes(message.available())); - final String[] split = data.split(";"); - log("Posted event"); - - final ResourcePackStatus status = ResourcePackStatus.valueOf(split[1]); - final UUID packId = UUID.fromString(split[0]); - final boolean proxyRemove = Boolean.parseBoolean(split[2]); - getScheduler().executeAsync(() -> Sponge.eventManager().post(new MultiVersionResourcePackStatusEvent(player, packId, status, true, proxyRemove))); + handlePackStatusMessage(state.profile(), message); + }); + + channel.play().addHandler(ServerConnectionState.Configuration.class, (message, state) -> { + handlePackStatusMessage(state.profile(), message); }); } + private void handlePackStatusMessage(GameProfile profile, ChannelBuf message) { + final ForcePackSpongePlayer player = getForcePackPlayer(profile.uniqueId()).orElse(null); + if (player == null) { + // Player isn't valid - wasn't added at auth + return; + } + + final String data = new String(message.readBytes(message.available())); + final String[] split = data.split(";"); + log("Posted event"); + + final ResourcePackStatus status = ResourcePackStatus.valueOf(split[1]); + final UUID packId = UUID.fromString(split[0]); + final boolean proxyRemove = Boolean.parseBoolean(split[2]); + Sponge.eventManager().post(new MultiVersionResourcePackStatusEvent(profile, player.getConnection(), packId, status, true, proxyRemove)); + } + private void registerListeners() { EventManager pm = Sponge.eventManager(); diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java index 3ec92e8..560c594 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java @@ -2,9 +2,9 @@ import com.convallyria.forcepack.api.permission.Permissions; import com.convallyria.forcepack.api.resourcepack.ResourcePack; -import com.convallyria.forcepack.api.utils.GeyserUtil; import com.convallyria.forcepack.sponge.ForcePackSponge; import com.convallyria.forcepack.sponge.event.ForcePackReloadEvent; +import com.convallyria.forcepack.sponge.player.ForcePackSpongePlayer; import com.convallyria.forcepack.sponge.util.ProtocolUtil; import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerResourcePackRemove; @@ -48,18 +48,21 @@ public void onReload(CommandCause sender, Sponge.eventManager().post(new ForcePackReloadEvent()); if (!plugin.getConfig().node("velocity-mode").getBoolean() && send) { for (ServerPlayer player : Sponge.server().onlinePlayers()) { - if (plugin.isWaiting(player)) continue; - boolean geyser = plugin.getConfig().node("Server", "geyser").getBoolean() && GeyserUtil.isBedrockPlayer(player.uniqueId()); - boolean canBypass = player.hasPermission(Permissions.BYPASS) && plugin.getConfig().node("Server", "bypass-permission").getBoolean(); - plugin.log(player.name() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); - if (geyser || canBypass) continue; + if (plugin.isWaiting(player.uniqueId())) { + // Player is already responding to a resource pack sent to them + // TODO: How do we fix this properly? + continue; + } + + if (!ForcePackSpongePlayer.profileIsValid(plugin, player.profile())) continue; player.sendMessage(Component.translatable("forcepack.reloading")); - final Set resourcePacks = plugin.getPacksForVersion(player); - plugin.addToWaiting(player.uniqueId(), resourcePacks); - if (ProtocolUtil.getProtocolVersion(player) >= 765) { // 1.20.3+ - ProtocolUtil.sendPacketBypassingVia(player, new WrapperPlayServerResourcePackRemove((UUID) null)); + final Set resourcePacks = plugin.getPacksForVersion(player.uniqueId()); + final ForcePackSpongePlayer fpPlayer = plugin.addToWaiting(player.uniqueId(), resourcePacks); + fpPlayer.setConnection(player.connection()); + if (ProtocolUtil.getProtocolVersion(player.uniqueId()) >= 765) { // 1.20.3+ + ProtocolUtil.sendPacketBypassingVia(player.uniqueId(), new WrapperPlayServerResourcePackRemove((UUID) null)); } resourcePacks.forEach(pack -> pack.setResourcePack(player.uniqueId())); } diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/event/MultiVersionResourcePackStatusEvent.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/event/MultiVersionResourcePackStatusEvent.java index d87416b..4b7d605 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/event/MultiVersionResourcePackStatusEvent.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/event/MultiVersionResourcePackStatusEvent.java @@ -3,11 +3,12 @@ import net.kyori.adventure.resource.ResourcePackStatus; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.event.Cancellable; import org.spongepowered.api.event.Cause; import org.spongepowered.api.event.EventContext; import org.spongepowered.api.event.impl.AbstractEvent; +import org.spongepowered.api.network.ServerSideConnection; +import org.spongepowered.api.profile.GameProfile; import java.util.UUID; @@ -16,7 +17,8 @@ */ public class MultiVersionResourcePackStatusEvent extends AbstractEvent implements Cancellable { - private final ServerPlayer player; + private final GameProfile profile; + private final ServerSideConnection connection; private final Cause cause; private final UUID id; private final ResourcePackStatus status; @@ -24,8 +26,11 @@ public class MultiVersionResourcePackStatusEvent extends AbstractEvent implement private final boolean proxyRemove; private boolean cancel; - public MultiVersionResourcePackStatusEvent(@NonNull final ServerPlayer who, @NonNull UUID id, @NonNull ResourcePackStatus resourcePackStatus, boolean proxy, boolean proxyRemove) { - this.player = who; + public MultiVersionResourcePackStatusEvent(@NonNull final GameProfile who, @NonNull final ServerSideConnection connection, + @NonNull UUID id, @NonNull ResourcePackStatus resourcePackStatus, + boolean proxy, boolean proxyRemove) { + this.profile = who; + this.connection = connection; this.id = id; this.status = resourcePackStatus; this.proxy = proxy; @@ -34,12 +39,21 @@ public MultiVersionResourcePackStatusEvent(@NonNull final ServerPlayer who, @Non } /** - * Returns the player involved in this event - * @return Player who is involved in this event + * Returns the profile involved in this event + * @return Profile who is involved in this event */ @NonNull - public final ServerPlayer getPlayer() { - return player; + public final GameProfile getProfile() { + return profile; + } + + /** + * Returns the connection involved in this event + * @return Connection involved in this event + */ + @NonNull + public final ServerSideConnection getConnection() { + return connection; } /** diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ExemptionListener.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ExemptionListener.java index 64400ca..25094ad 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ExemptionListener.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ExemptionListener.java @@ -20,7 +20,7 @@ public void onDamage(DamageEntityEvent event) { event.cause().first(ServerPlayer.class).ifPresent(damager -> { if (damager.equals(event.entity())) return; - if (plugin.isWaiting(damager)) { + if (plugin.isWaiting(damager.uniqueId())) { event.setCancelled(true); plugin.log("Cancelled damage for damager '" + damager.name() + "' due to resource pack not applied."); } @@ -28,7 +28,7 @@ public void onDamage(DamageEntityEvent event) { if (event.entity() instanceof ServerPlayer) { ServerPlayer damaged = (ServerPlayer) event.entity(); - if (plugin.isWaiting(damaged)) { + if (plugin.isWaiting(damaged.uniqueId())) { event.setCancelled(true); plugin.log("Cancelled damage for player '" + damaged.name() + "' due to resource pack not applied."); } @@ -41,7 +41,7 @@ public void onMove(MoveEntityEvent event) { if (!plugin.getConfig().node("prevent-movement").getBoolean()) return; ServerPlayer player = (ServerPlayer) event.entity(); - if (plugin.isWaiting(player)) { + if (plugin.isWaiting(player.uniqueId())) { event.setCancelled(true); plugin.log("Cancelled movement for player '" + player.name() + "' due to resource pack not applied."); } diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/PacketListener.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/PacketListener.java index fdf4139..74973d5 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/PacketListener.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/PacketListener.java @@ -2,13 +2,16 @@ import com.convallyria.forcepack.sponge.ForcePackSponge; import com.convallyria.forcepack.sponge.event.MultiVersionResourcePackStatusEvent; +import com.convallyria.forcepack.sponge.player.ForcePackSpongePlayer; import com.github.retrooper.packetevents.event.PacketListenerAbstract; import com.github.retrooper.packetevents.event.PacketReceiveEvent; import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.protocol.player.User; import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientResourcePackStatus; import net.kyori.adventure.resource.ResourcePackStatus; import org.spongepowered.api.Sponge; -import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.profile.GameProfile; import java.util.UUID; @@ -27,22 +30,30 @@ public PacketListener(ForcePackSponge plugin) { @Override public void onPacketReceive(PacketReceiveEvent event) { - if (event.getPacketType() == PacketType.Play.Client.RESOURCE_PACK_STATUS) { - final ServerPlayer player = Sponge.server().player(event.getUser().getUUID()).orElse(null); + if (isResourcePackStatus(event.getPacketType())) { + final User user = event.getUser(); + final ForcePackSpongePlayer player = plugin.getForcePackPlayer(user.getUUID()).orElse(null); if (player == null) { - plugin.getLogger().warn("Unable to get player for resource pack status!?!? {}, {}", event.getUser(), event.getPlayer()); +// plugin.getLogger().warn("Unable to get player for resource pack status!?!? {}, {}", user, event.getPlayer()); + // Player isn't valid - wasn't added at auth return; } - plugin.log("Received packet resource pack status from " + player.name() + " (version: " + event.getServerVersion().getReleaseName() + ")"); + plugin.log("Received packet resource pack status from " + user.getName() + " (version: " + event.getServerVersion().getReleaseName() + ")"); final WrapperPlayClientResourcePackStatus status = new WrapperPlayClientResourcePackStatus(event); final WrapperPlayClientResourcePackStatus.Result result = status.getResult(); final UUID packId = status.getPackId(); - final MultiVersionResourcePackStatusEvent packEvent = new MultiVersionResourcePackStatusEvent(player, packId, ResourcePackStatus.valueOf(result.name()), false, false); + + final GameProfile gameProfile = GameProfile.of(user.getUUID(), user.getName()); + final MultiVersionResourcePackStatusEvent packEvent = new MultiVersionResourcePackStatusEvent(gameProfile, player.getConnection(), packId, ResourcePackStatus.valueOf(result.name()), false, false); if (Sponge.eventManager().post(packEvent)) { event.setCancelled(true); } } } + + private boolean isResourcePackStatus(PacketTypeCommon type) { + return type == PacketType.Play.Client.RESOURCE_PACK_STATUS || type == PacketType.Configuration.Client.RESOURCE_PACK_STATUS; + } } diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ResourcePackListener.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ResourcePackListener.java index 8c5bd48..2330ff9 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ResourcePackListener.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/listener/ResourcePackListener.java @@ -1,15 +1,14 @@ package com.convallyria.forcepack.sponge.listener; import com.convallyria.forcepack.api.check.SpoofCheck; -import com.convallyria.forcepack.api.permission.Permissions; import com.convallyria.forcepack.api.player.ForcePackPlayer; import com.convallyria.forcepack.api.resourcepack.ResourcePack; import com.convallyria.forcepack.api.schedule.PlatformScheduler; import com.convallyria.forcepack.api.utils.ClientVersion; -import com.convallyria.forcepack.api.utils.GeyserUtil; import com.convallyria.forcepack.sponge.ForcePackSponge; import com.convallyria.forcepack.sponge.event.ForcePackReloadEvent; import com.convallyria.forcepack.sponge.event.MultiVersionResourcePackStatusEvent; +import com.convallyria.forcepack.sponge.player.ForcePackSpongePlayer; import com.convallyria.forcepack.sponge.util.ProtocolUtil; import net.kyori.adventure.resource.ResourcePackInfo; import net.kyori.adventure.resource.ResourcePackStatus; @@ -51,22 +50,15 @@ public ResourcePackListener(final ForcePackSponge plugin) { @Listener public void onStatus(MultiVersionResourcePackStatusEvent event) { - final ServerPlayer player = event.getPlayer(); - final UUID id = event.getID(); + final GameProfile player = event.getProfile(); - final boolean velocityMode = getConfig().node("velocity-mode").getBoolean(); - - boolean geyser = getConfig().node("Server", "geyser").getBoolean() && GeyserUtil.isBedrockPlayer(player.uniqueId()); - boolean canBypass = player.hasPermission(Permissions.BYPASS) - && getConfig().node("Server", "bypass-permission").getBoolean() - // Bypass should be handled by velocity instead. - && !velocityMode; - plugin.log(player.name() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); - - if (canBypass || geyser) { + if (!plugin.isWaiting(player.uniqueId())) { + // Player isn't valid - wasn't added at auth return; } + final UUID packId = event.getID(); + if (plugin.temporaryExemptedPlayers.remove(player.uniqueId())) { plugin.log("Ignoring player " + player.name() + " as they have a one-off exemption."); return; @@ -75,15 +67,15 @@ && getConfig().node("Server", "bypass-permission").getBoolean() final ResourcePackStatus status = event.getStatus(); plugin.log(player.name() + " sent status: " + status); - + final boolean velocityMode = getConfig().node("velocity-mode").getBoolean(); if (!velocityMode && tryValidateHacks(player, event, status)) return; // If we did not set this resource pack, ignore - if (!event.isProxy() && !plugin.isWaitingFor(player, id)) { - plugin.log("Ignoring resource pack " + id + " because it wasn't set by ForcePack."); + if (!event.isProxy() && !plugin.isWaitingFor(player.uniqueId(), packId)) { + plugin.log("Ignoring resource pack " + packId + " because it wasn't set by ForcePack."); return; } else if (event.isProxy()) { - plugin.log("Resource pack with id " + id + " sent by proxy. Removal state: " + event.isProxyRemove()); + plugin.log("Resource pack with id " + packId + " sent by proxy. Removal state: " + event.isProxyRemove()); } // Only remove from waiting if they actually loaded the resource pack, rather than any status @@ -91,10 +83,10 @@ && getConfig().node("Server", "bypass-permission").getBoolean() if (status != ResourcePackStatus.ACCEPTED && status != ResourcePackStatus.DOWNLOADED) { if (event.isProxy()) { if (event.isProxyRemove()) { - plugin.removeFromWaiting(player); + plugin.removeFromWaiting(player.uniqueId()); } } else { - plugin.processWaitingResourcePack(player, id); + plugin.processWaitingResourcePack(player.uniqueId(), packId); } } @@ -102,7 +94,7 @@ && getConfig().node("Server", "bypass-permission").getBoolean() for (String cmd : getConfig().node("Server", "Actions", status.name(), "Commands").getList(String.class, new ArrayList<>())) { ensureMainThread(() -> { try { - Sponge.server().commandManager().process(cmd.replace("[player]", player.name())); + Sponge.server().commandManager().process(cmd.replace("[player]", player.name().orElseThrow())); } catch (CommandException e) { throw new RuntimeException(e); } @@ -114,7 +106,7 @@ && getConfig().node("Server", "bypass-permission").getBoolean() // Don't execute kicks - handled by proxy if (velocityMode) { - plugin.getScheduler().executeOnMain(() -> this.callSpongeEvent(event)); + ensureMainThread(() -> this.callSpongeEvent(event)); return; } @@ -125,44 +117,62 @@ && getConfig().node("Server", "bypass-permission").getBoolean() sentAccept.put(player.uniqueId(), System.currentTimeMillis()); break; } + case DECLINED: { - ensureMainThread(() -> { - if (kick) player.kick(Component.translatable("forcepack.declined")); - else player.sendMessage(Component.translatable("forcepack.declined")); - }); + if (kick) { + event.getConnection().close(Component.translatable("forcepack.declined")); + } else { + ensureMainThread(() -> { + Sponge.server().player(player.uuid()).ifPresent(playerObject -> { + playerObject.sendMessage(Component.translatable("forcepack.declined")); + }); + }); + } sentAccept.remove(player.uniqueId()); break; } + case DISCARDED: case INVALID_URL: case FAILED_RELOAD: case FAILED_DOWNLOAD: { - ensureMainThread(() -> { - if (kick) player.kick(Component.translatable("forcepack.download_failed")); - else player.sendMessage(Component.translatable("forcepack.download_failed")); - }); + if (kick) { + event.getConnection().close(Component.translatable("forcepack.download_failed")); + } else { + ensureMainThread(() -> { + Sponge.server().player(player.uuid()).ifPresent(playerObject -> { + playerObject.sendMessage(Component.translatable("forcepack.download_failed")); + }); + }); + } sentAccept.remove(player.uniqueId()); break; } + case SUCCESSFULLY_LOADED: { - if (kick) ensureMainThread(() -> player.kick(Component.translatable("forcepack.accepted"))); - else { - ensureMainThread(() -> player.sendMessage(Component.translatable("forcepack.accepted"))); - boolean sendTitle = plugin.getConfig().node("send-loading-title").getBoolean(); - if (sendTitle) player.clearTitle(); + if (kick) { + event.getConnection().close(Component.translatable("forcepack.accepted")); + } else { + ensureMainThread(() -> { + Sponge.server().player(player.uuid()).ifPresent(playerObject -> { + playerObject.sendMessage(Component.translatable("forcepack.accepted")); + boolean sendTitle = plugin.getConfig().node("send-loading-title").getBoolean(); + if (sendTitle) playerObject.clearTitle(); + }); + }); } break; } } } - private boolean tryValidateHacks(ServerPlayer player, MultiVersionResourcePackStatusEvent event, ResourcePackStatus status) { + private boolean tryValidateHacks(GameProfile player, MultiVersionResourcePackStatusEvent event, ResourcePackStatus status) { final boolean tryPrevent = getConfig().node("try-to-stop-fake-accept-hacks").getBoolean(true); if (!tryPrevent) return false; - final ForcePackPlayer forcePackPlayer = plugin.getForcePackPlayer(player).orElse(null); + final ForcePackPlayer forcePackPlayer = plugin.getForcePackPlayer(player.uniqueId()).orElse(null); if (forcePackPlayer == null) { plugin.log("Not checking " + player.name() + " because they are not in waiting."); return false; @@ -181,7 +191,7 @@ private boolean tryValidateHacks(ServerPlayer player, MultiVersionResourcePackSt if (hasFailed) { plugin.log("Kicking player " + player.name() + " because they failed a check."); - ensureMainThread(() -> player.kick(Component.translatable("forcepack.download_failed"))); + event.getConnection().close(Component.translatable("forcepack.download_failed")); } return hasFailed; @@ -190,23 +200,24 @@ private boolean tryValidateHacks(ServerPlayer player, MultiVersionResourcePackSt private void callSpongeEvent(MultiVersionResourcePackStatusEvent event) { // Velocity doesn't correctly pass things to the backend server // Call sponge event manually for other plugins to handle status events + final Optional player = Sponge.server().player(event.getProfile().uuid()); // Can the ID be null? I'm not sure, let's just pass a random ID to avoid errors if this happens. // I doubt another plugin is using it anyway. Sponge.eventManager().post(new ResourcePackStatusEvent() { @Override public ServerSideConnection connection() { - return event.getPlayer().connection(); + return event.getConnection(); } @Override public GameProfile profile() { - return event.getPlayer().profile(); + return event.getProfile(); } @Override public Optional player() { - return Optional.of(event.getPlayer()); + return player; } @Override @@ -241,36 +252,60 @@ public Cause cause() { }); } + @Listener + public void onAuth(ServerSideConnectionEvent.Auth event) { + final GameProfile profile = event.profile(); + + if (ForcePackSpongePlayer.profileIsValid(plugin, profile)) { + final ForcePackSpongePlayer player = plugin.addToWaiting(profile.uniqueId(), Set.of()); + player.setConnection(event.connection()); + } + } + + @Listener + public void onConfig(ServerSideConnectionEvent.Configuration event) { + final GameProfile profile = event.profile(); + final ForcePackSpongePlayer player = plugin.getForcePackPlayer(profile.uniqueId()).orElse(null); + if (player == null) { + // Player isn't valid - wasn't added at auth + return; + } + + // Track connection so we can close it during config phase + player.setConnection(event.connection()); + } + @Listener public void onPlayerJoin(ServerSideConnectionEvent.Join event) { - ServerPlayer player = event.player(); + final ServerPlayer player = event.player(); - final boolean velocityMode = getConfig().node("velocity-mode").getBoolean(); - boolean geyser = getConfig().node("Server", "geyser").getBoolean() && GeyserUtil.isBedrockPlayer(player.uniqueId()); - boolean canBypass = player.hasPermission(Permissions.BYPASS) - && getConfig().node("Server", "bypass-permission").getBoolean() - && !velocityMode; - plugin.log(player.name() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); + final ForcePackSpongePlayer forcePackPlayer = plugin.getForcePackPlayer(player.uniqueId()).orElse(null); + if (forcePackPlayer == null) { + // Player isn't valid - wasn't added at auth + return; + } - if (canBypass || geyser) return; + forcePackPlayer.setConnection(event.connection()); - if (getConfig().node("velocity-mode").getBoolean()) { - plugin.log("Velocity mode is enabled"); - plugin.addToWaiting(player.uniqueId(), Set.of()); + final boolean velocityMode = getConfig().node("velocity-mode").getBoolean(); + if (velocityMode) { + // Nothing to do on our end, wait for proxy. return; } - final Set packs = plugin.getPacksForVersion(player); + final Set packs = plugin.getPacksForVersion(player.uniqueId()); if (packs.isEmpty()) { plugin.log("Warning: Packs for player " + player.name() + " are empty."); + // Nothing to do + plugin.removeFromWaiting(player.uniqueId()); return; } - plugin.addToWaiting(player.uniqueId(), packs); - + // Send resource packs. + // TODO: Support configuration phase for Sponge-only setup. for (ResourcePack pack : packs) { plugin.log("Sending pack " + pack.getUUID() + " to player " + player.name()); - final int version = ProtocolUtil.getProtocolVersion(player); + final int version = ProtocolUtil.getProtocolVersion(player.uniqueId()); final int maxSize = ClientVersion.getMaxSizeForVersion(version); final boolean forceSend = getConfig().node("Server", "force-invalid-size").getBoolean(); if (!forceSend && pack.getSize() > maxSize) { @@ -285,21 +320,22 @@ && getConfig().node("Server", "bypass-permission").getBoolean() @Listener public void onReload(ForcePackReloadEvent event) { for (ServerPlayer onlinePlayer : Sponge.server().onlinePlayers()) { - if (plugin.isWaiting(onlinePlayer)) continue; + if (plugin.isWaiting(onlinePlayer.uniqueId())) continue; sentAccept.remove(onlinePlayer.uniqueId()); } } private void runSetPackTask(ServerPlayer player, ResourcePack pack, int version) { + final UUID uuid = player.uniqueId(); AtomicReference task = new AtomicReference<>(); Runnable packTask = () -> { - if (plugin.isWaiting(player)) { + if (plugin.isWaiting(uuid)) { plugin.log("Sent resource pack to player"); - pack.setResourcePack(player.uniqueId()); + pack.setResourcePack(uuid); } boolean sendTitle = plugin.getConfig().node("send-loading-title").getBoolean(); - if (sendTitle && sentAccept.containsKey(player.uniqueId())) { + if (sendTitle && sentAccept.containsKey(uuid)) { player.showTitle(Title.title( Component.translatable("forcepack.download_start_title"), Component.translatable("forcepack.download_start_subtitle"), @@ -307,7 +343,7 @@ private void runSetPackTask(ServerPlayer player, ResourcePack pack, int version) } final PlatformScheduler.ForcePackTask acquired = task.get(); - if (acquired != null && !plugin.isWaiting(player) && !sentAccept.containsKey(player.uniqueId())) { + if (acquired != null && !plugin.isWaiting(uuid) && !sentAccept.containsKey(uuid)) { acquired.cancel(); } }; @@ -320,10 +356,11 @@ private void runSetPackTask(ServerPlayer player, ResourcePack pack, int version) } @Listener - public void onQuit(ServerSideConnectionEvent.Leave event) { - ServerPlayer player = event.player(); - plugin.removeFromWaiting(player); - sentAccept.remove(player.uniqueId()); + public void onQuit(ServerSideConnectionEvent.Disconnect event) { + event.profile().ifPresent(profile -> { + plugin.removeFromWaiting(profile.uniqueId()); + sentAccept.remove(profile.uniqueId()); + }); } private void ensureMainThread(Runnable runnable) { diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/player/ForcePackSpongePlayer.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/player/ForcePackSpongePlayer.java index 6160854..53a0da8 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/player/ForcePackSpongePlayer.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/player/ForcePackSpongePlayer.java @@ -3,24 +3,47 @@ import com.convallyria.forcepack.api.check.DelayedSuccessSpoofCheck; import com.convallyria.forcepack.api.check.InvalidOrderSpoofCheck; import com.convallyria.forcepack.api.check.SpoofCheck; +import com.convallyria.forcepack.api.permission.Permissions; import com.convallyria.forcepack.api.player.ForcePackPlayer; import com.convallyria.forcepack.api.resourcepack.ResourcePack; +import com.convallyria.forcepack.api.utils.GeyserUtil; import com.convallyria.forcepack.sponge.ForcePackSponge; -import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.network.ServerSideConnection; +import org.spongepowered.api.profile.GameProfile; +import org.spongepowered.api.service.permission.PermissionService; +import org.spongepowered.api.service.permission.Subject; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; public class ForcePackSpongePlayer implements ForcePackPlayer { + private ServerSideConnection connection; + private final UUID uniqueId; private final Set waitingPacks = new HashSet<>(); private final List checks = new ArrayList<>(); - public ForcePackSpongePlayer(ServerPlayer player) { - checks.add(new DelayedSuccessSpoofCheck(ForcePackSponge.getAPI(), player.uniqueId())); - checks.add(new InvalidOrderSpoofCheck(ForcePackSponge.getAPI(), player.uniqueId())); + public ForcePackSpongePlayer(UUID uuid) { + this.uniqueId = uuid; + checks.add(new DelayedSuccessSpoofCheck(ForcePackSponge.getAPI(), uuid)); + checks.add(new InvalidOrderSpoofCheck(ForcePackSponge.getAPI(), uuid)); + } + + public ServerSideConnection getConnection() { + return connection; + } + + public void setConnection(ServerSideConnection connection) { + this.connection = connection; + } + + @Override + public UUID uniqueId() { + return uniqueId; } @Override @@ -32,4 +55,30 @@ public Set getWaitingPacks() { public List getChecks() { return checks; } + + public static boolean profileIsValid(ForcePackSponge plugin, GameProfile profile) { + final PermissionService permissionService = Sponge.server().serviceProvider().permissionService(); + Subject subject = permissionService.userSubjects().subject(profile.uniqueId().toString()).orElse(null); + if (subject == null) { + plugin.log("Defaulting to default subject permissions"); + subject = permissionService.defaults(); + } + + final boolean geyser = plugin.getConfig().node("Server", "geyser").getBoolean() + && GeyserUtil.isBedrockPlayer(profile.uniqueId()); + + // Bypass should be handled by velocity instead. + final boolean velocityMode = plugin.getConfig().node("velocity-mode").getBoolean(); + if (velocityMode) { + plugin.log("Velocity mode is enabled"); + } + + final boolean canBypass = !velocityMode + && subject.hasPermission(Permissions.BYPASS) + && plugin.getConfig().node("Server", "bypass-permission").getBoolean(); + + plugin.log(profile.name().orElseThrow() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); + + return !canBypass && !geyser; + } } diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/resourcepack/SpongeResourcePack.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/resourcepack/SpongeResourcePack.java index cae902e..9258837 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/resourcepack/SpongeResourcePack.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/resourcepack/SpongeResourcePack.java @@ -40,7 +40,7 @@ private void runSetResourcePack(UUID uuid) { final ServerPlayer player = Sponge.server().player(uuid).orElse(null); if (player == null) return; // Either the player disconnected or this is an NPC - spongePlugin.getForcePackPlayer(player).ifPresent(forcePackPlayer -> { + spongePlugin.getForcePackPlayer(uuid).ifPresent(forcePackPlayer -> { forcePackPlayer.getChecks().forEach(check -> check.sendPack(this)); }); @@ -48,6 +48,6 @@ private void runSetResourcePack(UUID uuid) { spongePlugin.getConfig().node("use-new-force-pack-screen").getBoolean(true), Component.join(JoinConfiguration.newlines(), Component.translatable("forcepack.prompt_text"))); - ProtocolUtil.sendPacketBypassingVia(player, send); + ProtocolUtil.sendPacketBypassingVia(uuid, send); } } diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/util/ProtocolUtil.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/util/ProtocolUtil.java index 4836792..98ab5a2 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/util/ProtocolUtil.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/util/ProtocolUtil.java @@ -8,15 +8,20 @@ import com.github.retrooper.packetevents.wrapper.PacketWrapper; import com.viaversion.viaversion.api.Via; import org.spongepowered.api.Sponge; -import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.UUID; public class ProtocolUtil { - public static int getProtocolVersion(ServerPlayer player) { + public static int getProtocolVersion(UUID player) { final boolean viaversion = Sponge.pluginManager().plugin("viaversion").isPresent(); - return viaversion - ? Via.getAPI().getPlayerVersion(player) - : PacketEvents.getAPI().getPlayerManager().getUser(player).getClientVersion().getProtocolVersion(); + if (viaversion) { + return Via.getAPI().getPlayerVersion(player); + } + + final Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(player); + final User user = PacketEvents.getAPI().getProtocolManager().getUser(channel); + return user.getClientVersion().getProtocolVersion(); } private static boolean warnedBadPlugin; @@ -24,8 +29,10 @@ public static int getProtocolVersion(ServerPlayer player) { /** * Sends a packet to a player bypassing viaversion. */ - public static void sendPacketBypassingVia(ServerPlayer player, PacketWrapper packet) { - final User user = PacketEvents.getAPI().getPlayerManager().getUser(player); + public static void sendPacketBypassingVia(UUID player, PacketWrapper packet) { + final Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(player); + final User user = PacketEvents.getAPI().getProtocolManager().getUser(channel); + // This can only ever happen if there is a bad plugin creating NPCs AND adding them to the online player map if (user == null) { if (!warnedBadPlugin) { @@ -37,7 +44,6 @@ public static void sendPacketBypassingVia(ServerPlayer player, PacketWrapper return; } - final Object channel = user.getChannel(); if (!ChannelHelper.isOpen(channel)) return; sendBypassingPacket(user, channel, packet); diff --git a/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java b/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java index 3d193bd..d338be4 100644 --- a/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java +++ b/velocity/src/main/java/com/convallyria/forcepack/velocity/handler/PackHandler.java @@ -154,7 +154,7 @@ private void removeFromWaiting(Player player, boolean resumeConfigurationPhase) private void addToWaiting(Player player, @NonNull ResourcePack pack) { waiting.compute(player.getUniqueId(), (a, existing) -> { - ForcePackPlayer newPlayer = existing != null ? existing : new ForcePackVelocityPlayer(plugin, player); + ForcePackPlayer newPlayer = existing != null ? existing : new ForcePackVelocityPlayer(plugin, player.getUniqueId()); newPlayer.getWaitingPacks().add(pack); return newPlayer; }); diff --git a/velocity/src/main/java/com/convallyria/forcepack/velocity/player/ForcePackVelocityPlayer.java b/velocity/src/main/java/com/convallyria/forcepack/velocity/player/ForcePackVelocityPlayer.java index f4823db..84a3a91 100644 --- a/velocity/src/main/java/com/convallyria/forcepack/velocity/player/ForcePackVelocityPlayer.java +++ b/velocity/src/main/java/com/convallyria/forcepack/velocity/player/ForcePackVelocityPlayer.java @@ -6,21 +6,28 @@ import com.convallyria.forcepack.api.player.ForcePackPlayer; import com.convallyria.forcepack.api.resourcepack.ResourcePack; import com.convallyria.forcepack.velocity.ForcePackVelocity; -import com.velocitypowered.api.proxy.Player; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; public class ForcePackVelocityPlayer implements ForcePackPlayer { + private final UUID uniqueId; private final Set waitingPacks = new HashSet<>(); private final List checks = new ArrayList<>(); - public ForcePackVelocityPlayer(ForcePackVelocity plugin, Player player) { - checks.add(new DelayedSuccessSpoofCheck(plugin, player.getUniqueId())); - checks.add(new InvalidOrderSpoofCheck(plugin, player.getUniqueId())); + public ForcePackVelocityPlayer(ForcePackVelocity plugin, UUID uuid) { + this.uniqueId = uuid; + checks.add(new DelayedSuccessSpoofCheck(plugin, uuid)); + checks.add(new InvalidOrderSpoofCheck(plugin, uuid)); + } + + @Override + public UUID uniqueId() { + return uniqueId; } @Override From cca7447e5a6b824128a19d58335c7a8a24174d09 Mon Sep 17 00:00:00 2001 From: SamB440 Date: Tue, 23 Jun 2026 20:59:39 +0100 Subject: [PATCH 4/4] paper: support velocity proxy configuration-phase resource pack statuses. Additionally implement a temporary fix for translations on 26.1+ servers. --- build-logic/build.gradle.kts | 2 +- paper/build.gradle.kts | 3 - .../forcepack/paper/ForcePackPaper.java | 57 ++++--- .../paper/command/ForcePackCommand.java | 8 +- .../MultiVersionResourcePackStatusEvent.java | 24 +-- .../paper/listener/ExemptionListener.java | 8 +- .../paper/listener/PacketListener.java | 116 ++++++++++++-- .../paper/listener/ResourcePackListener.java | 151 +++++++++++------- .../listener/VelocityMessageListener.java | 37 ----- .../paper/player/ForcePackPaperPlayer.java | 17 ++ .../paper/resourcepack/PaperResourcePack.java | 6 +- .../paper/translation/Translations.java | 83 ++++++---- .../forcepack/paper/util/GameProfile.java | 22 +++ .../forcepack/paper/util/ProtocolUtil.java | 22 ++- .../sponge/command/ForcePackCommand.java | 1 + 15 files changed, 346 insertions(+), 211 deletions(-) delete mode 100644 paper/src/main/java/com/convallyria/forcepack/paper/listener/VelocityMessageListener.java create mode 100644 paper/src/main/java/com/convallyria/forcepack/paper/util/GameProfile.java diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 1d6c3d4..c89b733 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -16,7 +16,7 @@ repositories { } dependencies { - implementation("com.gradleup.shadow:shadow-gradle-plugin:9.4.1") + implementation("com.gradleup.shadow:shadow-gradle-plugin:9.4.2") implementation("com.diffplug.spotless:spotless-plugin-gradle:8.4.0") implementation("com.modrinth.minotaur:com.modrinth.minotaur.gradle.plugin:2.+") } diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts index 4d79f75..989bd3b 100644 --- a/paper/build.gradle.kts +++ b/paper/build.gradle.kts @@ -54,9 +54,6 @@ dependencies { exclude("net.kyori", "adventure-api") // not up-to-date - use minimessage version } implementation("net.kyori:adventure-text-minimessage:4.25.0") - implementation("com.convallyria.languagy:api:3.0.3-SNAPSHOT") { - exclude("com.convallyria.languagy.libs") - } implementation("org.bstats:bstats-bukkit:3.0.2") implementation("com.github.retrooper:packetevents-spigot:2.12.2+6dca3cc-SNAPSHOT") } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java b/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java index 18a05f2..962364d 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/ForcePackPaper.java @@ -16,7 +16,6 @@ import com.convallyria.forcepack.paper.listener.ExemptionListener; import com.convallyria.forcepack.paper.listener.PacketListener; import com.convallyria.forcepack.paper.listener.ResourcePackListener; -import com.convallyria.forcepack.paper.listener.VelocityMessageListener; import com.convallyria.forcepack.paper.player.ForcePackPaperPlayer; import com.convallyria.forcepack.paper.resourcepack.PaperResourcePack; import com.convallyria.forcepack.paper.schedule.BukkitScheduler; @@ -24,9 +23,6 @@ import com.convallyria.forcepack.paper.util.ProtocolUtil; import com.convallyria.forcepack.webserver.ForcePackWebServer; import com.convallyria.forcepack.webserver.downloader.WebServerDependencyDownloader; -import com.convallyria.languagy.api.adventure.AdventurePlatform; -import com.convallyria.languagy.api.language.Language; -import com.convallyria.languagy.api.language.Translator; import com.github.retrooper.packetevents.PacketEvents; import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; import net.kyori.adventure.platform.bukkit.BukkitAudiences; @@ -35,7 +31,6 @@ import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerResourcePackStatusEvent; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; @@ -61,7 +56,7 @@ public final class ForcePackPaper extends JavaPlugin implements ForcePackPlatform { - private Translator translator; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); private PlatformScheduler scheduler; private final Map> resourcePacks = new HashMap<>(); public boolean velocityMode; @@ -70,6 +65,14 @@ public final class ForcePackPaper extends JavaPlugin implements ForcePackPlatfor public final Set temporaryExemptedPlayers = new HashSet<>(); + public MiniMessage miniMessage() { + return miniMessage; + } + + public BukkitAudiences adventure() { + return adventure; + } + @Override public Set getResourcePacks() { return resourcePacks.values().stream() @@ -77,7 +80,7 @@ public Set getResourcePacks() { .collect(Collectors.toCollection(LinkedHashSet::new)); } - public Set getPacksForVersion(Player player) { + public Set getPacksForVersion(UUID player) { final int protocolVersion = ProtocolUtil.getProtocolVersion(player); return getPacksForVersion(protocolVersion); } @@ -92,17 +95,16 @@ public boolean exemptNextResourcePackSend(UUID uuid) { return temporaryExemptedPlayers.add(uuid); } - private final Map waiting = new HashMap<>(); + private final Map waiting = new HashMap<>(); - public void processWaitingResourcePack(Player player, UUID packId) { - final UUID playerId = player.getUniqueId(); + public void processWaitingResourcePack(UUID player, UUID packId) { // If the player is on a version older than 1.20.3, they can only have one resource pack. if (ProtocolUtil.getProtocolVersion(player) < 765) { removeFromWaiting(player); return; } - final ForcePackPlayer newPlayer = waiting.computeIfPresent(playerId, (a, forcePackPlayer) -> { + final ForcePackPlayer newPlayer = waiting.computeIfPresent(player, (a, forcePackPlayer) -> { final Set packs = forcePackPlayer.getWaitingPacks(); packs.removeIf(pack -> pack.getUUID().equals(packId)); return forcePackPlayer; @@ -113,15 +115,15 @@ public void processWaitingResourcePack(Player player, UUID packId) { } } - public Optional getForcePackPlayer(Player player) { - return Optional.ofNullable(waiting.get(player.getUniqueId())); + public Optional getForcePackPlayer(UUID player) { + return Optional.ofNullable(waiting.get(player)); } - public boolean isWaiting(Player player) { - return waiting.containsKey(player.getUniqueId()); + public boolean isWaiting(UUID player) { + return waiting.containsKey(player); } - public boolean isWaitingFor(Player player, UUID packId) { + public boolean isWaitingFor(UUID player, UUID packId) { if (!isWaiting(player)) return false; // If the player is on a version older than 1.20.3, they can only have one resource pack. @@ -129,17 +131,17 @@ public boolean isWaitingFor(Player player, UUID packId) { return true; } - final Set waitingPacks = waiting.get(player.getUniqueId()).getWaitingPacks(); + final Set waitingPacks = waiting.get(player).getWaitingPacks(); return waitingPacks.stream().anyMatch(pack -> pack.getUUID().equals(packId)); } - public void removeFromWaiting(Player player) { - waiting.remove(player.getUniqueId()); + public void removeFromWaiting(UUID uuid) { + waiting.remove(uuid); } - public void addToWaiting(UUID uuid, @NonNull Set packs) { - waiting.compute(uuid, (a, existing) -> { - ForcePackPlayer newPlayer = existing != null ? existing : new ForcePackPaperPlayer(uuid); + public ForcePackPaperPlayer addToWaiting(UUID uuid, @NonNull Set packs) { + return waiting.compute(uuid, (a, existing) -> { + ForcePackPaperPlayer newPlayer = existing != null ? existing : new ForcePackPaperPlayer(uuid); newPlayer.getWaitingPacks().addAll(packs); return newPlayer; }); @@ -163,12 +165,10 @@ public void onEnable() { GeyserUtil.isGeyserInstalledHere = Bukkit.getPluginManager().getPlugin("Geyser-Spigot") != null; this.adventure = BukkitAudiences.create(this); - MiniMessage miniMessage = MiniMessage.miniMessage(); this.velocityMode = getConfig().getBoolean("velocity-mode"); this.scheduler = FoliaScheduler.RUNNING_FOLIA ? new FoliaScheduler(this) : new BukkitScheduler(this); this.registerListeners(); this.registerCommands(); - this.translator = Translator.of(this, "lang", Language.BRITISH_ENGLISH, debug(), AdventurePlatform.create(miniMessage, adventure)); PacketEvents.getAPI().init(); // Convert legacy config @@ -227,7 +227,6 @@ public void onEnable() { public void onDisable() { PacketEvents.getAPI().terminate(); if (webServer != null) webServer.shutdown(); - if (translator != null) translator.close(); if (this.adventure != null) { this.adventure.close(); this.adventure = null; @@ -362,8 +361,10 @@ private boolean checkPack(@Nullable ResourcePackVersion version, String url, boo private void registerListeners() { PluginManager pm = Bukkit.getPluginManager(); if (velocityMode) { + // Proxy resource pack statuses (play + configuration phase) are handled directly + // via PacketEvents in PacketListener, as Bukkit's plugin messaging only delivers + // during the play phase. getLogger().info("Enabled velocity listener"); - this.getServer().getMessenger().registerIncomingPluginChannel(this, "forcepack:status", new VelocityMessageListener(this)); } pm.registerEvents(new ResourcePackListener(this), this); @@ -450,10 +451,6 @@ private void checkForServerProperties() throws IOException { } } - public Translator getTranslator() { - return translator; - } - public boolean debug() { return getConfig().getBoolean("Server.debug"); } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/command/ForcePackCommand.java b/paper/src/main/java/com/convallyria/forcepack/paper/command/ForcePackCommand.java index b165106..9c43fd7 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/command/ForcePackCommand.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/command/ForcePackCommand.java @@ -48,7 +48,7 @@ public void onReload(CommandSender sender, Bukkit.getPluginManager().callEvent(new ForcePackReloadEvent()); if (!plugin.velocityMode && send) { for (Player player : Bukkit.getOnlinePlayers()) { - if (plugin.isWaiting(player)) continue; + if (plugin.isWaiting(player.getUniqueId())) continue; boolean geyser = plugin.getConfig().getBoolean("Server.geyser") && GeyserUtil.isBedrockPlayer(player.getUniqueId()); boolean canBypass = player.hasPermission(Permissions.BYPASS) && plugin.getConfig().getBoolean("Server.bypass-permission"); plugin.log(player.getName() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); @@ -56,10 +56,10 @@ public void onReload(CommandSender sender, Translations.RELOADING.send(player); - final Set resourcePacks = plugin.getPacksForVersion(player); + final Set resourcePacks = plugin.getPacksForVersion(player.getUniqueId()); plugin.addToWaiting(player.getUniqueId(), resourcePacks); - if (ProtocolUtil.getProtocolVersion(player) >= 765) { // 1.20.3+ - ProtocolUtil.sendPacketBypassingVia(player, new WrapperPlayServerResourcePackRemove((UUID) null)); + if (ProtocolUtil.getProtocolVersion(player.getUniqueId()) >= 765) { // 1.20.3+ + ProtocolUtil.sendPacketBypassingVia(player.getUniqueId(), new WrapperPlayServerResourcePackRemove((UUID) null)); } resourcePacks.forEach(pack -> pack.setResourcePack(player.getUniqueId())); } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/event/MultiVersionResourcePackStatusEvent.java b/paper/src/main/java/com/convallyria/forcepack/paper/event/MultiVersionResourcePackStatusEvent.java index 302b020..916fa81 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/event/MultiVersionResourcePackStatusEvent.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/event/MultiVersionResourcePackStatusEvent.java @@ -1,7 +1,7 @@ package com.convallyria.forcepack.paper.event; +import com.convallyria.forcepack.paper.util.GameProfile; import net.kyori.adventure.resource.ResourcePackStatus; -import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; @@ -12,35 +12,35 @@ import java.util.UUID; /** - * Like {@link PlayerResourcePackStatusEvent} but helps with multiple version support + * Like {@link PlayerResourcePackStatusEvent} but helps with multiple version support. + *

+ * Unlike the native event, this event may be fired during the configuration phase, where no + * {@link org.bukkit.entity.Player} object exists yet. For that reason it carries the player's profile instead. */ public class MultiVersionResourcePackStatusEvent extends Event implements Cancellable { private static final HandlerList HANDLER_LIST = new HandlerList(); - private final Player player; + private final GameProfile profile; private final UUID id; private final ResourcePackStatus status; private final boolean proxy; private final boolean proxyRemove; private boolean cancel; - public MultiVersionResourcePackStatusEvent(@NotNull final Player who, @NotNull UUID id, @NotNull ResourcePackStatus resourcePackStatus, boolean proxy, boolean proxyRemove) { + public MultiVersionResourcePackStatusEvent(@NotNull final GameProfile profile, + @NotNull UUID id, @NotNull ResourcePackStatus resourcePackStatus, + boolean proxy, boolean proxyRemove) { super(true); - this.player = who; + this.profile = profile; this.id = id; this.status = resourcePackStatus; this.proxy = proxy; this.proxyRemove = proxyRemove; } - /** - * Returns the player involved in this event - * @return Player who is involved in this event - */ - @NotNull - public final Player getPlayer() { - return player; + public GameProfile getProfile() { + return profile; } /** diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/listener/ExemptionListener.java b/paper/src/main/java/com/convallyria/forcepack/paper/listener/ExemptionListener.java index 6ca13a4..e169e83 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/listener/ExemptionListener.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/listener/ExemptionListener.java @@ -22,7 +22,7 @@ public void onDamageByEntity(EntityDamageByEntityEvent event) { if (event.getEntity() instanceof Player) { Player player = (Player) event.getEntity(); - if (plugin.isWaiting(player)) { + if (plugin.isWaiting(player.getUniqueId())) { event.setCancelled(true); plugin.log("Cancelled damage for player '" + player.getName() + "' due to resource pack not applied."); } @@ -30,7 +30,7 @@ public void onDamageByEntity(EntityDamageByEntityEvent event) { if (event.getDamager() instanceof Player) { Player damager = (Player) event.getDamager(); - if (plugin.isWaiting(damager)) { + if (plugin.isWaiting(damager.getUniqueId())) { event.setCancelled(true); plugin.log("Cancelled damage for damager '" + damager.getName() + "' due to resource pack not applied."); } @@ -43,7 +43,7 @@ public void onDamage(EntityDamageEvent event) { if (event.getEntity() instanceof Player) { Player damaged = (Player) event.getEntity(); - if (plugin.isWaiting(damaged)) { + if (plugin.isWaiting(damaged.getUniqueId())) { event.setCancelled(true); plugin.log("Cancelled damage for player '" + damaged.getName() + "' due to resource pack not applied."); } @@ -55,7 +55,7 @@ public void onMove(PlayerMoveEvent event) { if (!plugin.getConfig().getBoolean("prevent-movement")) return; final Player player = event.getPlayer(); - if (plugin.isWaiting(player)) { + if (plugin.isWaiting(player.getUniqueId())) { event.setCancelled(true); plugin.log("Cancelled movement for player '" + player.getName() + "' due to resource pack not applied."); } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/listener/PacketListener.java b/paper/src/main/java/com/convallyria/forcepack/paper/listener/PacketListener.java index 04a3232..559c9de 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/listener/PacketListener.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/listener/PacketListener.java @@ -2,18 +2,31 @@ import com.convallyria.forcepack.paper.ForcePackPaper; import com.convallyria.forcepack.paper.event.MultiVersionResourcePackStatusEvent; +import com.convallyria.forcepack.paper.player.ForcePackPaperPlayer; +import com.convallyria.forcepack.paper.util.GameProfile; +import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketListenerAbstract; import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; +import com.github.retrooper.packetevents.manager.server.ServerVersion; import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.configuration.client.WrapperConfigClientPluginMessage; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPluginMessage; import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientResourcePackStatus; import net.kyori.adventure.resource.ResourcePackStatus; import org.bukkit.Bukkit; -import org.bukkit.entity.Player; +import java.nio.charset.StandardCharsets; +import java.util.Set; import java.util.UUID; public class PacketListener extends PacketListenerAbstract { + private static final String STATUS_CHANNEL = "forcepack:status"; + private final ForcePackPaper plugin; public PacketListener(ForcePackPaper plugin) { @@ -27,21 +40,92 @@ public boolean isPreVia() { @Override public void onPacketReceive(PacketReceiveEvent event) { - if (event.getPacketType() == PacketType.Play.Client.RESOURCE_PACK_STATUS) { - final Player player = Bukkit.getPlayer(event.getUser().getUUID()); - if (player == null) { - plugin.getLogger().warning("Unable to get player for resource pack status!?!? " + event.getUser() + ", " + event.getPlayer()); - return; - } - - plugin.log("Received packet resource pack status from " + player.getName() + " (version: " + event.getServerVersion().getReleaseName() + ")"); - - final WrapperPlayClientResourcePackStatus status = new WrapperPlayClientResourcePackStatus(event); - final WrapperPlayClientResourcePackStatus.Result result = status.getResult(); - final UUID packId = status.getPackId(); - final MultiVersionResourcePackStatusEvent packEvent = new MultiVersionResourcePackStatusEvent(player, packId, ResourcePackStatus.valueOf(result.name()), false, false); - Bukkit.getPluginManager().callEvent(packEvent); - event.setCancelled(packEvent.isCancelled()); + final PacketTypeCommon type = event.getPacketType(); + if (isResourcePackStatus(type)) { + handleResourcePackStatus(event); + } else if (isPluginMessage(type)) { + handlePluginMessage(event); + } + } + + @Override + public void onPacketSend(PacketSendEvent event) { + if (event.getPacketType() == PacketType.Login.Server.LOGIN_SUCCESS + && PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2)) { + // We blindly add here since we can't check permissions this early on Paper + plugin.addToWaiting(event.getUser().getUUID(), Set.of()); + } + } + + // When a player disconnects, remove them from the waiting list + // This is the only way to accurately detect disconnecting on both legacy and modern Paper + @Override + public void onUserDisconnect(UserDisconnectEvent event) { + final User user = event.getUser(); + plugin.removeFromWaiting(user.getUUID()); + } + + private void handleResourcePackStatus(PacketReceiveEvent event) { + final User user = event.getUser(); + final ForcePackPaperPlayer player = plugin.getForcePackPlayer(user.getUUID()).orElse(null); + if (player == null) { + // Player isn't valid - wasn't added to waiting (exempt or not yet tracked) + return; } + + plugin.log("Received packet resource pack status from " + user.getName() + " (version: " + event.getServerVersion().getReleaseName() + ")"); + + final WrapperPlayClientResourcePackStatus status = new WrapperPlayClientResourcePackStatus(event); + final WrapperPlayClientResourcePackStatus.Result result = status.getResult(); + final UUID packId = status.getPackId(); + final GameProfile gameProfile = new GameProfile(user.getUUID(), user.getName()); + final MultiVersionResourcePackStatusEvent packEvent = new MultiVersionResourcePackStatusEvent(gameProfile, packId, ResourcePackStatus.valueOf(result.name()), false, false); + Bukkit.getPluginManager().callEvent(packEvent); + event.setCancelled(packEvent.isCancelled()); + } + + private void handlePluginMessage(PacketReceiveEvent event) { + if (!plugin.velocityMode) return; + + final String channel; + final byte[] data; + if (event.getPacketType() == PacketType.Configuration.Client.PLUGIN_MESSAGE) { + final WrapperConfigClientPluginMessage wrapper = new WrapperConfigClientPluginMessage(event); + channel = wrapper.getChannelName(); + data = wrapper.getData(); + } else { + final WrapperPlayClientPluginMessage wrapper = new WrapperPlayClientPluginMessage(event); + channel = wrapper.getChannelName(); + data = wrapper.getData(); + } + + if (!STATUS_CHANNEL.equals(channel)) return; + + // The proxy sends us this on a custom channel - don't let it propagate any further. + event.setCancelled(true); + + final User user = event.getUser(); + final ForcePackPaperPlayer player = plugin.getForcePackPlayer(user.getUUID()).orElse(null); + if (player == null) { + // Player isn't valid - wasn't added to waiting (exempt or not yet tracked) + return; + } + + final String[] split = new String(data, StandardCharsets.UTF_8).split(";"); + plugin.log("Posted event"); + + final ResourcePackStatus status = ResourcePackStatus.valueOf(split[1]); + final UUID packId = UUID.fromString(split[0]); + final boolean proxyRemove = Boolean.parseBoolean(split[2]); + final GameProfile gameProfile = new GameProfile(user.getUUID(), user.getName()); + Bukkit.getPluginManager().callEvent(new MultiVersionResourcePackStatusEvent(gameProfile, packId, status, true, proxyRemove)); + } + + private boolean isResourcePackStatus(PacketTypeCommon type) { + return type == PacketType.Play.Client.RESOURCE_PACK_STATUS || type == PacketType.Configuration.Client.RESOURCE_PACK_STATUS; + } + + private boolean isPluginMessage(PacketTypeCommon type) { + return type == PacketType.Play.Client.PLUGIN_MESSAGE || type == PacketType.Configuration.Client.PLUGIN_MESSAGE; } } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/listener/ResourcePackListener.java b/paper/src/main/java/com/convallyria/forcepack/paper/listener/ResourcePackListener.java index bf51c59..9a6498d 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/listener/ResourcePackListener.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/listener/ResourcePackListener.java @@ -2,7 +2,6 @@ import com.convallyria.forcepack.api.check.SpoofCheck; import com.convallyria.forcepack.api.permission.Permissions; -import com.convallyria.forcepack.api.player.ForcePackPlayer; import com.convallyria.forcepack.api.resourcepack.ResourcePack; import com.convallyria.forcepack.api.schedule.PlatformScheduler; import com.convallyria.forcepack.api.utils.ClientVersion; @@ -10,9 +9,16 @@ import com.convallyria.forcepack.paper.ForcePackPaper; import com.convallyria.forcepack.paper.event.ForcePackReloadEvent; import com.convallyria.forcepack.paper.event.MultiVersionResourcePackStatusEvent; +import com.convallyria.forcepack.paper.player.ForcePackPaperPlayer; import com.convallyria.forcepack.paper.translation.Translations; +import com.convallyria.forcepack.paper.util.GameProfile; import com.convallyria.forcepack.paper.util.ProtocolUtil; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; import net.kyori.adventure.resource.ResourcePackStatus; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.title.Title; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; @@ -41,36 +47,32 @@ public ResourcePackListener(final ForcePackPaper plugin) { @EventHandler public void onStatus(MultiVersionResourcePackStatusEvent event) { - final Player player = event.getPlayer(); - final UUID id = event.getID(); + final GameProfile profile = event.getProfile(); + final UUID uuid = profile.uniqueId(); - boolean geyser = plugin.getConfig().getBoolean("Server.geyser") && GeyserUtil.isBedrockPlayer(player.getUniqueId()); - boolean canBypass = player.hasPermission(Permissions.BYPASS) - && getConfig().getBoolean("Server.bypass-permission") - // Bypass should be handled by velocity instead. - && !plugin.velocityMode; - plugin.log(player.getName() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); - - if (canBypass || geyser) { + if (!plugin.isWaiting(uuid)) { + // Player isn't valid - wasn't added to waiting (exempt or not yet tracked) return; } - if (plugin.temporaryExemptedPlayers.remove(player.getUniqueId())) { - plugin.log("Ignoring player " + player.getName() + " as they have a one-off exemption."); + final UUID packId = event.getID(); + + if (plugin.temporaryExemptedPlayers.remove(uuid)) { + plugin.log("Ignoring player " + uuid + " as they have a one-off exemption."); return; } final ResourcePackStatus status = event.getStatus(); - plugin.log(player.getName() + " sent status: " + status); + plugin.log(uuid + " sent status: " + status); - if (!plugin.velocityMode && tryValidateHacks(player, event, status)) return; + if (!plugin.velocityMode && tryValidateHacks(profile, event, status)) return; // If we did not set this resource pack, ignore - if (!event.isProxy() && !plugin.isWaitingFor(player, id)) { - plugin.log("Ignoring resource pack " + id + " because it wasn't set by ForcePack."); + if (!event.isProxy() && !plugin.isWaitingFor(uuid, packId)) { + plugin.log("Ignoring resource pack " + packId + " because it wasn't set by ForcePack."); return; } else if (event.isProxy()) { - plugin.log("Resource pack with id " + id + " sent by proxy."); + plugin.log("Resource pack with id " + packId + " sent by proxy. Removal state: " + event.isProxyRemove()); } // Only remove from waiting if they actually loaded the resource pack, rather than any status @@ -78,70 +80,81 @@ && getConfig().getBoolean("Server.bypass-permission") if (status != ResourcePackStatus.ACCEPTED && status != ResourcePackStatus.DOWNLOADED) { if (event.isProxy()) { if (event.isProxyRemove()) { - plugin.removeFromWaiting(player); + plugin.removeFromWaiting(uuid); } } else { - plugin.processWaitingResourcePack(player, id); + plugin.processWaitingResourcePack(uuid, packId); } } for (String cmd : getConfig().getStringList("Server.Actions." + status.name() + ".Commands")) { - ensureMainThread(() -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("[player]", player.getName()))); + ensureMainThread(() -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("[player]", profile.name()))); } // Don't execute kicks - handled by proxy if (plugin.velocityMode) { - plugin.getScheduler().executeOnMain(() -> this.callBukkitEvent(event)); + plugin.getScheduler().executeOnMain(() -> this.callBukkitEvent(profile, event)); return; } + final ForcePackPaperPlayer forcePackPlayer = plugin.getForcePackPlayer(uuid).orElseThrow(); + final boolean kick = getConfig().getBoolean("Server.Actions." + status.name() + ".kick"); switch (status) { case ACCEPTED: { - sentAccept.put(player.getUniqueId(), System.currentTimeMillis()); + sentAccept.put(uuid, System.currentTimeMillis()); break; } case DECLINED: { - ensureMainThread(() -> { - if (kick) player.kickPlayer(Translations.DECLINED.get(player)); - else Translations.DECLINED.send(player); - }); + if (kick) { + forcePackPlayer.closeConnection(Component.join(JoinConfiguration.newlines(), Translations.DECLINED.get())); + } else { + ensureMainThread(() -> { + forcePackPlayer.player().ifPresent(Translations.DECLINED::send); + }); + } - sentAccept.remove(player.getUniqueId()); + sentAccept.remove(uuid); break; } case DISCARDED: case INVALID_URL: case FAILED_RELOAD: case FAILED_DOWNLOAD: { - ensureMainThread(() -> { - if (kick) player.kickPlayer(Translations.DOWNLOAD_FAILED.get(player)); - else Translations.DOWNLOAD_FAILED.send(player); - }); + if (kick) { + forcePackPlayer.closeConnection(Component.join(JoinConfiguration.newlines(), Translations.DOWNLOAD_FAILED.get())); + } else { + ensureMainThread(() -> forcePackPlayer.player().ifPresent(Translations.DOWNLOAD_FAILED::send)); + } - sentAccept.remove(player.getUniqueId()); + sentAccept.remove(uuid); break; } case SUCCESSFULLY_LOADED: { - if (kick) ensureMainThread(() -> player.kickPlayer(Translations.ACCEPTED.get(player))); - else { - ensureMainThread(() -> Translations.ACCEPTED.send(player)); - boolean sendTitle = plugin.getConfig().getBoolean("send-loading-title"); - if (sendTitle) player.sendTitle(null, null, 0, 0, 0); // resetTitle doesn't clear subtitle + if (kick) { + forcePackPlayer.closeConnection(Component.join(JoinConfiguration.newlines(), Translations.ACCEPTED.get())); + } else { + ensureMainThread(() -> { + forcePackPlayer.player().ifPresent(player -> { + Translations.ACCEPTED.send(player); + boolean sendTitle = plugin.getConfig().getBoolean("send-loading-title"); + if (sendTitle) player.sendTitle(null, null, 0, 0, 0); // resetTitle doesn't clear subtitle + }); + }); } break; } } } - private boolean tryValidateHacks(Player player, MultiVersionResourcePackStatusEvent event, ResourcePackStatus status) { + private boolean tryValidateHacks(GameProfile profile, MultiVersionResourcePackStatusEvent event, ResourcePackStatus status) { final boolean tryPrevent = getConfig().getBoolean("try-to-stop-fake-accept-hacks", true); if (!tryPrevent) return false; - final ForcePackPlayer forcePackPlayer = plugin.getForcePackPlayer(player).orElse(null); + final ForcePackPaperPlayer forcePackPlayer = plugin.getForcePackPlayer(profile.uniqueId()).orElse(null); if (forcePackPlayer == null) { - plugin.log("Not checking " + player.getName() + " because they are not in waiting."); + plugin.log("Not checking " + profile.name() + " because they are not in waiting."); return false; } @@ -157,8 +170,8 @@ private boolean tryValidateHacks(Player player, MultiVersionResourcePackStatusEv } if (hasFailed) { - plugin.log("Kicking player " + player.getName() + " because they failed a check."); - ensureMainThread(() -> player.kickPlayer(Translations.DOWNLOAD_FAILED.get(player))); + plugin.log("Kicking player " + profile.name() + " because they failed a check."); + forcePackPlayer.closeConnection(Component.empty()); } return hasFailed; @@ -178,7 +191,13 @@ private boolean tryValidateHacks(Player player, MultiVersionResourcePackStatusEv LEGACY_CONSTRUCTOR = constructor; } - private void callBukkitEvent(MultiVersionResourcePackStatusEvent event) { + private void callBukkitEvent(GameProfile profile, MultiVersionResourcePackStatusEvent event) { + final Player player = Bukkit.getPlayer(profile.uniqueId()); + if (player == null) { + // Paper doesn't support configuration phase resource pack events. Nice. + return; + } + // Velocity doesn't correctly pass things to the backend server // Call bukkit event manually for other plugins to handle status events PlayerResourcePackStatusEvent.Status bukkitStatus; @@ -193,11 +212,11 @@ private void callBukkitEvent(MultiVersionResourcePackStatusEvent event) { try { // Can the ID be null? I'm not sure, let's just pass a random ID to avoid errors if this happens. // I doubt another plugin is using it anyway. - bukkit = new PlayerResourcePackStatusEvent(event.getPlayer(), event.getID() == null ? UUID.randomUUID() : event.getID(), bukkitStatus); + bukkit = new PlayerResourcePackStatusEvent(player, event.getID() == null ? UUID.randomUUID() : event.getID(), bukkitStatus); } catch (NoSuchMethodError ignored) { // We are on a server version that doesn't have resource pack UUIDs. try { - bukkit = LEGACY_CONSTRUCTOR.newInstance(event.getPlayer(), bukkitStatus); + bukkit = LEGACY_CONSTRUCTOR.newInstance(player, bukkitStatus); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } @@ -206,36 +225,48 @@ private void callBukkitEvent(MultiVersionResourcePackStatusEvent event) { Bukkit.getPluginManager().callEvent(bukkit); } + private static final boolean HAS_CONFIGURATION_PHASE = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2); + @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); if (player.hasMetadata("NPC") || player.hasMetadata("fake-player")) return; - boolean geyser = plugin.getConfig().getBoolean("Server.geyser") && GeyserUtil.isBedrockPlayer(player.getUniqueId()); + final UUID uuid = player.getUniqueId(); + boolean geyser = plugin.getConfig().getBoolean("Server.geyser") && GeyserUtil.isBedrockPlayer(uuid); boolean canBypass = player.hasPermission(Permissions.BYPASS) && getConfig().getBoolean("Server.bypass-permission") && !plugin.velocityMode; plugin.log(player.getName() + "'s exemptions: geyser, " + geyser + ". permission, " + canBypass + "."); - if (canBypass || geyser) return; + if (canBypass || geyser) { + if (HAS_CONFIGURATION_PHASE) { + // Player was added to waiting in configuration start phase, but we can now check their permissions and they shouldn't be waiting + plugin.removeFromWaiting(uuid); + } + return; + } if (plugin.velocityMode) { plugin.log("Velocity mode is enabled"); - plugin.addToWaiting(player.getUniqueId(), Set.of()); + // Player was already added in configuration start phase, don't add again as we might have already handled resource packs + if (!HAS_CONFIGURATION_PHASE) { + plugin.addToWaiting(uuid, Set.of()); + } return; } - final Set packs = plugin.getPacksForVersion(player); + final Set packs = plugin.getPacksForVersion(uuid); if (packs.isEmpty()) { plugin.log("Warning: Packs for player " + player.getName() + " are empty."); return; } - plugin.addToWaiting(player.getUniqueId(), packs); + plugin.addToWaiting(uuid, packs); for (ResourcePack pack : packs) { plugin.log("Sending pack " + pack.getUUID() + " to player " + player.getName()); - final int version = ProtocolUtil.getProtocolVersion(player); + final int version = ProtocolUtil.getProtocolVersion(uuid); final int maxSize = ClientVersion.getMaxSizeForVersion(version); final boolean forceSend = getConfig().getBoolean("Server.force-invalid-size"); if (!forceSend && pack.getSize() > maxSize) { @@ -250,26 +281,28 @@ && getConfig().getBoolean("Server.bypass-permission") @EventHandler public void onReload(ForcePackReloadEvent event) { for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { - if (plugin.isWaiting(onlinePlayer)) continue; + if (plugin.isWaiting(onlinePlayer.getUniqueId())) continue; sentAccept.remove(onlinePlayer.getUniqueId()); } } private void runSetPackTask(Player player, ResourcePack pack, int version) { + final UUID uuid = player.getUniqueId(); AtomicReference task = new AtomicReference<>(); Runnable packTask = () -> { - if (plugin.isWaiting(player)) { + if (plugin.isWaiting(uuid)) { plugin.log("Sent resource pack to player"); - pack.setResourcePack(player.getUniqueId()); + pack.setResourcePack(uuid); } boolean sendTitle = plugin.getConfig().getBoolean("send-loading-title"); - if (sendTitle && sentAccept.containsKey(player.getUniqueId())) { - player.sendTitle(Translations.DOWNLOAD_START_TITLE.get(player), Translations.DOWNLOAD_START_SUBTITLE.get(player), 0, 30, 10); + if (sendTitle && sentAccept.containsKey(uuid)) { + plugin.adventure().player(player).showTitle(Title.title(Translations.DOWNLOAD_START_TITLE.get().get(0), + Translations.DOWNLOAD_START_SUBTITLE.get().get(0), 0, 30, 10)); } final PlatformScheduler.ForcePackTask acquired = task.get(); - if (acquired != null && !plugin.isWaiting(player) && !sentAccept.containsKey(player.getUniqueId())) { + if (acquired != null && !plugin.isWaiting(uuid) && !sentAccept.containsKey(uuid)) { acquired.cancel(); } }; @@ -284,7 +317,7 @@ private void runSetPackTask(Player player, ResourcePack pack, int version) { @EventHandler public void onQuit(PlayerQuitEvent pqe) { Player player = pqe.getPlayer(); - plugin.removeFromWaiting(player); + plugin.removeFromWaiting(player.getUniqueId()); sentAccept.remove(player.getUniqueId()); plugin.temporaryExemptedPlayers.remove(player.getUniqueId()); } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/listener/VelocityMessageListener.java b/paper/src/main/java/com/convallyria/forcepack/paper/listener/VelocityMessageListener.java deleted file mode 100644 index 8486032..0000000 --- a/paper/src/main/java/com/convallyria/forcepack/paper/listener/VelocityMessageListener.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.convallyria.forcepack.paper.listener; - -import com.convallyria.forcepack.paper.ForcePackPaper; -import com.convallyria.forcepack.paper.event.MultiVersionResourcePackStatusEvent; -import net.kyori.adventure.resource.ResourcePackStatus; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerResourcePackStatusEvent; -import org.bukkit.plugin.messaging.PluginMessageListener; - -import java.util.UUID; - -public class VelocityMessageListener implements PluginMessageListener { - - private static final String CHANNEL = "forcepack:status"; - - private final ForcePackPaper plugin; - - public VelocityMessageListener(final ForcePackPaper plugin) { - this.plugin = plugin; - } - - @Override - public void onPluginMessageReceived(String channel, Player player, byte[] message) { - if (!channel.equals(CHANNEL)) { - plugin.log("Ignoring channel " + channel); - return; - } - - final String data = new String(message); - final String[] split = data.split(";"); - plugin.log("Posted event"); - - final PlayerResourcePackStatusEvent.Status status = PlayerResourcePackStatusEvent.Status.valueOf(split[1]); - plugin.getScheduler().executeAsync(() -> Bukkit.getPluginManager().callEvent(new MultiVersionResourcePackStatusEvent(player, UUID.fromString(split[0]), ResourcePackStatus.valueOf(status.name()), true, Boolean.parseBoolean(split[2])))); - } -} diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java b/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java index e7e5d1f..0d1a180 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/player/ForcePackPaperPlayer.java @@ -6,11 +6,17 @@ import com.convallyria.forcepack.api.player.ForcePackPlayer; import com.convallyria.forcepack.api.resourcepack.ResourcePack; import com.convallyria.forcepack.paper.ForcePackPaper; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDisconnect; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -26,6 +32,17 @@ public ForcePackPaperPlayer(UUID uuid) { checks.add(new InvalidOrderSpoofCheck(ForcePackPaper.getAPI(), uuid)); } + public void closeConnection(Component reason) { + final Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(uniqueId); + final User user = PacketEvents.getAPI().getProtocolManager().getUser(channel); + user.writePacket(new WrapperPlayServerDisconnect(reason)); + user.closeConnection(); + } + + public Optional player() { + return Optional.ofNullable(Bukkit.getPlayer(uniqueId)); + } + @Override public UUID uniqueId() { return uniqueId; diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/resourcepack/PaperResourcePack.java b/paper/src/main/java/com/convallyria/forcepack/paper/resourcepack/PaperResourcePack.java index c6e45f1..ae6846c 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/resourcepack/PaperResourcePack.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/resourcepack/PaperResourcePack.java @@ -41,14 +41,14 @@ private void runSetResourcePack(UUID uuid) { final Player player = Bukkit.getPlayer(uuid); if (player == null) return; // Either the player disconnected or this is an NPC - paperPlugin.getForcePackPlayer(player).ifPresent(forcePackPlayer -> { + paperPlugin.getForcePackPlayer(uuid).ifPresent(forcePackPlayer -> { forcePackPlayer.getChecks().forEach(check -> check.sendPack(this)); }); WrapperPlayServerResourcePackSend send = new WrapperPlayServerResourcePackSend(getUUID(), url, getHash(), paperPlugin.getConfig().getBoolean("use-new-force-pack-screen", true), - Component.join(JoinConfiguration.newlines(), Translations.PROMPT_TEXT.getProper(player))); + Component.join(JoinConfiguration.newlines(), Translations.PROMPT_TEXT.get())); - ProtocolUtil.sendPacketBypassingVia(player, send); + ProtocolUtil.sendPacketBypassingVia(uuid, send); } } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/translation/Translations.java b/paper/src/main/java/com/convallyria/forcepack/paper/translation/Translations.java index 41877ad..73607a5 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/translation/Translations.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/translation/Translations.java @@ -1,78 +1,93 @@ package com.convallyria.forcepack.paper.translation; import com.convallyria.forcepack.paper.ForcePackPaper; -import com.convallyria.languagy.api.language.Language; -import com.convallyria.languagy.api.language.key.TranslationKey; -import com.convallyria.languagy.api.language.translation.Translation; -import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer; +import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import java.io.File; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; public enum Translations { - DECLINED(TranslationKey.of("declined")), - ACCEPTED(TranslationKey.of("accepted")), - DOWNLOAD_START_TITLE(TranslationKey.of("download_start_title")), - DOWNLOAD_START_SUBTITLE(TranslationKey.of("download_start_subtitle")), - DOWNLOAD_FAILED(TranslationKey.of("download_failed")), - PROMPT_TEXT(TranslationKey.of("prompt_text")), - RELOADING(TranslationKey.of("reloading")); + DECLINED(Component.translatable("declined")), + ACCEPTED(Component.translatable("accepted")), + DOWNLOAD_START_TITLE(Component.translatable("download_start_title")), + DOWNLOAD_START_SUBTITLE(Component.translatable("download_start_subtitle")), + DOWNLOAD_FAILED(Component.translatable("download_failed")), + PROMPT_TEXT(Component.translatable("prompt_text")), + RELOADING(Component.translatable("reloading")); - private final TranslationKey key; + private final TranslatableComponent key; - Translations(TranslationKey key) { + Translations(TranslatableComponent key) { this.key = key; } - public TranslationKey getKey() { - return key; + private String getPath() { + return this.key.key(); } public void send(Player player, Object... values) { - Translation message = getTranslation(player); - message.format(values); - message.send(); + get(values).forEach(component -> { + ForcePackPaper.getInstance().adventure().player(player).sendMessage(component); + }); } - public List getProper(Player player) { - return getTranslation(player).colour(); + public void send(Audience audience, Object... values) { + get(values).forEach(audience::sendMessage); } - @Deprecated - public String get(Player player) { - return BukkitComponentSerializer.legacy().serialize(getTranslation(player).colour().get(0)); - } + public List get(Object... values) { + // TODO: This is a temporary solution until locale is implemented + final Object translationObject = defaultConfig.get(getPath()); + List toTranslate = new ArrayList<>(); + if (translationObject instanceof String) { + toTranslate.add(String.format((String) translationObject, values)); + } else if (translationObject instanceof List) { + for (String string : (List) translationObject) { + toTranslate.add(String.format(string, values)); + } + } - public Translation getTranslation(Player player) { - return ForcePackPaper.getInstance().getTranslator().getTranslationFor(player, key); + List components = new ArrayList<>(toTranslate.size()); + for (String string : toTranslate) { + components.add(ForcePackPaper.getInstance().miniMessage().deserialize(string)); + } + + return components; } + static FileConfiguration defaultConfig; + public static void generateLang(ForcePackPaper plugin) { File lang = new File(plugin.getDataFolder() + "/lang/"); lang.mkdirs(); - for (Language language : Language.values()) { + for (Locale locale : Locale.getAvailableLocales()) { + final File target = new File(lang + File.separator + locale.toString() + ".yml"); try { - if (!new File(lang + File.separator + language.getKey().getCode() + ".yml").exists()) { - plugin.saveResource("lang/" + language.getKey().getCode() + ".yml", false); + if (!target.exists()) { + plugin.saveResource("lang/" + locale + ".yml", false); } - plugin.getLogger().info("Generated " + language.getKey().getCode() + ".yml"); + plugin.getLogger().info("Generated " + locale + ".yml"); } catch (IllegalArgumentException ignored) { } - File file = new File(plugin.getDataFolder() + "/lang/" + language.getKey().getCode() + ".yml"); - if (file.exists()) { - FileConfiguration config = YamlConfiguration.loadConfiguration(file); + if (target.exists()) { + FileConfiguration config = YamlConfiguration.loadConfiguration(target); for (Translations key : values()) { - if (config.get(key.toString().toLowerCase()) == null) { + if (config.get(key.getPath()) == null) { plugin.getLogger().warning("No value in translation file for key " + key + " was found. Regenerate language files?"); } } } } + + defaultConfig = YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder() + "/lang/en_GB.yml")); } } diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/util/GameProfile.java b/paper/src/main/java/com/convallyria/forcepack/paper/util/GameProfile.java new file mode 100644 index 0000000..b04bcfe --- /dev/null +++ b/paper/src/main/java/com/convallyria/forcepack/paper/util/GameProfile.java @@ -0,0 +1,22 @@ +package com.convallyria.forcepack.paper.util; + +import java.util.UUID; + +public final class GameProfile { + + private final UUID uuid; + private final String name; + + public GameProfile(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + } + + public UUID uniqueId() { + return uuid; + } + + public String name() { + return name; + } +} diff --git a/paper/src/main/java/com/convallyria/forcepack/paper/util/ProtocolUtil.java b/paper/src/main/java/com/convallyria/forcepack/paper/util/ProtocolUtil.java index d4cce64..45bd733 100644 --- a/paper/src/main/java/com/convallyria/forcepack/paper/util/ProtocolUtil.java +++ b/paper/src/main/java/com/convallyria/forcepack/paper/util/ProtocolUtil.java @@ -8,15 +8,20 @@ import com.github.retrooper.packetevents.wrapper.PacketWrapper; import com.viaversion.viaversion.api.Via; import org.bukkit.Bukkit; -import org.bukkit.entity.Player; + +import java.util.UUID; public class ProtocolUtil { - public static int getProtocolVersion(Player player) { + public static int getProtocolVersion(UUID player) { final boolean viaversion = Bukkit.getPluginManager().isPluginEnabled("ViaVersion"); - return viaversion - ? Via.getAPI().getPlayerVersion(player) - : PacketEvents.getAPI().getPlayerManager().getUser(player).getClientVersion().getProtocolVersion(); + if (viaversion) { + return Via.getAPI().getPlayerVersion(player); + } + + final Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(player); + final User user = PacketEvents.getAPI().getProtocolManager().getUser(channel); + return user.getClientVersion().getProtocolVersion(); } private static boolean warnedBadPlugin; @@ -24,8 +29,10 @@ public static int getProtocolVersion(Player player) { /** * Sends a packet to a player bypassing viaversion. */ - public static void sendPacketBypassingVia(Player player, PacketWrapper packet) { - final User user = PacketEvents.getAPI().getPlayerManager().getUser(player); + public static void sendPacketBypassingVia(UUID player, PacketWrapper packet) { + final Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(player); + final User user = PacketEvents.getAPI().getProtocolManager().getUser(channel); + // This can only ever happen if there is a bad plugin creating NPCs AND adding them to the online player map if (user == null) { if (!warnedBadPlugin) { @@ -37,7 +44,6 @@ public static void sendPacketBypassingVia(Player player, PacketWrapper packet return; } - final Object channel = user.getChannel(); if (!ChannelHelper.isOpen(channel)) return; sendBypassingPacket(user, channel, packet); diff --git a/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java b/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java index 560c594..0ba2fea 100644 --- a/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java +++ b/sponge/src/main/java/com/convallyria/forcepack/sponge/command/ForcePackCommand.java @@ -61,6 +61,7 @@ public void onReload(CommandCause sender, final Set resourcePacks = plugin.getPacksForVersion(player.uniqueId()); final ForcePackSpongePlayer fpPlayer = plugin.addToWaiting(player.uniqueId(), resourcePacks); fpPlayer.setConnection(player.connection()); + plugin.addToWaiting(player.uniqueId(), resourcePacks); if (ProtocolUtil.getProtocolVersion(player.uniqueId()) >= 765) { // 1.20.3+ ProtocolUtil.sendPacketBypassingVia(player.uniqueId(), new WrapperPlayServerResourcePackRemove((UUID) null)); }