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

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 {
}