Skip to content

Commit

Permalink
Kick only idle when no link request, send message on link req replace
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim203 committed Jan 18, 2025
1 parent c326378 commit 8ea5760
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 77 deletions.
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ group = "org.geysermc.globallinkserver"
dependencies {
paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT")

implementation(libs.bundles.fastutil)
compileOnly(libs.floodgate.api)
implementation(libs.mariadb.client)
implementation(libs.bundles.fastutil)

compileOnly(libs.checker.qual)
}

Expand Down
8 changes: 5 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
[versions]
fastutil = "8.5.2"
floodgate = "2.2.3-SNAPSHOT"
mariadb-client = "2.7.3"
fastutil = "8.5.2"
checker-qual = "3.21.1"

indra = "3.1.2"
paperweight = "2.0.0-beta.12"
runpaper = "2.3.1"

[libraries]
floodgate-api = { group = "org.geysermc.floodgate", name = "api", version = "2.2.3-SNAPSHOT" }
floodgate-api = { group = "org.geysermc.floodgate", name = "api", version.ref = "floodgate" }
mariadb-client = { module = "org.mariadb.jdbc:mariadb-java-client", version.ref = "mariadb-client" }

fastutil-int-int-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-int-maps", version.ref = "fastutil" }
fastutil-int-object-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-object-maps", version.ref = "fastutil" }
fastutil-object-int-maps = { group = "com.nukkitx.fastutil", name = "fastutil-object-int-maps", version.ref = "fastutil" }
fastutil-object-object-maps = { group = "com.nukkitx.fastutil", name = "fastutil-object-object-maps", version.ref = "fastutil" }
mariadb-client = { module = "org.mariadb.jdbc:mariadb-java-client", version.ref = "mariadb-client" }

checker-qual = { module = "org.checkerframework:checker-qual", version.ref = "checker-qual" }

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/geysermc/globallinkserver/Components.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ private Components() {}
public static final Component LINK_ALREADY_LINKED = Component.text(
"You are already linked! You need to unlink first before linking again.", NamedTextColor.RED);
public static final Component LINK_CODE_INVALID_RANGE = Component.text("Invalid link code!", NamedTextColor.RED);
public static final Component LINK_REQUEST_REPLACED = Component.text(
"You already had an active link request, so your old request has been invalidated.", NamedTextColor.AQUA);
public static final Component LINK_REQUEST_NOT_FOUND =
Component.text("Could not find the provided link. Is it expired?", NamedTextColor.RED);
Component.text("Could not find the provided link. Has it expired?", NamedTextColor.RED);
public static final Component LINK_REQUEST_SAME_PLATFORM = Component.text(
"You can only link a Java account to a Bedrock account. ", NamedTextColor.RED)
.append(Component.text("Try to start the linking process again!"));
Expand Down
96 changes: 40 additions & 56 deletions src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@
package org.geysermc.globallinkserver;

import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.event.player.AsyncChatEvent;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.GameRule;
Expand All @@ -34,41 +29,37 @@
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerCommandSendEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.globallinkserver.config.ConfigReader;
import org.geysermc.globallinkserver.handler.CommandHandler;
import org.geysermc.globallinkserver.handler.JoinHandler;
import org.geysermc.globallinkserver.handler.MoveInactivityHandler;
import org.geysermc.globallinkserver.link.LinkManager;
import org.geysermc.globallinkserver.manager.DatabaseManager;
import org.geysermc.globallinkserver.manager.PlayerManager;
import org.geysermc.globallinkserver.service.LinkLookupService;
import org.geysermc.globallinkserver.service.LinkInfoService;
import org.geysermc.globallinkserver.handler.CommandHandler;
import org.geysermc.globallinkserver.service.LinkLookupService;
import org.geysermc.globallinkserver.util.MultiConditionSet;
import org.geysermc.globallinkserver.util.Utils;

@SuppressWarnings("UnstableApiUsage")
public class GlobalLinkServer extends JavaPlugin implements Listener {
private static final Set<String> PERMITTED_COMMANDS =
Set.of("link", "linkaccount", "linkinfo", "info", "unlink", "unlinkaccount", "help");

private final Cache<UUID, Instant> playerIdleCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.<UUID, Instant>removalListener(notification -> {
if (notification.wasEvicted() && notification.getCause() == RemovalCause.EXPIRED) {
Player player = Bukkit.getPlayer(notification.getKey());
if (player != null) {
Bukkit.getScheduler().callSyncMethod(this, () -> {
player.kick(Components.KICK_IDLE);
return null;
});
}
}
})
.build();
private final MultiConditionSet<UUID> playerIdleTracker = new MultiConditionSet<>(15_000, uuid -> {
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
Bukkit.getScheduler().callSyncMethod(this, () -> {
player.kick(Components.KICK_IDLE);
return null;
});
}
});

private LinkLookupService linkLookupService;
private LinkInfoService linkInfoService;
Expand All @@ -85,28 +76,18 @@ public void onEnable() {

var commandUtils = new CommandHandler(linkLookupService, linkInfoService, linkManager, playerManager, this);

Bukkit.getScheduler().scheduleSyncRepeatingTask(this, linkManager::cleanupTempLinks, 0, 1);

Bukkit.getScheduler()
.scheduleSyncRepeatingTask(
this,
() -> {
Bukkit.getOnlinePlayers().forEach(player -> {
if (linkLookupService.isLookupCompleted(player)) {
if (linkLookupService.isLinkedCached(player)) {
player.sendActionBar(Components.UNLINK_INSTRUCTION);
} else {
player.sendActionBar(Components.LINK_INSTRUCTION);
}
}
});
},
10,
15);
// clean up link requests every 30s
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, linkManager::cleanupLinkRequests, 60 * 20, 30 * 20);

Bukkit.getScheduler().scheduleSyncRepeatingTask(this, this::broadcastLinkStatusActionbar, 10, 15);

var pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(this, this);
pluginManager.registerEvents(new JoinHandler(linkLookupService, playerIdleCache, this), this);
pluginManager.registerEvents(new JoinHandler(linkLookupService, playerIdleTracker, this), this);
pluginManager.registerEvents(new MoveInactivityHandler(playerIdleTracker), this);

// if the player has an active link request, don't kick the player
playerIdleTracker.addRemovalCondition(uuid -> !linkManager.hasActiveLinkRequest(uuid));

LifecycleEventManager<@NonNull Plugin> manager = this.getLifecycleManager();
manager.registerEventHandler(LifecycleEvents.COMMANDS, event -> {
Expand Down Expand Up @@ -159,12 +140,26 @@ public void onEnable() {
getServer().clearRecipes();
getServer().setDefaultGameMode(GameMode.ADVENTURE);

// Clean up every 10 seconds
Bukkit.getScheduler().runTaskTimer(this, playerIdleCache::cleanUp, 0, 10 * 20);

getLogger().info("Started Global Linking plugin!");
}

private void broadcastLinkStatusActionbar() {
Bukkit.getOnlinePlayers().forEach(player -> {
if (linkLookupService.isLookupCompleted(player)) {
if (linkLookupService.isLinkedCached(player)) {
player.sendActionBar(Components.UNLINK_INSTRUCTION);
} else {
player.sendActionBar(Components.LINK_INSTRUCTION);
}
}
});
}

@Override
public void onDisable() {
playerIdleTracker.close();
}

@EventHandler
public void onCommands(PlayerCommandSendEvent event) {
if (event.getPlayer().isOp()) {
Expand Down Expand Up @@ -219,15 +214,14 @@ public void preCommand(PlayerCommandPreprocessEvent event) {
@EventHandler
public void onPlayerLeave(PlayerQuitEvent event) {
event.quitMessage(null);
playerIdleCache.invalidate(event.getPlayer().getUniqueId());
playerIdleTracker.remove(event.getPlayer().getUniqueId());
linkLookupService.invalidate(event.getPlayer());
}

@EventHandler
public void onEntityDamage(EntityDamageEvent event) {
if (event.getCause() == EntityDamageEvent.DamageCause.VOID && event.getEntity() instanceof Player player) {
event.setCancelled(true);

Utils.fakeRespawn(player);
}
}
Expand Down Expand Up @@ -263,14 +257,4 @@ public void onServerListPing(PaperServerListPingEvent event) {
event.setNumPlayers(0);
event.setMaxPlayers(1);
}

@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// todo player idle should not only depend on movement, also check for pending links
int diffX = event.getFrom().getBlockX() - event.getTo().getBlockX();
int diffY = event.getFrom().getBlockZ() - event.getTo().getBlockZ();
if (Math.abs(diffX) > 0 || Math.abs(diffY) > 0) {
playerIdleCache.put(event.getPlayer().getUniqueId(), Instant.now());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ public int startLink(CommandContext<CommandSourceStack> ctx) {
return Command.SINGLE_SUCCESS;
}

//todo use the boolean return and send a message that the active link request has been invalidated
linkManager.removeActiveLinkRequest(player);
if (linkManager.removeActiveLinkRequest(player)) {
player.sendMessage(Components.LINK_REQUEST_REPLACED);
}

String code = String.format("%04d", linkManager.createTempLink(player));
String otherPlatform = playerManager.isBedrockPlayer(player) ? "Java" : "Bedrock";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
*/
package org.geysermc.globallinkserver.handler;

import com.google.common.cache.Cache;
import java.time.Instant;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
Expand All @@ -15,17 +13,18 @@
import org.bukkit.plugin.Plugin;
import org.geysermc.globallinkserver.Components;
import org.geysermc.globallinkserver.service.LinkLookupService;
import org.geysermc.globallinkserver.util.MultiConditionSet;
import org.jspecify.annotations.NullMarked;

@NullMarked
public final class JoinHandler implements Listener {
private final LinkLookupService linkLookupService;
private final Cache<UUID, Instant> playerIdleCache;
private final MultiConditionSet<UUID> playerIdleTracker;
private final Plugin plugin;

public JoinHandler(LinkLookupService linkLookupService, Cache<UUID, Instant> playerIdleCache, Plugin plugin) {
public JoinHandler(LinkLookupService linkLookupService, MultiConditionSet<UUID> playerIdleTracker, Plugin plugin) {
this.linkLookupService = linkLookupService;
this.playerIdleCache = playerIdleCache;
this.playerIdleTracker = playerIdleTracker;
this.plugin = plugin;
}

Expand All @@ -43,7 +42,7 @@ public void onPlayerLoad(PlayerJoinEvent event) {
otherPlayer.hidePlayer(plugin, player);
});

playerIdleCache.put(player.getUniqueId(), Instant.now());
playerIdleTracker.add(player.getUniqueId());

linkLookupService.lookup(player).whenComplete(($, throwable) -> {
if (throwable != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 GeyserMC
* Licensed under the MIT license
* @link https://github.com/GeyserMC/GlobalLinkServer
*/
package org.geysermc.globallinkserver.handler;

import it.unimi.dsi.fastutil.objects.Object2LongMap;
import it.unimi.dsi.fastutil.objects.Object2LongMaps;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import java.util.UUID;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.geysermc.globallinkserver.util.MultiConditionSet;

public final class MoveInactivityHandler implements Listener {
private static final long TIME_TILL_IDLE_MILLIS = 15 * 60 * 1000; // 15 minutes

private final Object2LongMap<UUID> lastMoveAction = Object2LongMaps.synchronize(new Object2LongOpenHashMap<>());

public MoveInactivityHandler(MultiConditionSet<UUID> playerIdleTracker) {
playerIdleTracker
.addRemovalCondition(key -> {
long lastMovement = lastMoveAction.getLong(key);
// if not present, the value will be 0. It should never happen, so if that happens we remove them.
return System.currentTimeMillis() - lastMovement >= TIME_TILL_IDLE_MILLIS;
})
.addRemovalListener(lastMoveAction::removeLong);
}

@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
// just to make sure that there aren't any weird edge cases of a player not firing a PlayerMoveEvent immediately
lastMoveAction.put(event.getPlayer().getUniqueId(), System.currentTimeMillis());
}

@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
int diffX = event.getFrom().getBlockX() - event.getTo().getBlockX();
int diffY = event.getFrom().getBlockZ() - event.getTo().getBlockZ();
if (Math.abs(diffX) > 0 || Math.abs(diffY) > 0) {
lastMoveAction.put(event.getPlayer().getUniqueId(), System.currentTimeMillis());
}
}
}
26 changes: 18 additions & 8 deletions src/main/java/org/geysermc/globallinkserver/link/LinkManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

@NullMarked
public class LinkManager {
private static final int PENDING_LINK_DURATION = 60_000 * 15; // 15 min
private static final int PENDING_LINK_TTL_MILLIS = 15 * 60 * 1000; // 15 min

private final PlayerManager playerManager;
private final DatabaseManager database;
Expand All @@ -46,7 +46,7 @@ public LinkManager(PlayerManager playerManager, DatabaseManager database) {
}

public int createTempLink(Player player) {
var linkRequest = new LinkRequest(createCode(), PENDING_LINK_DURATION, player);
var linkRequest = new LinkRequest(createCode(), PENDING_LINK_TTL_MILLIS, player);

linkRequests.put(linkRequest.code(), linkRequest);
linkRequestForPlayer.put(player.getUniqueId(), linkRequest.code());
Expand All @@ -73,15 +73,25 @@ private int createCode() {

private boolean isLinkValid(@Nullable LinkRequest link) {
long currentMillis = System.currentTimeMillis();
return link != null && currentMillis - link.expiryTime() < PENDING_LINK_DURATION;
return link != null && currentMillis < link.expiryTime();
}

public boolean removeActiveLinkRequest(Player player) {
int linkId = linkRequestForPlayer.removeInt(player.getUniqueId());
if (linkId != -1) {
linkRequests.remove(linkId);
int code = linkRequestForPlayer.removeInt(player.getUniqueId());
if (code != -1) {
linkRequests.remove(code);
}
return linkId != -1;
return code != -1;
}

public boolean hasActiveLinkRequest(UUID uuid) {
int code = linkRequestForPlayer.getInt(uuid);
if (code == -1) {
return false;
}
var request = linkRequests.get(code);
//noinspection ConstantValue ??
return request != null && isLinkValid(request);
}

public CompletableFuture<Boolean> finaliseLink(Link linkRequest) {
Expand Down Expand Up @@ -128,7 +138,7 @@ public CompletableFuture<Boolean> unlinkAccount(Player player) {
database.executor());
}

public void cleanupTempLinks() {
public void cleanupLinkRequests() {
Iterator<LinkRequest> iterator = linkRequests.values().iterator();

long ctm = System.currentTimeMillis();
Expand Down
Loading

0 comments on commit 8ea5760

Please sign in to comment.