feat: code

This commit is contained in:
2025-12-28 23:34:25 +08:00
parent 486f1dc150
commit a63c841076
4 changed files with 199 additions and 167 deletions

Binary file not shown.

View File

@@ -90,7 +90,7 @@ public class ProxyNetworkService implements NetworkServiceInterface, PluginMessa
if (f != null) { if (f != null) {
f.complete(new NetworkResponse(504, "Proxy request timed out (Proxy didn't respond or no player online)")); f.complete(new NetworkResponse(504, "Proxy request timed out (Proxy didn't respond or no player online)"));
} }
}, 100L); // 5 seconds timeout }, 200L); // 10 seconds timeout
} catch (Exception e) { } catch (Exception e) {
future.completeExceptionally(e); future.completeExceptionally(e);

View File

@@ -1,22 +1,18 @@
package online.mineroo.paper.listeners; package online.mineroo.paper.listeners;
import com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent;
import io.papermc.paper.connection.PlayerConfigurationConnection;
import io.papermc.paper.connection.PlayerGameConnection; import io.papermc.paper.connection.PlayerGameConnection;
import io.papermc.paper.dialog.Dialog; import io.papermc.paper.dialog.Dialog;
import io.papermc.paper.dialog.DialogResponseView; import io.papermc.paper.dialog.DialogResponseView;
import io.papermc.paper.event.connection.configuration.AsyncPlayerConnectionConfigureEvent;
import io.papermc.paper.event.player.PlayerCustomClickEvent; import io.papermc.paper.event.player.PlayerCustomClickEvent;
import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.registry.RegistryKey;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@@ -26,9 +22,18 @@ import online.mineroo.common.BindRequest.PlayerBindResponse;
import online.mineroo.common.BindRequest.PlayerBindStatusEnum; import online.mineroo.common.BindRequest.PlayerBindStatusEnum;
import online.mineroo.paper.Config; import online.mineroo.paper.Config;
import online.mineroo.paper.MinerooCore; import online.mineroo.paper.MinerooCore;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullMarked;
@NullMarked @NullMarked
@@ -38,190 +43,211 @@ public class PlayerBindListener implements Listener {
private final Map<UUID, CompletableFuture<PlayerBindResponse>> awaitingResponse = private final Map<UUID, CompletableFuture<PlayerBindResponse>> awaitingResponse =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
// Set to store UUIDs of players who are currently being checked or binding
private final Set<UUID> restrictedPlayers = ConcurrentHashMap.newKeySet();
public PlayerBindListener(MinerooCore plugin) { public PlayerBindListener(MinerooCore plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@EventHandler @EventHandler
public void onPlayerConfigure(AsyncPlayerConnectionConfigureEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
Config config = this.plugin.getConfigObject(); Config config = this.plugin.getConfigObject();
if (!config.getPlayer().getPlayerBind().isRequired()) { if (!config.getPlayer().getPlayerBind().isRequired()) {
return; return;
} }
UUID uuid = event.getConnection().getProfile().getId(); Player player = event.getPlayer();
if (uuid == null) UUID uuid = player.getUniqueId();
return;
try { // Lock player immediately upon join
var response = this.plugin.getBindRequest().checkPlayerBindStatus(uuid, null).join(); restrictedPlayers.add(uuid);
PlayerBindStatusEnum status = response.getStatus(); // 延迟 2 秒 (40 ticks) 再发送检查请求,给予 Proxy 通道建立缓冲时间,防止发包过快导致丢失 (504)
Bukkit.getScheduler().runTaskLater(plugin, () -> {
plugin.getSLF4JLogger().info(status.toString()); if (!player.isOnline()) {
if (status == PlayerBindStatusEnum.BOUND) { restrictedPlayers.remove(uuid); // 玩家中途退出,清理缓存
return;
} else if (status == PlayerBindStatusEnum.NOT_BOUND) {
Dialog dialog = RegistryAccess.registryAccess()
.getRegistry(RegistryKey.DIALOG)
.get(Key.key("mineroo:bind_user"));
if (dialog == null) {
return;
}
PlayerConfigurationConnection connection = event.getConnection();
UUID uniqueId = connection.getProfile().getId();
if (uniqueId == null) {
return;
}
CompletableFuture<PlayerBindResponse> dialogResponse = new CompletableFuture<>();
dialogResponse.completeOnTimeout(null, 1, TimeUnit.MINUTES);
awaitingResponse.put(uniqueId, dialogResponse);
Audience audience = connection.getAudience();
audience.showDialog(dialog);
PlayerBindResponse bindResponse = dialogResponse.join();
if (bindResponse == null) {
audience.closeDialog();
connection.disconnect(
Component.text("Operation timed out, please try again.", NamedTextColor.RED)
);
awaitingResponse.remove(uniqueId);
return;
}
if (bindResponse.getStatus() == PlayerBindEnum.BOUND) {
return;
} else if (bindResponse.getStatus() == PlayerBindEnum.PENDING) {
audience.closeDialog();
String msg = "Binding request submitted!\nPlease go to Mineroo.Online to confirm the "
+ "binding. After confirmation, please re-enter the server.";
connection.disconnect(Component.text(msg, NamedTextColor.GREEN));
} else {
audience.closeDialog();
String statusStr = (bindResponse.getStatus() != null)
? bindResponse.getStatus().toString()
: "UNKNOWN_STATUS";
String msg = bindResponse.getMessage();
if (msg == null)
msg = "Bind failed.";
String kickMessage = statusStr + " : " + msg;
connection.disconnect(Component.text(kickMessage, NamedTextColor.RED));
}
awaitingResponse.remove(uniqueId);
} else {
event.getConnection().disconnect(
Component.text("Internal server error, please try again later.", NamedTextColor.RED)
);
return; return;
} }
} catch (Exception e) { // 异步检查,避免阻塞主线程
this.plugin.getLogger().severe("Failed to check bind status for " + uuid); this.plugin.getBindRequest()
e.printStackTrace(); .checkPlayerBindStatus(uuid, null)
event.getConnection().disconnect( .thenAccept(response -> {
Component.text("Internal server error, please try again later.", NamedTextColor.RED) // 回到主线程处理 UI 和 踢出逻辑
); Bukkit.getScheduler().runTask(plugin, () -> {
} if (!player.isOnline())
} return; // 玩家可能在检查期间退出了
@EventHandler handleBindStatus(player, response.getStatus(), response.getMessage());
void onHandleDialog(PlayerCustomClickEvent event) { });
// Main handlr only use for PlayerConfigureationConnection, })
// Inside Block use for handle command bind request. .exceptionally(e -> {
if (event.getCommonConnection() instanceof PlayerGameConnection playerConn) { plugin.getLogger().severe("Failed to check bind status for " + uuid);
// handle command bind e.printStackTrace();
Player player = playerConn.getPlayer(); Bukkit.getScheduler().runTask(plugin, () -> {
UUID playerUuid = player.getUniqueId(); player.kick(
this.PlayerBindProcess(event, playerUuid, response -> { Component.text("Internal server error, please try again later.", NamedTextColor.RED)
if (response.getStatus() == PlayerBindEnum.PENDING) { );
player.sendMessage(this.plugin.getMessageManager().get("info.bind.player.pending")); });
} else if (response.getStatus() == PlayerBindEnum.BOUND) {
player.sendMessage(this.plugin.getMessageManager().get("info.bind.player.success"));
} else {
String statusText =
(response.getStatus() != null) ? response.getStatus().toString() : "UNKNOWN_STATUS";
String messageText =
(response.getMessage() != null) ? response.getMessage() : "Bind Failed";
String fullMessage = "[ " + statusText + " ]: " + messageText;
player.sendMessage(Component.text(fullMessage, NamedTextColor.RED));
}
});
} else if (event.getCommonConnection()
instanceof PlayerConfigurationConnection configurationConnection) {
// handle required configuration event bind
UUID playerUuid = configurationConnection.getProfile().getId();
this.PlayerBindProcess(event, playerUuid, response -> {
setConnectionJoinResult(playerUuid, response);
});
}
}
private void PlayerBindProcess(
PlayerCustomClickEvent event, UUID playerUuid, Consumer<PlayerBindResponse> callback
) {
if (playerUuid == null) {
return;
}
Key key = event.getIdentifier();
if (key.equals(Key.key("mineroo:bind_user/confirm"))) {
DialogResponseView view = event.getDialogResponseView();
if (view == null) {
return;
}
String userEmail = view.getText("user_email");
String playerName = "Unknown";
if (event.getCommonConnection() instanceof PlayerConfigurationConnection conn) {
playerName = conn.getProfile().getName();
} else if (event.getCommonConnection() instanceof PlayerGameConnection conn) {
playerName = conn.getPlayer().getName();
}
BindRequest bindRequest = plugin.getBindRequest();
bindRequest.PlayerBindRequest(userEmail, playerUuid, playerName)
.thenAccept(response -> { callback.accept(response); })
.exceptionally(ex -> {
callback.accept(BindRequest.createPlayerBindErrorResponse(
"Network request failed: " + ex.getMessage()
));
return null; return null;
}); });
}, 40L);
}
} else if (key.equals(Key.key("mineroo:bind_user/cancel"))) { private void handleBindStatus(Player player, PlayerBindStatusEnum status, String message) {
callback.accept(BindRequest.createPlayerBindErrorResponse( if (status == PlayerBindStatusEnum.BOUND) {
this.plugin.getMessageManager().getString("info.bind.player.canceled") // Player is bound, release restriction
restrictedPlayers.remove(player.getUniqueId());
return;
} else if (status == PlayerBindStatusEnum.NOT_BOUND) {
Dialog dialog = RegistryAccess.registryAccess()
.getRegistry(RegistryKey.DIALOG)
.get(Key.key("mineroo:bind_user"));
if (dialog == null) {
plugin.getLogger().severe("Dialog 'mineroo:bind_user' not found!");
return;
}
UUID uniqueId = player.getUniqueId();
CompletableFuture<PlayerBindResponse> dialogResponse = new CompletableFuture<>();
dialogResponse.completeOnTimeout(null, 2, TimeUnit.MINUTES);
awaitingResponse.put(uniqueId, dialogResponse);
// 使用 player 直接显示 Dialog
player.showDialog(dialog);
// 异步等待 Dialog 结果,不阻塞主线程
dialogResponse.thenAccept(bindResponse -> {
Bukkit.getScheduler().runTask(plugin, () -> {
awaitingResponse.remove(uniqueId);
if (bindResponse == null) {
player.closeDialog();
player.kick(
Component.text("Operation timed out, please try again.", NamedTextColor.RED)
);
return;
}
if (bindResponse.getStatus() == PlayerBindEnum.BOUND) {
// 极其罕见的情况:请求瞬间已绑定(通常不会发生,除非已有自动绑定逻辑)
restrictedPlayers.remove(uniqueId);
player.sendMessage(this.plugin.getMessageManager().get("info.bind.player.success"));
} else if (bindResponse.getStatus() == PlayerBindEnum.PENDING) {
player.closeDialog();
// 提交成功,踢出玩家,让其去网站确认
player.kick(this.plugin.getMessageManager().get("info.bind.player.pending"));
} else {
player.closeDialog();
String statusStr = (bindResponse.getStatus() != null)
? bindResponse.getStatus().toString()
: "UNKNOWN";
String msg =
bindResponse.getMessage() != null ? bindResponse.getMessage() : "Bind failed";
player.kick(Component.text(statusStr + " : " + msg, NamedTextColor.RED));
}
});
});
} else {
player.kick(Component.text(
"Bind Check Error: " + (message != null ? message : status.toString()), NamedTextColor.RED
)); ));
} }
} }
@EventHandler @EventHandler
void onConnectionClose(PlayerConnectionCloseEvent event) { void onHandleDialog(PlayerCustomClickEvent event) {
awaitingResponse.remove(event.getPlayerUniqueId()); Player player = null;
if (event.getCommonConnection() instanceof PlayerGameConnection conn) {
player = conn.getPlayer();
}
if (player == null)
return;
UUID playerUuid = player.getUniqueId();
Key key = event.getIdentifier();
if (key.equals(Key.key("mineroo:bind_user/confirm"))) {
DialogResponseView view = event.getDialogResponseView();
if (view == null)
return;
String userEmail = view.getText("user_email");
String playerName = player.getName();
final UUID targetUuid = playerUuid;
plugin.getBindRequest()
.PlayerBindRequest(userEmail, targetUuid, playerName)
.thenAccept(response -> setConnectionJoinResult(targetUuid, response))
.exceptionally(ex -> {
setConnectionJoinResult(
targetUuid,
BindRequest.createPlayerBindErrorResponse("Network Error: " + ex.getMessage())
);
return null;
});
} else if (key.equals(Key.key("mineroo:bind_user/cancel"))) {
setConnectionJoinResult(
playerUuid, BindRequest.createPlayerBindErrorResponse("Canceled by user")
);
}
} }
/** @EventHandler
void onPlayerQuit(PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
awaitingResponse.remove(uuid);
restrictedPlayers.remove(uuid);
}
* Simple utility method for setting a connection's dialog response result. // --- Restriction Event Handlers ---
*/ @EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerMove(PlayerMoveEvent event) {
if (restrictedPlayers.contains(event.getPlayer().getUniqueId())) {
if (event.getFrom().getX() != event.getTo().getX()
|| event.getFrom().getY() != event.getTo().getY()
|| event.getFrom().getZ() != event.getTo().getZ()) {
event.setCancelled(true);
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerDropItem(PlayerDropItemEvent event) {
if (restrictedPlayers.contains(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerInteract(PlayerInteractEvent event) {
if (restrictedPlayers.contains(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onInventoryOpen(InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player
&& restrictedPlayers.contains(player.getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onInventoryClick(InventoryClickEvent event) {
if (event.getWhoClicked() instanceof Player player
&& restrictedPlayers.contains(player.getUniqueId())) {
event.setCancelled(true);
}
}
private void setConnectionJoinResult(UUID uniqueId, PlayerBindResponse value) { private void setConnectionJoinResult(UUID uniqueId, PlayerBindResponse value) {
CompletableFuture<PlayerBindResponse> future = awaitingResponse.get(uniqueId); CompletableFuture<PlayerBindResponse> future = awaitingResponse.get(uniqueId);
if (future != null) { if (future != null) {
future.complete(value); future.complete(value);
} }

View File

@@ -68,6 +68,9 @@ public class ChannelListener {
String jsonPayload = in.readUTF(); String jsonPayload = in.readUTF();
ProxyNetworkRequest req = gson.fromJson(jsonPayload, ProxyNetworkRequest.class); ProxyNetworkRequest req = gson.fromJson(jsonPayload, ProxyNetworkRequest.class);
reqId = req.getRequestId(); reqId = req.getRequestId();
long startTime = System.currentTimeMillis();
plugin.getLogger().info("[Debug] Received API Request: " + req.getEndpoint() + " (ID: " + reqId + ")");
CompletableFuture<NetworkResponse> future; CompletableFuture<NetworkResponse> future;
@@ -89,34 +92,37 @@ public class ChannelListener {
future = httpService.postData(req.getEndpoint(), bodyJson); future = httpService.postData(req.getEndpoint(), bodyJson);
} else { } else {
// ... (GET logic)
// replace placeholders // ...
java.util.Map<String, String> finalParams = new java.util.HashMap<>(); java.util.Map<String, String> finalParams = new java.util.HashMap<>();
if (req.getParams() != null) { if (req.getParams() != null) {
for (java.util.Map.Entry<String, String> entry : req.getParams().entrySet()) { for (java.util.Map.Entry<String, String> entry : req.getParams().entrySet()) {
String originalKey = entry.getKey(); String originalKey = entry.getKey();
String originalValue = entry.getValue(); String originalValue = entry.getValue();
String replacedValue = resolvePlaceholders(originalValue); String replacedValue = resolvePlaceholders(originalValue);
finalParams.put(originalKey, replacedValue); finalParams.put(originalKey, replacedValue);
} }
} }
plugin.getLogger().info("Proxy GET [ " + req.getEndpoint() + " ]\n\t" + finalParams.toString()); plugin.getLogger().info("Proxy GET [ " + req.getEndpoint() + " ] params: " + finalParams);
future = httpService.getData(req.getEndpoint(), finalParams); future = httpService.getData(req.getEndpoint(), finalParams);
} }
// 3. Handle the result and send back the response // 3. Handle the result and send back the response
String finalReqId = reqId; String finalReqId = reqId;
future.thenAccept(response -> { future.thenAccept(response -> {
long duration = System.currentTimeMillis() - startTime;
plugin.getLogger().info("[Debug] API Response received for " + finalReqId + " in " + duration + "ms. Status: " + response.getStatusCode());
// Check if HTTP status code is 2xx // Check if HTTP status code is 2xx
boolean success = response.getStatusCode() >= 200 && response.getStatusCode() < 300; boolean success = response.getStatusCode() >= 200 && response.getStatusCode() < 300;
sendResponse(backendServer, finalReqId, success, response.getBody()); sendResponse(backendServer, finalReqId, success, response.getBody());
}).exceptionally(e -> { }).exceptionally(e -> {
plugin.getLogger().error("API Proxy Request Failed", e); long duration = System.currentTimeMillis() - startTime;
plugin.getLogger().error("[Debug] API Request Failed for " + finalReqId + " after " + duration + "ms", e);
sendResponse(backendServer, finalReqId, false, "Proxy Error: " + e.getMessage()); sendResponse(backendServer, finalReqId, false, "Proxy Error: " + e.getMessage());
return null; return null;
}); });