feat: use clang-format

This commit is contained in:
2025-12-18 04:41:06 -08:00
parent 35fae3c3b3
commit 4049bc6a0d
18 changed files with 784 additions and 312 deletions

9
.clang-format Normal file
View File

@@ -0,0 +1,9 @@
---
Language: Java
BasedOnStyle: Google
AlignAfterOpenBracket: BlockIndent
ColumnLimit: 100
BinPackArguments: false
AllowAllArgumentsOnNextLine: true

View File

@@ -1,12 +1,17 @@
package online.mineroo.common;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
@@ -43,14 +48,14 @@ public class BindRequest {
* @return A future that completes with true if status is 'bound', false
* otherwise
*/
public CompletableFuture<Boolean> checkBindStatus(String hostname, int port) {
public CompletableFuture<Boolean> checkBindStatus(String hostname, String port) {
// NetworkService is already configured with BaseURL, only need the path here
String path = "/server/server-bind-status";
Map<String, String> params = new HashMap<>();
params.put("address", hostname);
params.put("port", String.valueOf(port));
params.put("port", port);
return networkService.getData(path, params)
.thenApply(response -> {
@@ -86,12 +91,26 @@ public class BindRequest {
* @param port The server port
* @return A future that completes with true if tokens are received
*/
public CompletableFuture<Boolean> initialMotdVerifyRequest(String hostname, int port) {
public CompletableFuture<Boolean> initialMotdVerifyRequest(String hostname, String port) {
String path = "/server/motd-verify";
Integer num_port = -1;
if (!"$port".equals(port)) {
try {
num_port = Integer.valueOf(port);
} catch (NumberFormatException ignored) {
num_port = 25565;
}
}
JsonObject json = new JsonObject();
json.addProperty("server_address", hostname);
json.addProperty("server_port", port);
if (num_port == -1) {
json.addProperty("server_port", port);
} else {
json.addProperty("server_port", num_port);
}
return networkService.postData(path, json)
.thenApply(response -> {
@@ -149,7 +168,6 @@ public class BindRequest {
return networkService.postData(path, json)
.thenApply(response -> {
String responseBody = response.getBody();
// Assume as long as the request returns successfully and no exception is
// thrown, it is considered successful (HTTP 200)
// If the API returns a specific {"success": false}, you need to parse the JSON
@@ -162,6 +180,152 @@ public class BindRequest {
});
}
public class PlayerBindResponse implements Serializable {
@SerializedName("status")
private PlayerBindStatus status;
@SerializedName("message")
private String message;
@SerializedName("cached")
private boolean cached;
@SerializedName("timestamp")
private long timestamp;
@SerializedName("info")
private PlayerBindInfo info;
// --- Getters ---
public PlayerBindStatus getStatus() {
return status;
}
public String getMessage() {
return message;
}
public boolean isCached() {
return cached;
}
public long getTimestamp() {
return timestamp;
}
public PlayerBindInfo getInfo() {
return info;
}
public void setStatus(PlayerBindStatus status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
}
public static enum PlayerBindStatus {
@SerializedName("notBound")
NOT_BOUND,
@SerializedName("bound")
BOUND,
@SerializedName("conflictUser")
CONFLICT_USER,
@SerializedName("conflictPlayer")
CONFLICT_PLAYER,
@SerializedName("error")
ERROR
}
public static class PlayerBindInfo implements Serializable {
@SerializedName("bound_uuid")
private String boundUuid;
@SerializedName("minecraft_uuid")
private String minecraftUuid;
@SerializedName("bound_user_id")
private Integer boundUserId;
@SerializedName("user_id")
private Integer userId;
// --- Getters ---
public String getBoundUuid() {
return boundUuid;
}
public String getMinecraftUuid() {
return minecraftUuid;
}
public Integer getBoundUserId() {
return boundUserId;
}
public Integer getUserId() {
return userId;
}
public String getEffectiveUuid() {
return minecraftUuid != null ? minecraftUuid : boundUuid;
}
public Integer getEffectiveUserId() {
return userId != null ? userId : boundUserId;
}
}
private PlayerBindResponse createErrorResponse(String errorMessage) {
PlayerBindResponse errorRes = new PlayerBindResponse();
errorRes.setStatus(PlayerBindStatus.ERROR);
errorRes.setMessage(errorMessage);
return errorRes;
}
public CompletableFuture<PlayerBindResponse> checkPlayerBindStatus(UUID player_uuid, String user_email) {
String path = "/server/user-bind-status";
Map<String, String> params = new HashMap<>();
params.put("user_email", user_email);
params.put("player_uuid", player_uuid.toString());
return networkService.getData(path, params)
.thenApply(response -> {
Gson gson = new Gson();
String responseBody = response.getBody();
if (response.getStatusCode() != 200) {
try {
return gson.fromJson(responseBody, PlayerBindResponse.class);
} catch (Exception ignored) {
return createErrorResponse("HTTP Error: " + response.getStatusCode());
}
}
try {
return gson.fromJson(responseBody, PlayerBindResponse.class);
} catch (Exception e) {
logger.error("Mineroo Bind: Failed to parse JSON", e);
return createErrorResponse("JSON Parse Error");
}
})
.exceptionally(e -> {
logger.error("Mineroo Bind: Network exception", e);
return createErrorResponse("Network Error: " + e.getMessage());
});
}
/**
* Gets the MOTD token received from the API.
*

View File

@@ -2,9 +2,10 @@ package online.mineroo.common;
public class ProtocolConstants {
// Protocol channel name
public static String PROTOCOL_CHANNEL = "mineroo:plugin:api";
public static String PROTOCOL_CHANNEL = "mineroo:net";
// Sub-channel/event names
public static String API_REQUEST = "API_REQUEST";
public static String API_REQUEST = "OAPI_REQUEST";
public static String API_RESPONSE = "OAPI_RESPONSE";
public static String BIND_MOTD_TOKEN = "SET_MOTD_TOKEN";
}

View File

@@ -66,7 +66,7 @@ public class Config {
this.description = config.getString("server.description", "A minecraft server");
this.bind = new ServerBindSection(
config.getString("server.bind.address", ""),
config.getInt("server.bind.port", 0),
config.getObject("server.bind.port", Integer.class, null),
config.getString("server.bind.token", "get token from `mineroo.online/resources/servers` page!"));
}
@@ -85,10 +85,10 @@ public class Config {
public static class ServerBindSection {
private final String address;
private final int port;
private final Integer port;
private final String bindToken;
public ServerBindSection(String address, int port, String bindToken) {
public ServerBindSection(String address, Integer port, String bindToken) {
this.address = address;
this.port = port;
this.bindToken = bindToken;
@@ -98,7 +98,7 @@ public class Config {
return address;
}
public int getPort() {
public Integer getPort() {
return port;
}

View File

@@ -1,38 +1,38 @@
package online.mineroo.paper;
import org.bukkit.plugin.java.JavaPlugin;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import online.mineroo.common.BindRequest;
import online.mineroo.common.HttpNetworkService;
import online.mineroo.common.MessageManager;
import online.mineroo.common.NetworkServiceInterface;
import online.mineroo.common.ProtocolConstants;
import online.mineroo.paper.commands.MainCommand;
import online.mineroo.paper.utils.PlayerBindDialog;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import online.mineroo.paper.listeners.BindListener;
import online.mineroo.paper.listeners.PlayerBindListener;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
public class MinerooCore extends JavaPlugin implements Listener {
private MessageManager messageManager;
private NetworkServiceInterface networkService;
private BindRequest bindRequest;
private Config config;
private BindListener bindListener;
@Override
public void onEnable() {
saveDefaultConfig();
this.bindListener = new BindListener();
getServer().getPluginManager().registerEvents(bindListener, this);
reloadAll();
this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> {
commands.registrar().register(new MainCommand(this).build());
});
this.getServer().getMessenger().registerOutgoingPluginChannel(this, ProtocolConstants.PROTOCOL_CHANNEL);
getServer().getPluginManager().registerEvents(new PlayerBindListener(this), this);
messageManager = new MessageManager();
@@ -40,19 +40,7 @@ public class MinerooCore extends JavaPlugin implements Listener {
}
@Override
public void onDisable() {
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
PlayerBindDialog dialog = new PlayerBindDialog();
player.showDialog(dialog.getDialog(messageManager));
player.sendMessage(messageManager.get("message.test", "player", player.getName()));
}
public void onDisable() {}
public void reloadAll() {
reloadConfig();
@@ -66,10 +54,11 @@ public class MinerooCore extends JavaPlugin implements Listener {
String token = config.getServer().getServerBind().getBindToken();
String hostname = config.getTest().getApiHostname();
this.networkService = new HttpNetworkService(hostname, "mserver", token);
getLogger().info("Using direct HTTP network service.");
getLogger().info("API: " + hostname);
getLogger().info("Using direct HTTP network [ " + hostname + " ]");
}
bindRequest = new BindRequest(this.getSLF4JLogger(), this.getNetworkService());
if (this.messageManager == null) {
this.messageManager = new MessageManager();
} else {
@@ -88,4 +77,12 @@ public class MinerooCore extends JavaPlugin implements Listener {
public MessageManager getMessageManager() {
return messageManager;
}
public BindListener getBindListener() {
return this.bindListener;
}
public BindRequest getBindRequest() {
return bindRequest;
}
}

View File

@@ -0,0 +1,48 @@
package online.mineroo.paper;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.registry.data.dialog.ActionButton;
import io.papermc.paper.registry.data.dialog.DialogBase;
import io.papermc.paper.registry.data.dialog.body.DialogBody;
import io.papermc.paper.registry.data.dialog.input.DialogInput;
import io.papermc.paper.registry.data.dialog.type.DialogType;
import io.papermc.paper.registry.event.RegistryEvents;
import io.papermc.paper.registry.keys.DialogKeys;
import java.util.List;
import net.kyori.adventure.key.Key;
import online.mineroo.common.MessageManager;
public class MinerooCoreBootstrap implements PluginBootstrap {
@Override
public void bootstrap(BootstrapContext context) {
MessageManager messageManager = new MessageManager();
ActionButton confirmButton =
ActionButton.create(messageManager.get("dialog.bind.player.confirm"), null, 100, null);
ActionButton cancelButton =
ActionButton.create(messageManager.get("dialog.bind.player.cancel"), null, 100, null);
DialogInput usernameInput =
DialogInput.text("username", messageManager.get("dialog.bind.player.email")).build();
DialogBase dialogBase =
DialogBase.builder(messageManager.get("dialog.bind.player.title"))
.body(
List.of(DialogBody.plainMessage(messageManager.get("dialog.bind.player.content")))
)
.inputs(List.of(usernameInput))
.build();
DialogType confirmationType = DialogType.confirmation(confirmButton, cancelButton);
context.getLifecycleManager().registerEventHandler(RegistryEvents.DIALOG.compose().newHandler(
event
-> event.registry().register(
DialogKeys.create(Key.key("papermc:bind_mineroo_user")),
builder -> builder.base(dialogBase).type(confirmationType)
)
));
}
}

View File

@@ -5,33 +5,44 @@ import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import online.mineroo.common.ProxyNetworkRequest;
import online.mineroo.common.NetworkResponse;
import online.mineroo.common.NetworkServiceInterface;
import online.mineroo.common.ProtocolConstants;
import online.mineroo.common.NetworkResponse;
import online.mineroo.common.ProxyNetworkRequest;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.messaging.PluginMessageListener;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ProxyNetworkService implements NetworkServiceInterface, PluginMessageListener {
public class ProxyNetworkService implements NetworkServiceInterface, PluginMessageListener, Listener {
private final JavaPlugin plugin;
private final Gson gson = new Gson();
private final String CHANNEL = "mineroo:net";
private final String CHANNEL = ProtocolConstants.PROTOCOL_CHANNEL;
// Store pending requests <RequestId, Future>
// Stores sent requests <RequestId, Future>
private final Map<String, CompletableFuture<NetworkResponse>> pendingRequests = new ConcurrentHashMap<>();
// Buffer queue: stores raw packets that cannot be sent when no player is online
private final Queue<byte[]> messageQueue = new ConcurrentLinkedQueue<>();
public ProxyNetworkService(JavaPlugin plugin) {
this.plugin = plugin;
// Register channels
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, CHANNEL);
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, CHANNEL, this);
// Register event (used to listen for player join to flush queue)
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
@@ -48,57 +59,110 @@ public class ProxyNetworkService implements NetworkServiceInterface, PluginMessa
JsonObject body) {
CompletableFuture<NetworkResponse> future = new CompletableFuture<>();
// 1. Check if there is any player online (Plugin Message must be sent via a
// player)
Player player = com.google.common.collect.Iterables.getFirst(Bukkit.getOnlinePlayers(), null);
if (player == null) {
future.completeExceptionally(new IllegalStateException("No player online to proxy request to Velocity"));
return future;
try {
// 1. Prepare request data
ProxyNetworkRequest req = new ProxyNetworkRequest(method, endpoint, params, body);
String jsonPayload = gson.toJson(req);
// Safety check: Plugin Message single packet cannot be too large (conservative
// limit 30KB)
byte[] payloadBytes = jsonPayload.getBytes(StandardCharsets.UTF_8);
if (payloadBytes.length > 30000) {
future.completeExceptionally(
new IllegalArgumentException("Request body too large (>30KB). Protocol limit exceeded."));
return future;
}
// 2. Register Future
pendingRequests.put(req.getRequestId(), future);
// 3. Build packet
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(ProtocolConstants.API_REQUEST);
out.writeUTF(jsonPayload); // Length checked above, writeUTF is safe here
// 4. Try to send or queue
trySendOrQueue(out.toByteArray());
// 5. Set timeout (prevent Future from hanging forever)
plugin.getServer().getScheduler().runTaskLaterAsynchronously(plugin, () -> {
CompletableFuture<NetworkResponse> f = pendingRequests.remove(req.getRequestId());
if (f != null) {
f.complete(new NetworkResponse(504, "Proxy request timed out (Proxy didn't respond or no player online)"));
}
}, 100L); // 5 seconds timeout
} catch (Exception e) {
future.completeExceptionally(e);
}
// 2. Prepare the packet
ProxyNetworkRequest req = new ProxyNetworkRequest(method, endpoint, params, body);
pendingRequests.put(req.getRequestId(), future);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(ProtocolConstants.API_REQUEST); // Sub-channel
out.writeUTF(gson.toJson(req)); // Payload
// 3. Send
player.sendPluginMessage(plugin, CHANNEL, out.toByteArray());
// 4. Set timeout (prevent memory leak if Velocity does not respond)
plugin.getServer().getScheduler().runTaskLaterAsynchronously(plugin, () -> {
CompletableFuture<NetworkResponse> f = pendingRequests.remove(req.getRequestId());
if (f != null) {
f.complete(new NetworkResponse(504, "Proxy request timed out"));
}
}, 100L); // 5 seconds timeout
return future;
}
/**
* Core logic: send if there is a player online, otherwise queue
*/
private void trySendOrQueue(byte[] message) {
Player player = com.google.common.collect.Iterables.getFirst(Bukkit.getOnlinePlayers(), null);
if (player != null) {
player.sendPluginMessage(plugin, CHANNEL, message);
} else {
// No player online, add to queue
// plugin.getLogger().info("[Network] Request queued (No player online)");
messageQueue.add(message);
}
}
/**
* Only flush queued requests when a player joins
*/
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if (messageQueue.isEmpty())
return;
plugin.getLogger().info("[Network] Player joined. Flushing " + messageQueue.size() + " queued requests...");
// Delay 1 second to ensure player connection is stable
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
Player player = event.getPlayer();
// Double check: prevent player from leaving immediately after joining
if (player == null || !player.isOnline())
return;
while (!messageQueue.isEmpty()) {
byte[] msg = messageQueue.poll();
if (msg != null) {
player.sendPluginMessage(plugin, CHANNEL, msg);
}
}
}, 20L);
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
if (!channel.equals(CHANNEL))
return;
ByteArrayDataInput in = ByteStreams.newDataInput(message);
String subChannel = in.readUTF();
try {
ByteArrayDataInput in = ByteStreams.newDataInput(message);
String subChannel = in.readUTF();
if (subChannel.equals("API_RESP")) {
String reqId = in.readUTF();
boolean success = in.readBoolean();
String data = in.readUTF(); // If success: body, if failure: error message
if (subChannel.equals(ProtocolConstants.API_RESPONSE)) {
String reqId = in.readUTF();
boolean success = in.readBoolean();
String data = in.readUTF();
CompletableFuture<NetworkResponse> future = pendingRequests.remove(reqId);
if (future != null) {
if (success) {
future.complete(new NetworkResponse(200, data));
} else {
future.complete(new NetworkResponse(500, data));
CompletableFuture<NetworkResponse> future = pendingRequests.remove(reqId);
if (future != null) {
// If Proxy did not provide a specific status code, map simply here
int status = success ? 200 : 500;
future.complete(new NetworkResponse(status, data));
}
}
} catch (Exception e) {
plugin.getLogger().warning("[Network] Failed to parse plugin message: " + e.getMessage());
}
}
}

View File

@@ -10,6 +10,8 @@ import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import online.mineroo.common.BindRequest;
import online.mineroo.paper.MinerooCore;
import online.mineroo.paper.ProxyNetworkService;
import org.slf4j.Logger;
public class BindCommand {
@@ -46,6 +48,14 @@ public class BindCommand {
return Command.SINGLE_SUCCESS;
}
private void applyMotdToken(String motdToken) {
if (plugin.getNetworkService() instanceof ProxyNetworkService) {
sendMotdTokenToVelocity(motdToken);
} else {
plugin.getBindListener().setVerificationToken(motdToken);
}
}
private void sendMotdTokenToVelocity(String motdToken) {
// Send via any online player (Plugin Messaging must be attached to a player)
org.bukkit.entity.Player player = org.bukkit.Bukkit.getOnlinePlayers().stream().findFirst().orElse(null);
@@ -61,12 +71,15 @@ public class BindCommand {
// Run the binding process asynchronously to avoid blocking the main thread
org.bukkit.Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Logger logger = plugin.getSLF4JLogger();
BindRequest bindRequest = new BindRequest(
logger,
plugin.getNetworkService());
BindRequest bindRequest = plugin.getBindRequest();
try {
String address = plugin.getConfigObject().getServer().getServerBind().getAddress();
int port = plugin.getConfigObject().getServer().getServerBind().getPort();
String port = String.valueOf(plugin.getConfigObject().getServer().getServerBind().getPort());
if (plugin.getNetworkService() instanceof ProxyNetworkService) {
address = "$hostname";
port = "$port";
}
// 1. Check binding status
if (bindRequest.checkBindStatus(address, port).join()) {
@@ -80,7 +93,7 @@ public class BindCommand {
if (motdOk) {
String motdToken = bindRequest.getMotdToken();
// Send MOTD token to Velocity
sendMotdTokenToVelocity(motdToken);
applyMotdToken(motdToken);
context.getSource().getSender().sendMessage(plugin.getMessageManager().get("command.bind.server.wait"));
try {
Thread.sleep(2 * 60 * 1000 + 5000); // 2m 5s
@@ -124,7 +137,7 @@ public class BindCommand {
plugin.getMessageManager().get("command.bind.server.failed"));
} finally {
// After binding is complete, you can send a message to clear the MOTD token
sendMotdTokenToVelocity(""); // Clear MOTD
applyMotdToken(""); // Clear MOTD
}
});
return Command.SINGLE_SUCCESS;

View File

@@ -0,0 +1,34 @@
package online.mineroo.paper.listeners;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.server.ServerListPingEvent;
import net.kyori.adventure.text.Component;
import java.util.concurrent.atomic.AtomicReference;
public class BindListener implements Listener {
private final AtomicReference<String> verificationToken = new AtomicReference<>(null);
public void setVerificationToken(String token) {
if (token == null || token.isEmpty()) {
this.verificationToken.set(null);
} else {
this.verificationToken.set(token);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onServerListPing(ServerListPingEvent event) {
String token = verificationToken.get();
if (token == null || token.isEmpty()) {
return;
}
event.motd(Component.text(token));
}
}

View File

@@ -0,0 +1,175 @@
package online.mineroo.paper.listeners;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
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.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jspecify.annotations.NullMarked;
import online.mineroo.common.BindRequest.PlayerBindStatus;
import online.mineroo.common.MessageManager;
import online.mineroo.paper.Config;
import online.mineroo.paper.MinerooCore;
import online.mineroo.paper.utils.PlayerBindDialog;
@NullMarked
public class PlayerBindListener implements Listener {
private final MinerooCore plugin;
private Set<UUID> pendingBindPlayers = new HashSet<>();
public PlayerBindListener(MinerooCore plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
MessageManager messageManager = this.plugin.getMessageManager();
Config config = this.plugin.getConfigObject();
Player player = event.getPlayer();
boolean isForceBind = config.getPlayer().getPlayerBind().isRequired();
if (isForceBind) {
lockPlayer(player);
}
this.plugin.getBindRequest().checkPlayerBindStatus(player.getUniqueId(), null)
.thenAccept(response -> {
// 4. 切回主线程 (Bukkit API 必须在主线程调用)
// 这一步非常重要,否则报错 "Asynchronous player tracker update"
this.plugin.getServer().getScheduler().runTask(this.plugin, () -> {
// 检查玩家是否还在同一个服务器 (防止请求期间玩家退出了)
if (!player.isOnline())
return;
PlayerBindStatus status = response.getStatus();
if (status == PlayerBindStatus.BOUND) {
if (isForceBind) {
unlockPlayer(player);
}
} else {
if (status == PlayerBindStatus.ERROR) {
this.plugin.getLogger()
.warning("Bind check failed for " + player.getName() + ": " + response.getMessage());
}
PlayerBindDialog dialog = new PlayerBindDialog();
player.showDialog(dialog.getDialog(messageManager));
}
});
});
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
if (!pendingBindPlayers.contains(player.getUniqueId())) {
return;
}
Location from = event.getFrom();
Location to = event.getTo();
if (from.getX() == to.getX() && from.getY() == to.getY() && from.getZ() == to.getZ()) {
return;
}
Location newLocation = from.clone();
newLocation.setYaw(to.getYaw());
newLocation.setPitch(to.getPitch());
event.setTo(newLocation);
}
@EventHandler
public void onInteract(PlayerInteractEvent event) {
if (isLocked(event.getPlayer()))
event.setCancelled(true);
}
@EventHandler
public void onDrop(PlayerDropItemEvent event) {
if (isLocked(event.getPlayer()))
event.setCancelled(true);
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (event.getWhoClicked() instanceof Player player && isLocked(player)) {
event.setCancelled(true);
}
}
@EventHandler
public void onDamage(EntityDamageEvent event) {
if (event.getEntity() instanceof Player player && isLocked(player)) {
event.setCancelled(true);
}
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (isLocked(event.getPlayer()))
event.setCancelled(true);
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
if (isLocked(event.getPlayer()))
event.setCancelled(true);
}
@EventHandler
public void onChat(io.papermc.paper.event.player.AsyncChatEvent event) {
if (isLocked(event.getPlayer()))
event.setCancelled(true);
}
private boolean isLocked(Player player) {
return pendingBindPlayers.contains(player.getUniqueId());
}
private void lockPlayer(Player player) {
pendingBindPlayers.add(player.getUniqueId());
player.setWalkSpeed(0f);
player.setFlySpeed(0f);
// player.addPotionEffect(new PotionEffect(PotionEffectType.JUMP,
// Integer.MAX_VALUE, 200, false, false));
player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, Integer.MAX_VALUE, 1, false, false));
}
public void unlockPlayer(Player player) {
if (pendingBindPlayers.remove(player.getUniqueId())) {
player.setWalkSpeed(0.2f);
player.setFlySpeed(0.1f);
// player.removePotionEffect(PotionEffectType.JUMP);
player.removePotionEffect(PotionEffectType.BLINDNESS);
}
}
}

View File

@@ -1,9 +0,0 @@
package online.mineroo.paper.listeners;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerJoinListener implements Listener {
}

View File

@@ -5,14 +5,15 @@ package online.mineroo.velocity;
* Handles plugin initialization, configuration loading, and provides access to core components.
*/
import com.google.inject.Inject;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import online.mineroo.common.MessageManager;
import online.mineroo.common.ProtocolConstants;
import online.mineroo.velocity.listeners.BindListener;
import java.io.IOException;
@@ -27,7 +28,7 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
@Plugin(id = "mineroo-velocity", name = "Mineroo Velocity", version = "0.1.0-SNAPSHOT", url = "https://mineroo.online", description = "Mineroo main plugin", authors = {
"YuKun Liu" })
public class MinerooCore {
public class MinerooVelocity {
// Reference to the Velocity ProxyServer instance
private final ProxyServer server;
@@ -53,7 +54,7 @@ public class MinerooCore {
* @param dataDirectory Directory for plugin data
*/
@Inject
public MinerooCore(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
public MinerooVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
@@ -61,16 +62,19 @@ public class MinerooCore {
@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
// Register event listeners
this.bindListener = new BindListener();
server.getEventManager().register(this, bindListener);
// Register ChannelListener to handle cross-platform MOTD token messages
server.getEventManager().register(this, new online.mineroo.velocity.listeners.ChannelListener(this));
// Load configuration
reloadConfig();
server.getChannelRegistrar().register(MinecraftChannelIdentifier.from(ProtocolConstants.PROTOCOL_CHANNEL));
// Register ChannelListener to handle cross-platform MOTD token messages
server.getEventManager().register(this, new online.mineroo.velocity.listeners.ChannelListener(this));
// Initialize message manager
this.messageManager = new MessageManager();
}

View File

@@ -1,200 +0,0 @@
// package online.mineroo.velocity.commands;
//
// import java.util.List;
// import java.util.Optional;
// import java.util.concurrent.CompletionException;
// import java.util.stream.Collectors;
// import java.util.stream.Stream;
//
// import org.slf4j.Logger;
//
// import com.velocitypowered.api.command.CommandSource;
// import com.velocitypowered.api.command.SimpleCommand;
//
// import net.kyori.adventure.text.Component;
// import net.kyori.adventure.text.format.NamedTextColor;
// import online.mineroo.common.MessageManager;
// import online.mineroo.common.NetworkServiceInterface;
// import online.mineroo.velocity.Config;
// import online.mineroo.velocity.MinerooCore;
// import online.mineroo.common.BindRequest;
// import online.mineroo.common.HttpNetworkService;
//
// public class MainCommand implements SimpleCommand {
//
// private final MinerooCore plugin;
//
// public MainCommand(MinerooCore plugin) {
// this.plugin = plugin;
// }
//
// @Override
// public void execute(Invocation invocation) {
// String[] args = invocation.arguments();
// if (args.length < 1) {
// sendHelp(invocation.source());
// return;
// }
//
// String subCommand = args[0];
//
// if (subCommand.equals("bind")) {
// bindServer(invocation.source());
// }
//
// if (subCommand.equalsIgnoreCase("reload")) {
// reload(invocation.source());
// }
//
// }
//
// @Override
// public List<String> suggest(Invocation invocation) {
// String[] args = invocation.arguments();
//
// if (args.length == 0) {
// return List.of("bind", "reload");
// }
//
// if (args.length == 1) {
// String input = args[0].toLowerCase();
// return Stream.of("bind", "reload")
// .filter(cmd -> cmd.startsWith(input))
// .collect(Collectors.toList());
// }
//
// return List.of();
// }
//
// public void reload(CommandSource source) {
// MessageManager msg = plugin.getMessageManager();
//
// long start = System.currentTimeMillis();
// boolean success = plugin.reloadConfig();
//
// if (success) {
// long time = System.currentTimeMillis() - start;
// source.sendMessage(msg.get("command.reload.success", "time",
// String.valueOf(time)));
// } else {
//
// source.sendMessage(msg.get("command.reload.failed"));
// }
// return;
// }
//
// public void bindServer(CommandSource source) {
//
// Config config = plugin.getConfig();
// MessageManager msg = plugin.getMessageManager();
//
// String bind_name = config.getServerName();
// String bind_description = config.getDescription();
//
// String bind_token = config.getBind().getBindToken();
// String bind_address = config.getBind().getAddress();
//
// int bind_port =
// Optional.ofNullable(config.getBind().getPort()).orElse(25565);
//
// source.sendMessage(msg.get("command.bind.server.start"));
//
// Logger logger = plugin.getLogger();
//
// // Run in a separate worker thread to allow blocking (Thread.sleep)
// plugin.getServer().getScheduler().buildTask(plugin, () -> {
//
// // 1. Initialize the Network Service locally (Velocity connects directly)
// NetworkServiceInterface networkService = new HttpNetworkService(
// "https://oapi.mineroo.online",
// "mserver",
// bind_token);
//
// BindRequest request = new BindRequest(logger, networkService);
//
// try {
//
// // 2. Check Status
// // We use .join() to block the thread until the Future completes.
// if (request.checkBindStatus(bind_address, bind_port).join()) {
// source.sendMessage(msg.get("command.bind.server.already-bound"));
// return;
// }
//
// // 3. Initial Verification
// boolean inital_bind = request.initialMotdVerifyRequest(bind_address,
// bind_port).join();
//
// if (inital_bind) {
// String motdToken = request.getMotdToken();
// plugin.getBindListener().setVerificationToken(motdToken);
//
// source.sendMessage(msg.get("command.bind.server.wait"));
//
// // 4. Wait for external refresh (Blocking this worker thread is intended)
// try {
// Thread.sleep(2 * 60 * 1000 + 5000); // 2m 5s
// } catch (InterruptedException e) {
// logger.error("Bind thread interrupted", e);
// return;
// }
//
// // 5. Final Bind & Retry Loop
// boolean success = false;
// int maxRetries = 3;
//
// for (int i = 1; i <= maxRetries; i++) {
// // Check final status synchronously using join()
// if (request.finalizeServerBinding(bind_name, bind_description).join()) {
// success = true;
// break;
// }
//
// if (i < maxRetries) {
// source.sendMessage(msg.get("command.bind.server.retry",
// "current", String.valueOf(i),
// "max", String.valueOf(maxRetries)));
//
// try {
// Thread.sleep(10000); // Wait 10s before retry
// } catch (InterruptedException e) {
// break;
// }
// }
// }
//
// if (success) {
// source.sendMessage(msg.get("command.bind.server.success"));
// } else {
// source.sendMessage(msg.get("command.bind.server.failed"));
// }
//
// } else {
// source.sendMessage(
// Component.text("Initial handshake failed. Check your token or network.",
// NamedTextColor.RED));
// }
//
// } catch (CompletionException e) {
// // Unpack the wrapper exception from join() to see the real network error
// logger.error("Mineroo Bind Network Error: " + e.getCause().getMessage());
// } catch (Exception e) {
// logger.error("Mineroo Bind Error: " + e.toString());
// } finally {
// plugin.getBindListener().clearVerificationToken();
// }
//
// }).schedule();
// }
//
// public void sendHelp(CommandSource source) {
// source.sendMessage(Component.text("=== # @Mineroo # ===",
// NamedTextColor.AQUA));
// source.sendMessage(Component.text(" /mineroo bind - Start to bind server",
// NamedTextColor.YELLOW));
// source.sendMessage(Component.text(" /mineroo reload - Reload config",
// NamedTextColor.YELLOW));
// source.sendMessage(Component.text("=== mineroo.online ===",
// NamedTextColor.AQUA));
// }
// }

View File

@@ -23,7 +23,7 @@ public class BindListener {
public void onPing(ProxyPingEvent event) {
String token = verificationToken.get();
if (token == null) {
if (token == null || token.isEmpty()) {
return;
}

View File

@@ -1,36 +1,208 @@
package online.mineroo.velocity.listeners;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import online.mineroo.common.*;
import online.mineroo.velocity.MinerooVelocity;
import online.mineroo.common.ProtocolConstants;
import online.mineroo.velocity.MinerooCore;
import java.util.concurrent.CompletableFuture;
public class ChannelListener {
private final MinerooCore plugin;
public ChannelListener(MinerooCore plugin) {
private final MinerooVelocity plugin;
private final Gson gson = new Gson();
private final HttpNetworkService httpService;
public ChannelListener(MinerooVelocity plugin) {
this.plugin = plugin;
String apiUrl = "https://oapi.mineroo.online";
this.httpService = new HttpNetworkService(apiUrl, "mserver", plugin.getConfig().getBind().getBindToken());
}
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
// Only handle messages from the specified protocol channel
if (!event.getIdentifier().getId().equals(ProtocolConstants.PROTOCOL_CHANNEL))
return;
// Only handle messages from a backend server
if (!(event.getSource() instanceof ServerConnection))
return;
ServerConnection backendServer = (ServerConnection) event.getSource();
ByteArrayDataInput in = event.dataAsDataStream();
String subChannel = in.readUTF();
if (subChannel.equals(ProtocolConstants.BIND_MOTD_TOKEN)) {
String token = in.readUTF();
plugin.getBindListener().setVerificationToken(token);
try {
String subChannel = in.readUTF();
if (subChannel.equals(ProtocolConstants.API_REQUEST)) {
handleApiRequest(in, backendServer);
}
if (subChannel.equals(ProtocolConstants.BIND_MOTD_TOKEN)) {
String token = in.readUTF();
plugin.getBindListener().setVerificationToken(token);
}
} catch (Exception e) {
plugin.getLogger().error("Failed to handle plugin message", e);
}
}
}
private void handleApiRequest(ByteArrayDataInput in, ServerConnection backendServer) {
String reqId = "unknown";
try {
// 1. Parse the request
String jsonPayload = in.readUTF();
ProxyNetworkRequest req = gson.fromJson(jsonPayload, ProxyNetworkRequest.class);
reqId = req.getRequestId();
CompletableFuture<NetworkResponse> future;
// 2. Delegate to HttpNetworkService for processing
if ("POST".equalsIgnoreCase(req.getMethod())) {
JsonObject bodyJson = null;
try {
JsonObject originalJson = gson.fromJson(req.getJsonBody(), JsonObject.class);
if (originalJson == null)
originalJson = new JsonObject();
bodyJson = resolvePlacehodersInJson(originalJson).getAsJsonObject();
plugin.getLogger().info("Proxy POST [ " + req.getEndpoint() + " ]\n\t" + bodyJson.toString());
} catch (Exception e) {
plugin.getLogger().error("JSON parsing error", e);
bodyJson = new JsonObject();
}
future = httpService.postData(req.getEndpoint(), bodyJson);
} else {
// replace placeholders
java.util.Map<String, String> finalParams = new java.util.HashMap<>();
if (req.getParams() != null) {
for (java.util.Map.Entry<String, String> entry : req.getParams().entrySet()) {
String originalKey = entry.getKey();
String originalValue = entry.getValue();
String replacedValue = resolvePlaceholders(originalValue);
finalParams.put(originalKey, replacedValue);
}
}
plugin.getLogger().info("Proxy GET [ " + req.getEndpoint() + " ]\n\t" + finalParams.toString());
future = httpService.getData(req.getEndpoint(), finalParams);
}
// 3. Handle the result and send back the response
String finalReqId = reqId;
future.thenAccept(response -> {
// Check if HTTP status code is 2xx
boolean success = response.getStatusCode() >= 200 && response.getStatusCode() < 300;
sendResponse(backendServer, finalReqId, success, response.getBody());
}).exceptionally(e -> {
plugin.getLogger().error("API Proxy Request Failed", e);
sendResponse(backendServer, finalReqId, false, "Proxy Error: " + e.getMessage());
return null;
});
} catch (Exception e) {
plugin.getLogger().error("Error parsing API request", e);
sendResponse(backendServer, reqId, false, "Protocol Error");
}
}
private void sendResponse(ServerConnection server, String reqId, boolean success, String data) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(ProtocolConstants.API_RESPONSE);
out.writeUTF(reqId);
out.writeBoolean(success);
out.writeUTF(data != null ? data : "");
server.sendPluginMessage(MinecraftChannelIdentifier.from(ProtocolConstants.PROTOCOL_CHANNEL), out.toByteArray());
}
private JsonElement resolvePlacehodersInJson(JsonElement element) {
if (element.isJsonPrimitive()) {
JsonPrimitive primitive = element.getAsJsonPrimitive();
if (primitive.isString()) {
String original = primitive.getAsString();
if ("$port".equals(original)) {
Integer port = null;
if (plugin.getConfig().getBind() != null) {
port = plugin.getConfig().getBind().getPort();
}
if (port == null || port <= 0) {
return null;
}
return new JsonPrimitive(port);
}
if ("$timestamp".equals(original)) {
return new JsonPrimitive(System.currentTimeMillis());
}
String replaced = resolvePlaceholders(original);
return new JsonPrimitive(replaced);
}
return element;
}
if (element.isJsonObject()) {
JsonObject obj = element.getAsJsonObject();
JsonObject newObj = new JsonObject();
for (java.util.Map.Entry<String, JsonElement> entry : obj.entrySet()) {
JsonElement processedValue = resolvePlacehodersInJson(entry.getValue());
if (processedValue != null) {
newObj.add(entry.getKey(), processedValue);
}
}
return newObj;
}
if (element.isJsonArray()) {
JsonArray array = element.getAsJsonArray();
JsonArray newArray = new JsonArray();
for (JsonElement item : array) {
JsonElement processedItem = resolvePlacehodersInJson(item);
if (processedItem != null) {
newArray.add(processedItem);
}
}
return newArray;
}
return element;
}
private String resolvePlaceholders(String input) {
if (input == null)
return null;
String hostname = plugin.getConfig().getBind().getAddress();
String port = String.valueOf(plugin.getConfig().getBind().getPort());
return input
.replace("$hostname", hostname != null ? hostname : "unknown")
.replace("$port", port)
.replace("$timestamp", String.valueOf(System.currentTimeMillis()));
}
}