feat: use clang-format
This commit is contained in:
9
.clang-format
Normal file
9
.clang-format
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
Language: Java
|
||||
BasedOnStyle: Google
|
||||
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
|
||||
ColumnLimit: 100
|
||||
BinPackArguments: false
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
@@ -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);
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
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. Prepare the packet
|
||||
ProxyNetworkRequest req = new ProxyNetworkRequest(method, endpoint, params, body);
|
||||
// 2. Register Future
|
||||
pendingRequests.put(req.getRequestId(), future);
|
||||
|
||||
// 3. Build packet
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF(ProtocolConstants.API_REQUEST); // Sub-channel
|
||||
out.writeUTF(gson.toJson(req)); // Payload
|
||||
out.writeUTF(ProtocolConstants.API_REQUEST);
|
||||
out.writeUTF(jsonPayload); // Length checked above, writeUTF is safe here
|
||||
|
||||
// 3. Send
|
||||
player.sendPluginMessage(plugin, CHANNEL, out.toByteArray());
|
||||
// 4. Try to send or queue
|
||||
trySendOrQueue(out.toByteArray());
|
||||
|
||||
// 4. Set timeout (prevent memory leak if Velocity does not respond)
|
||||
// 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"));
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
ByteArrayDataInput in = ByteStreams.newDataInput(message);
|
||||
String subChannel = in.readUTF();
|
||||
|
||||
if (subChannel.equals("API_RESP")) {
|
||||
if (subChannel.equals(ProtocolConstants.API_RESPONSE)) {
|
||||
String reqId = in.readUTF();
|
||||
boolean success = in.readBoolean();
|
||||
String data = in.readUTF(); // If success: body, if failure: error message
|
||||
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));
|
||||
}
|
||||
}
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
// }
|
||||
// }
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user