Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions velocity/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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/")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -41,18 +43,70 @@ public final class PackHandler {
private final Map<UUID, ForcePackPlayer> waiting;
private final Map<UUID, Set<PendingResourcePackSend>> pendingTasks;

private final Map<UUID, CompletableFuture<Void>> configurationPhaseCompletions;
private final Map<UUID, ServerConnection> configurationPhaseServers;
private final Set<UUID> 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<Void> 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<ServerConnection> getConfigurationPhaseServer(final Player player) {
return Optional.ofNullable(configurationPhaseServers.get(player.getUniqueId()));
}

private void completeConfigurationPhase(final UUID uuid) {
configurationPhaseServers.remove(uuid);
final CompletableFuture<Void> 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;
}

Expand All @@ -62,7 +116,7 @@ public void processWaitingResourcePack(Player player, UUID packId) {
});

if (newPlayer == null || newPlayer.getWaitingPacks().isEmpty()) {
removeFromWaiting(player);
removeFromWaiting(player, true);
}
}

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,16 +39,18 @@ public ResourcePackListener(final ForcePackVelocity plugin) {
@Subscribe(order = PostOrder.EARLY)
public void onPackStatus(PlayerResourcePackStatusEvent event) {
final Player player = event.getPlayer();
final Optional<ServerConnection> 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;
}

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());
Expand Down Expand Up @@ -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());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is broken when using configuration phase (sendPluginMessage).

We need to somehow update the Paper and Sponge submodules to listen to this channel on configuration phase.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on refactoring the event handling and push to this PR

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not possible to support configuration stage status events. Paper doesn't even have an event for configuration resource pack statuses and player object doesn't exist during config phase.

// 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));
Expand Down Expand Up @@ -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<ServerConnection> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ private void runSetResourcePack(UUID uuid) {
if (server.contains(ForcePackVelocity.GLOBAL_SERVER_NAME)) {
serverConfig = velocityPlugin.getConfig().getConfig("global-pack");
final List<String> excluded = serverConfig.getStringList("exclude");
final Optional<ServerConnection> currentServer = player.getCurrentServer();
final Optional<ServerConnection> currentServer = player.getCurrentServer()
.or(() -> velocityPlugin.getPackHandler().getConfigurationPhaseServer(player));
if (currentServer.isPresent()) {
if (excluded.contains(currentServer.get().getServerInfo().getName())) return;
} else {
Expand Down
4 changes: 4 additions & 0 deletions velocity/src/main/resources/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down