From ec697a133f8c010e32f0c3750c73dba45b1c7aa5 Mon Sep 17 00:00:00 2001
From: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
Date: Sat, 5 Jul 2025 23:58:46 -0700
Subject: [PATCH] Add support for transfers; closes #81
Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
---
pom.xml | 2 +-
src/main/java/com/loohp/limbo/Limbo.java | 28 ++++++
.../loohp/limbo/commands/DefaultCommands.java | 92 +++++++++++++++++++
.../packets/ClientboundTransferPacket.java | 59 ++++++++++++
.../java/com/loohp/limbo/player/Player.java | 10 ++
.../loohp/limbo/registry/PacketRegistry.java | 2 +
src/main/resources/permission.yml | 1 +
7 files changed, 193 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/com/loohp/limbo/network/protocol/packets/ClientboundTransferPacket.java
diff --git a/pom.xml b/pom.xml
index dbd4049..17d6cfc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
com.loohp
Limbo
Limbo
- 0.7.15-ALPHA
+ 0.7.16-ALPHA
Standalone Limbo Minecraft Server.
https://github.com/LOOHP/Limbo
diff --git a/src/main/java/com/loohp/limbo/Limbo.java b/src/main/java/com/loohp/limbo/Limbo.java
index 80c86ac..05cfc96 100644
--- a/src/main/java/com/loohp/limbo/Limbo.java
+++ b/src/main/java/com/loohp/limbo/Limbo.java
@@ -41,6 +41,7 @@ import com.loohp.limbo.player.Player;
import com.loohp.limbo.plugins.LimboPlugin;
import com.loohp.limbo.plugins.PluginManager;
import com.loohp.limbo.scheduler.LimboScheduler;
+import com.loohp.limbo.scheduler.LimboTask;
import com.loohp.limbo.scheduler.Tick;
import com.loohp.limbo.utils.CustomStringUtils;
import com.loohp.limbo.utils.ImageUtils;
@@ -542,4 +543,31 @@ public final class Limbo {
}
}
+ public void transferAll(String host, int port, int batchDelayTicks, int batchSize) {
+ if (batchDelayTicks <= 0 || batchSize <= 0) {
+ for (Player player : getPlayers()) {
+ player.transfer(host, port);
+ }
+ } else {
+ long totalDelay = 0;
+ Set currentBatch = new HashSet<>();
+ List players = new ArrayList<>(getPlayers());
+
+ for (int i = 0; i < players.size(); i++) {
+ currentBatch.add(players.get(i));
+ if (currentBatch.size() < batchSize && i != players.size() - 1) {
+ continue;
+ }
+ Set batch = new HashSet<>(currentBatch);
+ LimboTask task = () -> {
+ for (Player p : batch) {
+ p.transfer(host, port);
+ }
+ };
+ Limbo.getInstance().getScheduler().runTaskLater(null, task, totalDelay);
+ totalDelay += batchDelayTicks;
+ currentBatch.clear();
+ }
+ }
+ }
}
diff --git a/src/main/java/com/loohp/limbo/commands/DefaultCommands.java b/src/main/java/com/loohp/limbo/commands/DefaultCommands.java
index ce42507..03778ef 100644
--- a/src/main/java/com/loohp/limbo/commands/DefaultCommands.java
+++ b/src/main/java/com/loohp/limbo/commands/DefaultCommands.java
@@ -180,6 +180,77 @@ public class DefaultCommands implements CommandExecutor, TabCompletor {
}
return;
}
+
+ if (args[0].equalsIgnoreCase("transfer")) {
+ if (sender.hasPermission("limboserver.transfer")) {
+ if (args.length == 4) {
+ Player player = Limbo.getInstance().getPlayer(args[1]);
+ if (player != null) {
+ String host = args[2];
+ int port;
+ try {
+ port = Integer.parseInt(args[3]);
+ } catch (NumberFormatException e) {
+ sender.sendMessage(ChatColor.RED + "Invalid port number!");
+ return;
+ }
+ player.transfer(host, port);
+ sender.sendMessage(ChatColor.GOLD + "Transferred " + player.getName() + " to " + host + ":" + port);
+ } else {
+ sender.sendMessage(ChatColor.RED + "Player is not online!");
+ }
+ } else {
+ sender.sendMessage(ChatColor.RED + "Invalid usage! Use: /transfer ");
+ }
+ } else {
+ sender.sendMessage(ChatColor.RED + "You do not have permission to use that command!");
+ }
+ return;
+ }
+
+ if (args[0].equalsIgnoreCase("transferall")) {
+ if (sender.hasPermission("limboserver.transfer")) {
+ if (args.length >= 3 && args.length <= 5) {
+ String host = args[1];
+ int port;
+ int batchDelay;
+ int batchSize;
+ try {
+ port = Integer.parseInt(args[2]);
+ } catch (NumberFormatException e) {
+ sender.sendMessage(ChatColor.RED + "Invalid port!");
+ return;
+ }
+ if (args.length == 4) {
+ try {
+ batchDelay = Integer.parseInt(args[3]);
+ } catch (NumberFormatException e) {
+ sender.sendMessage(ChatColor.RED + "Invalid batch delay ticks!");
+ return;
+ }
+ batchSize = 1;
+ } else if (args.length == 5) {
+ try {
+ batchDelay = Integer.parseInt(args[3]);
+ batchSize = Integer.parseInt(args[4]);
+ } catch (NumberFormatException e) {
+ sender.sendMessage(ChatColor.RED + "Invalid batch delay ticks or size!");
+ return;
+ }
+ } else {
+ batchDelay = 1;
+ batchSize = 10;
+ }
+ Limbo.getInstance().transferAll(host, port, batchDelay, batchSize);
+ sender.sendMessage(ChatColor.GOLD + "Transferred all players to " + host + ":" + port);
+ } else {
+ sender.sendMessage(ChatColor.RED + "Invalid usage! Use: /transferall [ ]");
+ }
+ } else {
+ sender.sendMessage(ChatColor.RED + "You do not have permission to use that command!");
+ }
+ return;
+ }
}
@Override
@@ -202,6 +273,10 @@ public class DefaultCommands implements CommandExecutor, TabCompletor {
if (sender.hasPermission("limboserver.gamemode")) {
tab.add("gamemode");
}
+ if (sender.hasPermission("limboserver.transfer")) {
+ tab.add("transfer");
+ tab.add("transferall");
+ }
break;
case 1:
if (sender.hasPermission("limboserver.spawn")) {
@@ -229,6 +304,14 @@ public class DefaultCommands implements CommandExecutor, TabCompletor {
tab.add("gamemode");
}
}
+ if (sender.hasPermission("limboserver.transfer")) {
+ if ("transfer".startsWith(args[0].toLowerCase())) {
+ tab.add("transfer");
+ }
+ if ("transferall".startsWith(args[0].toLowerCase())) {
+ tab.add("transferall");
+ }
+ }
break;
case 2:
if (sender.hasPermission("limboserver.kick")) {
@@ -249,6 +332,15 @@ public class DefaultCommands implements CommandExecutor, TabCompletor {
}
}
}
+ if (sender.hasPermission("limboserver.transfer")) {
+ if (args[0].equalsIgnoreCase("transfer")) {
+ for (Player player : Limbo.getInstance().getPlayers()) {
+ if (player.getName().toLowerCase().startsWith(args[1].toLowerCase())) {
+ tab.add(player.getName());
+ }
+ }
+ }
+ }
break;
case 3:
if (sender.hasPermission("limboserver.gamemode")) {
diff --git a/src/main/java/com/loohp/limbo/network/protocol/packets/ClientboundTransferPacket.java b/src/main/java/com/loohp/limbo/network/protocol/packets/ClientboundTransferPacket.java
new file mode 100644
index 0000000..95ed6e6
--- /dev/null
+++ b/src/main/java/com/loohp/limbo/network/protocol/packets/ClientboundTransferPacket.java
@@ -0,0 +1,59 @@
+/*
+ * This file is part of Limbo.
+ *
+ * Copyright (C) 2022. LoohpJames
+ * Copyright (C) 2022. Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.loohp.limbo.network.protocol.packets;
+
+import com.loohp.limbo.registry.PacketRegistry;
+import com.loohp.limbo.utils.DataTypeIO;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class ClientboundTransferPacket extends PacketOut {
+
+ private final String host;
+ private final int port;
+
+ public ClientboundTransferPacket(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ @Override
+ public byte[] serializePacket() throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ DataOutputStream output = new DataOutputStream(buffer);
+ output.writeByte(PacketRegistry.getPacketId(getClass()));
+ DataTypeIO.writeString(output, host, StandardCharsets.UTF_8);
+ DataTypeIO.writeVarInt(output, port);
+
+ return buffer.toByteArray();
+ }
+}
diff --git a/src/main/java/com/loohp/limbo/player/Player.java b/src/main/java/com/loohp/limbo/player/Player.java
index a3885f4..fe3db32 100644
--- a/src/main/java/com/loohp/limbo/player/Player.java
+++ b/src/main/java/com/loohp/limbo/player/Player.java
@@ -45,6 +45,7 @@ import com.loohp.limbo.network.protocol.packets.ClientboundSetSubtitleTextPacket
import com.loohp.limbo.network.protocol.packets.ClientboundSetTitleTextPacket;
import com.loohp.limbo.network.protocol.packets.ClientboundSetTitlesAnimationPacket;
import com.loohp.limbo.network.protocol.packets.ClientboundSystemChatPacket;
+import com.loohp.limbo.network.protocol.packets.ClientboundTransferPacket;
import com.loohp.limbo.network.protocol.packets.PacketOut;
import com.loohp.limbo.network.protocol.packets.PacketPlayOutCloseWindow;
import com.loohp.limbo.network.protocol.packets.PacketPlayOutGameStateChange;
@@ -650,4 +651,13 @@ public class Player extends LivingEntity implements CommandSender, InventoryHold
public InventoryHolder getHolder() {
return this;
}
+
+ public void transfer(String host, int port) {
+ try {
+ ClientboundTransferPacket transferPacket = new ClientboundTransferPacket(host, port);
+ clientConnection.sendPacket(transferPacket);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
diff --git a/src/main/java/com/loohp/limbo/registry/PacketRegistry.java b/src/main/java/com/loohp/limbo/registry/PacketRegistry.java
index 89a0c02..ddcd346 100644
--- a/src/main/java/com/loohp/limbo/registry/PacketRegistry.java
+++ b/src/main/java/com/loohp/limbo/registry/PacketRegistry.java
@@ -35,6 +35,7 @@ import com.loohp.limbo.network.protocol.packets.ClientboundSetSubtitleTextPacket
import com.loohp.limbo.network.protocol.packets.ClientboundSetTitleTextPacket;
import com.loohp.limbo.network.protocol.packets.ClientboundSetTitlesAnimationPacket;
import com.loohp.limbo.network.protocol.packets.ClientboundSystemChatPacket;
+import com.loohp.limbo.network.protocol.packets.ClientboundTransferPacket;
import com.loohp.limbo.network.protocol.packets.Packet;
import com.loohp.limbo.network.protocol.packets.PacketHandshakingIn;
import com.loohp.limbo.network.protocol.packets.PacketLoginInLoginStart;
@@ -220,6 +221,7 @@ public class PacketRegistry {
registerClass(PacketPlayOutWindowData.class, "minecraft:container_set_data", NetworkPhase.PLAY, PacketBound.CLIENTBOUND);
registerClass(ClientboundChunkBatchFinishedPacket.class, "minecraft:chunk_batch_finished", NetworkPhase.PLAY, PacketBound.CLIENTBOUND);
registerClass(ClientboundChunkBatchStartPacket.class, "minecraft:chunk_batch_start", NetworkPhase.PLAY, PacketBound.CLIENTBOUND);
+ registerClass(ClientboundTransferPacket.class, "minecraft:transfer", NetworkPhase.PLAY, PacketBound.CLIENTBOUND);
}
private static void registerClass(Class extends Packet> packetClass, String key, NetworkPhase networkPhase, PacketBound packetBound) {
diff --git a/src/main/resources/permission.yml b/src/main/resources/permission.yml
index b6b2334..1dc025e 100644
--- a/src/main/resources/permission.yml
+++ b/src/main/resources/permission.yml
@@ -4,6 +4,7 @@ groups:
- limboserver.kick
- limboserver.say
- limboserver.gamemode
+ - limboserver.transfer
default:
- limboserver.spawn
- limboserver.chat