├── USAGE.md ├── EXPLANATIONS.md ├── img └── schema.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── example ├── hub │ ├── config.json │ ├── info.json │ └── groups.json ├── creative │ └── info.json ├── survival1 │ └── info.json └── survival2 │ └── info.json ├── src └── main │ ├── resources │ ├── assets │ │ └── servers-link │ │ │ └── icon.png │ ├── servers-link.mixins.json │ └── fabric.mod.json │ └── java │ └── io │ └── github │ └── kgriff0n │ ├── packet │ ├── server │ │ ├── ServerStopPacket.java │ │ ├── PreventConnectPacket.java │ │ ├── PreventDisconnectPacket.java │ │ ├── PlayerDataSyncPacket.java │ │ ├── UpdateWhitelistPacket.java │ │ ├── PlayerAcknowledgementPacket.java │ │ ├── UpdateRolesPacket.java │ │ └── PlayerDataPacket.java │ ├── info │ │ ├── ServersInfoPacket.java │ │ ├── ServerStatusPacket.java │ │ ├── NewServerPacket.java │ │ └── NewPlayerPacket.java │ ├── Packet.java │ └── play │ │ ├── SystemChatPacket.java │ │ ├── PlayerChatPacket.java │ │ ├── PlayerDisconnectPacket.java │ │ ├── TeleportationAcceptPacket.java │ │ ├── TeleportationRequestPacket.java │ │ ├── PlayerTransferPacket.java │ │ └── CommandPacket.java │ ├── mixin │ ├── PlayerManagerAccessor.java │ ├── EntitySelectorMixin.java │ ├── CommandManagerMixin.java │ ├── ServerPlayNetworkHandlerMixin.java │ ├── MinecraftServerMixin.java │ ├── PlayerManagerMixin.java │ └── PlayerEntityMixin.java │ ├── util │ ├── DummyPlayer.java │ ├── IPlayerServersLink.java │ └── PlayerData.java │ ├── event │ ├── ServerStopped.java │ ├── ServerStart.java │ ├── ServerStopping.java │ ├── PlayerDisconnect.java │ ├── ServerTick.java │ └── PlayerJoin.java │ ├── server │ ├── Group.java │ ├── Settings.java │ └── ServerInfo.java │ ├── PlayersInformation.java │ ├── ServersLink.java │ ├── socket │ ├── SubServer.java │ ├── G2SConnection.java │ └── Gateway.java │ ├── api │ └── ServersLinkApi.java │ └── command │ └── ServerCommand.java ├── settings.gradle ├── .gitattributes ├── .gitignore ├── gradle.properties ├── LICENSE ├── .github └── workflows │ └── build.yml ├── gradlew.bat ├── gradlew └── README.md /USAGE.md: -------------------------------------------------------------------------------- 1 | # TODO -------------------------------------------------------------------------------- /EXPLANATIONS.md: -------------------------------------------------------------------------------- 1 | # TODO -------------------------------------------------------------------------------- /img/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kgriff0n/servers-link/HEAD/img/schema.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kgriff0n/servers-link/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/hub/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": false, 3 | "whitelist_ip": false, 4 | "whitelisted_ip": [], 5 | "reconnect_last_server": true 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/servers-link/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kgriff0n/servers-link/HEAD/src/main/resources/assets/servers-link/icon.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } -------------------------------------------------------------------------------- /example/hub/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": "global", 3 | "gateway": true, 4 | "gateway-ip": "127.0.0.1", 5 | "gateway-port": 59001, 6 | "server-name": "Hub", 7 | "server-ip": "127.0.0.1", 8 | "server-port": 25565 9 | } -------------------------------------------------------------------------------- /example/creative/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": "creative", 3 | "gateway": false, 4 | "gateway-ip": "127.0.0.1", 5 | "gateway-port": 59001, 6 | "server-name": "Creative", 7 | "server-ip": "127.0.0.1", 8 | "server-port": 25566 9 | } -------------------------------------------------------------------------------- /example/survival1/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": "survival", 3 | "gateway": false, 4 | "gateway-ip": "127.0.0.1", 5 | "gateway-port": 59001, 6 | "server-name": "Survival-1", 7 | "server-ip": "127.0.0.1", 8 | "server-port": 25567 9 | } -------------------------------------------------------------------------------- /example/survival2/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": "survival", 3 | "gateway": false, 4 | "gateway-ip": "127.0.0.1", 5 | "gateway-port": 59001, 6 | "server-name": "Survival-2", 7 | "server-ip": "127.0.0.1", 8 | "server-port": 25568 9 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | remappedSrc/ 35 | 36 | # java 37 | 38 | hs_err_*.log 39 | replay_*.log 40 | *.hprof 41 | *.jfr 42 | logs/ 43 | -------------------------------------------------------------------------------- /src/main/resources/servers-link.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "io.github.kgriff0n.mixin", 4 | "compatibilityLevel": "JAVA_21", 5 | "mixins": [ 6 | "CommandManagerMixin", 7 | "EntitySelectorMixin", 8 | "MinecraftServerMixin", 9 | "PlayerEntityMixin", 10 | "PlayerManagerAccessor", 11 | "PlayerManagerMixin", 12 | "ServerPlayNetworkHandlerMixin" 13 | ], 14 | "injectors": { 15 | "defaultRequire": 1 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/ServerStopPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.packet.Packet; 4 | 5 | import static io.github.kgriff0n.ServersLink.IS_RUNNING; 6 | import static io.github.kgriff0n.ServersLink.SERVER; 7 | 8 | public class ServerStopPacket implements Packet { 9 | @Override 10 | public void onReceive() { 11 | SERVER.stop(false); // onReceive is always executed on server thread 12 | IS_RUNNING = false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.parallel=true 4 | 5 | # Fabric Properties 6 | # check these on https://fabricmc.net/develop 7 | minecraft_version=1.21.10 8 | yarn_mappings=1.21.10+build.2 9 | loader_version=0.17.3 10 | loom_version=1.11-SNAPSHOT 11 | 12 | # Mod Properties 13 | mod_version=2.4.0 14 | maven_group=io.github.kgriff0n 15 | archives_base_name=servers-link 16 | 17 | # Dependencies 18 | fabric_version=0.136.0+1.21.10 19 | permission_api_version=0.5.0 -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/PreventConnectPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.api.ServersLinkApi; 4 | import io.github.kgriff0n.packet.Packet; 5 | 6 | import java.util.UUID; 7 | 8 | public class PreventConnectPacket implements Packet { 9 | 10 | private final UUID uuid; 11 | 12 | public PreventConnectPacket(UUID uuid) { 13 | this.uuid = uuid; 14 | } 15 | 16 | @Override 17 | public void onReceive() { 18 | ServersLinkApi.getPreventConnect().add(uuid); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/hub/groups.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "global": { 4 | "chat": false, 5 | "player_data": false, 6 | "player_list": false, 7 | "roles": false, 8 | "whitelist": false 9 | }, 10 | "survival": { 11 | "player_data": true, 12 | "player_list": true, 13 | "chat": true 14 | }, 15 | "creative": { 16 | "player_data": true, 17 | "player_list": true, 18 | "chat": true 19 | } 20 | }, 21 | "rules": [ 22 | { 23 | "groups": ["survival", "creative"], 24 | "player_list": true, 25 | "chat": true 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/PreventDisconnectPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.api.ServersLinkApi; 5 | import io.github.kgriff0n.packet.Packet; 6 | 7 | import java.util.UUID; 8 | 9 | public class PreventDisconnectPacket implements Packet { 10 | 11 | private final UUID uuid; 12 | 13 | public PreventDisconnectPacket(UUID uuid) { 14 | this.uuid = uuid; 15 | } 16 | 17 | @Override 18 | public void onReceive() { 19 | ServersLinkApi.getPreventDisconnect().add(uuid); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/PlayerManagerAccessor.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import net.minecraft.advancement.PlayerAdvancementTracker; 4 | import net.minecraft.server.PlayerManager; 5 | import net.minecraft.stat.ServerStatHandler; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.gen.Accessor; 8 | 9 | import java.util.Map; 10 | import java.util.UUID; 11 | 12 | @Mixin(PlayerManager.class) 13 | public interface PlayerManagerAccessor { 14 | @Accessor 15 | Map getStatisticsMap(); 16 | @Accessor 17 | Map getAdvancementTrackers(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/util/DummyPlayer.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.util; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import net.fabricmc.fabric.impl.event.interaction.FakePlayerNetworkHandler; 5 | import net.minecraft.network.packet.c2s.common.SyncedClientOptions; 6 | import net.minecraft.server.network.ServerPlayerEntity; 7 | 8 | import static io.github.kgriff0n.ServersLink.SERVER; 9 | 10 | public class DummyPlayer extends ServerPlayerEntity { 11 | public DummyPlayer(GameProfile profile) { 12 | super(SERVER, SERVER.getOverworld(), profile, SyncedClientOptions.createDefault()); 13 | this.networkHandler = new FakePlayerNetworkHandler(this); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/info/ServersInfoPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.info; 2 | 3 | import io.github.kgriff0n.packet.Packet; 4 | import io.github.kgriff0n.server.ServerInfo; 5 | import io.github.kgriff0n.api.ServersLinkApi; 6 | 7 | import java.util.ArrayList; 8 | 9 | /** 10 | * Only send from hub to other sub-servers 11 | * Used to let sub-servers know each other 12 | */ 13 | public class ServersInfoPacket implements Packet { 14 | 15 | private final ArrayList servers; 16 | 17 | public ServersInfoPacket(ArrayList serversInfo) { 18 | this.servers = serversInfo; 19 | } 20 | 21 | @Override 22 | public void onReceive() { 23 | ServersLinkApi.setServerList(servers); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/event/ServerStopped.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.event; 2 | 3 | import io.github.kgriff0n.socket.Gateway; 4 | import io.github.kgriff0n.socket.SubServer; 5 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 6 | import net.minecraft.server.MinecraftServer; 7 | 8 | import static io.github.kgriff0n.ServersLink.*; 9 | 10 | public class ServerStopped implements ServerLifecycleEvents.ServerStopped { 11 | @Override 12 | public void onServerStopped(MinecraftServer server) { 13 | if (!CONFIG_ERROR) { 14 | if (isGateway) { 15 | Gateway.getInstance().interrupt(); 16 | } else { 17 | SubServer.getInstance().interrupt(); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "servers-link", 4 | "version": "${version}", 5 | "name": "Servers Link", 6 | "description": "Link multiple servers together and synchronize players list, chat, inventory...", 7 | "authors": [ 8 | "KGriffon" 9 | ], 10 | "contact": { 11 | "homepage": "https://modrinth.com/mod/servers-link", 12 | "sources": "https://github.com/kgriff0n/servers-link" 13 | }, 14 | "license": "MIT", 15 | "icon": "assets/servers-link/icon.png", 16 | "environment": "*", 17 | "entrypoints": { 18 | "main": [ 19 | "io.github.kgriff0n.ServersLink" 20 | ] 21 | }, 22 | "mixins": [ 23 | "servers-link.mixins.json" 24 | ], 25 | "depends": { 26 | "fabricloader": ">=0.16.9", 27 | "minecraft": ">=1.21.6", 28 | "java": ">=21", 29 | "fabric-api": "*" 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/util/IPlayerServersLink.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.util; 2 | 3 | import net.minecraft.server.world.ServerWorld; 4 | import net.minecraft.util.math.Vec3d; 5 | 6 | import java.util.List; 7 | 8 | public interface IPlayerServersLink { 9 | 10 | void servers_link$setServerPos(String name, Vec3d pos); 11 | Vec3d servers_link$getServerPos(String name); 12 | void servers_link$removeServerPos(String name); 13 | 14 | void servers_link$setServerRot(String name, float yaw, float pitch); 15 | List servers_link$getServerRot(String name); 16 | void servers_link$removeServerRot(String name); 17 | 18 | void servers_link$setServerDim(String name, ServerWorld dim); 19 | ServerWorld servers_link$getServerDim(String name); 20 | void servers_link$removeServerDim(String name); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/info/ServerStatusPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.info; 2 | 3 | import io.github.kgriff0n.packet.Packet; 4 | import io.github.kgriff0n.api.ServersLinkApi; 5 | import io.github.kgriff0n.server.ServerInfo; 6 | 7 | public class ServerStatusPacket implements Packet { 8 | 9 | private final String serverName; 10 | private final float tps; 11 | private final boolean down; 12 | 13 | public ServerStatusPacket(String serverName, float tps, boolean down) { 14 | this.serverName = serverName; 15 | this.tps = tps; 16 | this.down = down; 17 | } 18 | 19 | @Override 20 | public void onReceive() { 21 | ServerInfo serverInfo = ServersLinkApi.getServer(serverName); 22 | if (serverInfo != null) { 23 | serverInfo.setTps(tps); 24 | serverInfo.setDown(down); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/PlayerDataSyncPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import io.github.kgriff0n.socket.SubServer; 6 | import net.minecraft.server.network.ServerPlayerEntity; 7 | 8 | import java.io.IOException; 9 | 10 | import static io.github.kgriff0n.ServersLink.SERVER; 11 | 12 | public class PlayerDataSyncPacket implements Packet { 13 | @Override 14 | public void onReceive() { 15 | for (ServerPlayerEntity player : SERVER.getPlayerManager().getPlayerList()) { 16 | try { 17 | SubServer.getInstance().send(new PlayerDataPacket(player.getUuid())); 18 | } catch (IOException e) { 19 | ServersLink.LOGGER.error("Unable to send player data for {}", player.getName()); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 KGriffon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/UpdateWhitelistPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import net.fabricmc.loader.api.FabricLoader; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | 11 | public class UpdateWhitelistPacket implements Packet { 12 | 13 | private static final Path PATH = FabricLoader.getInstance().getGameDir().resolve("whitelist.json"); 14 | 15 | private final byte[] whitelist; 16 | 17 | public UpdateWhitelistPacket() throws IOException { 18 | this.whitelist = read(); 19 | } 20 | 21 | @Override 22 | public void onReceive() { 23 | try { 24 | write(); 25 | } catch (IOException e) { 26 | ServersLink.LOGGER.error("Unable to write whitelist"); 27 | } 28 | } 29 | 30 | private byte[] read() throws IOException { 31 | return Files.readAllBytes(PATH); 32 | } 33 | 34 | private void write() throws IOException { 35 | Files.write(PATH, whitelist); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/Packet.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.api.ServersLinkApi; 5 | import io.github.kgriff0n.server.Settings; 6 | import io.github.kgriff0n.socket.Gateway; 7 | 8 | import java.io.Serializable; 9 | 10 | public interface Packet extends Serializable { 11 | 12 | /** 13 | * Must be implemented by all the packets. 14 | * Will be executed when the packet is received. 15 | */ 16 | void onReceive(); 17 | 18 | default void onGatewayReceive(String sender) { 19 | Gateway.getInstance().forward(this, sender); 20 | if (shouldReceive(Gateway.getInstance().getSettings(ServersLink.getServerInfo().getGroupId(), ServersLinkApi.getServer(sender).getGroupId()))) { 21 | onReceive(); 22 | } 23 | } 24 | 25 | /** 26 | * Determines whether a server should 27 | * receive the packet, based on settings 28 | * @return true if the packet should be received, 29 | * false otherwise 30 | */ 31 | default boolean shouldReceive(Settings settings) { 32 | return true; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/server/Group.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.server; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | 7 | public class Group { 8 | 9 | private final String name; 10 | private final List serversList; 11 | private final Settings settings; 12 | private final HashMap rules; 13 | 14 | public Group(String name, Settings settings) { 15 | this.name = name; 16 | this.serversList = new ArrayList<>(); 17 | this.settings = settings; 18 | this.rules = new HashMap<>(); 19 | } 20 | 21 | public String getName() { 22 | return this.name; 23 | } 24 | 25 | public void addServer(ServerInfo server) { 26 | this.serversList.add(server); 27 | } 28 | 29 | public List getServersList() { 30 | return serversList; 31 | } 32 | 33 | public void addRule(String groupId, Settings rule) { 34 | rules.put(groupId, rule); 35 | } 36 | 37 | public Settings getSettings() { 38 | return settings; 39 | } 40 | 41 | public HashMap getRules() { 42 | return rules; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/SystemChatPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import com.google.gson.JsonParser; 4 | import com.mojang.serialization.JsonOps; 5 | import io.github.kgriff0n.packet.Packet; 6 | import io.github.kgriff0n.server.Settings; 7 | import net.minecraft.registry.RegistryOps; 8 | import net.minecraft.server.network.ServerPlayerEntity; 9 | import net.minecraft.text.TextCodecs; 10 | 11 | import static io.github.kgriff0n.ServersLink.SERVER; 12 | 13 | public class SystemChatPacket implements Packet { 14 | 15 | private final String serializedMessage; 16 | 17 | public SystemChatPacket(String serializedMessage) { 18 | this.serializedMessage = serializedMessage; 19 | } 20 | 21 | @Override 22 | public boolean shouldReceive(Settings settings) { 23 | return settings.isChatSynced(); 24 | } 25 | 26 | @Override 27 | public void onReceive() { 28 | /* Send message */ 29 | for (ServerPlayerEntity player : SERVER.getPlayerManager().getPlayerList()) { 30 | player.sendMessage(TextCodecs.CODEC.parse(RegistryOps.of(JsonOps.INSTANCE, SERVER.getRegistryManager()), JsonParser.parseString(serializedMessage)).getOrThrow()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | # Use these Java versions 14 | java: [ 15 | 21, # Current Java LTS 16 | ] 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - name: checkout repository 20 | uses: actions/checkout@v4 21 | - name: validate gradle wrapper 22 | uses: gradle/wrapper-validation-action@v2 23 | - name: setup jdk ${{ matrix.java }} 24 | uses: actions/setup-java@v4 25 | with: 26 | java-version: ${{ matrix.java }} 27 | distribution: 'microsoft' 28 | - name: make gradle wrapper executable 29 | run: chmod +x ./gradlew 30 | - name: build 31 | run: ./gradlew build 32 | - name: capture build artifacts 33 | if: ${{ matrix.java == '21' }} # Only upload artifacts built from latest java 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: Artifacts 37 | path: build/libs/ -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/PlayerChatPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import com.google.gson.JsonParser; 4 | import com.mojang.serialization.JsonOps; 5 | import io.github.kgriff0n.packet.Packet; 6 | import io.github.kgriff0n.server.Settings; 7 | import net.minecraft.registry.RegistryOps; 8 | import net.minecraft.server.network.ServerPlayerEntity; 9 | import net.minecraft.text.TextCodecs; 10 | 11 | import static io.github.kgriff0n.ServersLink.SERVER; 12 | 13 | public class PlayerChatPacket implements Packet { 14 | 15 | private final String serializedMessage; 16 | private final String receiver; 17 | 18 | public PlayerChatPacket(String serializedMessage, String receiver) { 19 | this.serializedMessage = serializedMessage; 20 | this.receiver = receiver; 21 | } 22 | 23 | @Override 24 | public boolean shouldReceive(Settings settings) { 25 | return settings.isChatSynced(); 26 | } 27 | 28 | @Override 29 | public void onReceive() { 30 | /* Send message */ 31 | ServerPlayerEntity player = SERVER.getPlayerManager().getPlayer(receiver); 32 | if (player != null) { 33 | player.sendMessage(TextCodecs.CODEC.parse(RegistryOps.of(JsonOps.INSTANCE, SERVER.getRegistryManager()), JsonParser.parseString(serializedMessage)).getOrThrow()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/info/NewServerPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.info; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import io.github.kgriff0n.packet.server.UpdateRolesPacket; 6 | import io.github.kgriff0n.packet.server.UpdateWhitelistPacket; 7 | import io.github.kgriff0n.socket.Gateway; 8 | import io.github.kgriff0n.server.ServerInfo; 9 | import io.github.kgriff0n.api.ServersLinkApi; 10 | 11 | import java.io.IOException; 12 | 13 | public class NewServerPacket implements Packet { 14 | 15 | private final ServerInfo server; 16 | 17 | public NewServerPacket(ServerInfo server) { 18 | this.server = server; 19 | } 20 | 21 | public ServerInfo getServer() { 22 | return this.server; 23 | } 24 | 25 | @Override 26 | public void onReceive() {} 27 | 28 | @Override 29 | public void onGatewayReceive(String sender) { 30 | Gateway gateway = Gateway.getInstance(); 31 | try { 32 | gateway.sendTo(new UpdateWhitelistPacket(), this.server.getName()); 33 | gateway.sendTo(new UpdateRolesPacket(), this.server.getName()); 34 | } catch (IOException e) { 35 | ServersLink.LOGGER.error("Unable to send data to {}", this.server.getName()); 36 | } 37 | Gateway.getInstance().sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/info/NewPlayerPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.info; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonParser; 5 | import com.mojang.authlib.GameProfile; 6 | import com.mojang.authlib.properties.Property; 7 | import com.mojang.authlib.properties.PropertyMap; 8 | import io.github.kgriff0n.packet.Packet; 9 | import io.github.kgriff0n.api.ServersLinkApi; 10 | import io.github.kgriff0n.server.Settings; 11 | 12 | import java.util.Objects; 13 | import java.util.UUID; 14 | 15 | public class NewPlayerPacket implements Packet { 16 | 17 | private final UUID uuid; 18 | private final String name; 19 | 20 | private final String properties; 21 | 22 | public NewPlayerPacket(GameProfile profile) { 23 | this.uuid = profile.id(); 24 | this.name = profile.name(); 25 | this.properties = new Gson().toJson(new PropertyMap.Serializer().serialize(profile.properties(), null, null)); 26 | } 27 | 28 | @Override 29 | public boolean shouldReceive(Settings settings) { 30 | return settings.isPlayerListSynced(); 31 | } 32 | 33 | @Override 34 | public void onReceive() { 35 | PropertyMap properties = new PropertyMap.Serializer().deserialize(JsonParser.parseString(this.properties), null, null); 36 | GameProfile profile = new GameProfile(this.uuid, this.name, properties); 37 | 38 | ServersLinkApi.addDummyPlayer(profile); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/PlayerAcknowledgementPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import com.google.gson.Gson; 4 | import com.mojang.authlib.GameProfile; 5 | import com.mojang.authlib.properties.PropertyMap; 6 | import io.github.kgriff0n.packet.Packet; 7 | import io.github.kgriff0n.packet.info.ServersInfoPacket; 8 | import io.github.kgriff0n.socket.Gateway; 9 | import io.github.kgriff0n.api.ServersLinkApi; 10 | 11 | import java.util.UUID; 12 | 13 | 14 | public class PlayerAcknowledgementPacket implements Packet { 15 | 16 | private final String serverName; 17 | private final UUID uuid; 18 | private final String name; 19 | private final String properties; 20 | 21 | public PlayerAcknowledgementPacket(String serverName, GameProfile profile) { 22 | this.serverName = serverName; 23 | this.uuid = profile.id(); 24 | this.name = profile.name(); 25 | this.properties = new Gson().toJson(new PropertyMap.Serializer().serialize(profile.properties(), null, null)); 26 | } 27 | 28 | @Override 29 | public void onReceive() { 30 | 31 | } 32 | 33 | @Override 34 | public void onGatewayReceive(String sender) { 35 | Packet.super.onGatewayReceive(sender); 36 | ServersLinkApi.getServer(serverName).addPlayer(this.uuid, this.name, this.properties); 37 | Gateway.getInstance().sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/event/ServerStart.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.event; 2 | 3 | import io.github.kgriff0n.PlayersInformation; 4 | import io.github.kgriff0n.socket.SubServer; 5 | import io.github.kgriff0n.ServersLink; 6 | import io.github.kgriff0n.socket.Gateway; 7 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 8 | import net.minecraft.server.MinecraftServer; 9 | 10 | import static io.github.kgriff0n.ServersLink.CONFIG_ERROR; 11 | 12 | public class ServerStart implements ServerLifecycleEvents.ServerStarted { 13 | @Override 14 | public void onServerStarted(MinecraftServer minecraftServer) { 15 | if (CONFIG_ERROR) { 16 | ServersLink.LOGGER.error("You must configure servers-link before starting your server"); 17 | minecraftServer.stop(false); 18 | } else { 19 | /* Initialize SERVER */ 20 | ServersLink.SERVER = minecraftServer; 21 | if (ServersLink.isGateway) { 22 | // Players information 23 | PlayersInformation.loadNbt(minecraftServer); 24 | 25 | Gateway gateway = new Gateway(ServersLink.getGatewayPort()); 26 | gateway.loadConfig(); 27 | gateway.setDaemon(true); 28 | gateway.start(); 29 | } else { 30 | SubServer connection = new SubServer(ServersLink.getGatewayIp(), ServersLink.getGatewayPort()); 31 | connection.setDaemon(true); 32 | connection.start(); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/server/Settings.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.server; 2 | 3 | public class Settings { 4 | 5 | private boolean playerList; 6 | private boolean chat; 7 | private boolean playerData; 8 | private boolean whitelist; 9 | private boolean roles; 10 | 11 | public Settings(boolean playerList, boolean chat, boolean playerData, boolean whitelist, boolean roles) { 12 | this.playerList = playerList; 13 | this.chat = chat; 14 | this.playerData = playerData; 15 | this.whitelist = whitelist; 16 | this.roles = roles; 17 | } 18 | 19 | public boolean isPlayerListSynced() { 20 | return playerList; 21 | } 22 | 23 | public boolean isChatSynced() { 24 | return chat; 25 | } 26 | 27 | public boolean isPlayerDataSynced() { 28 | return playerData; 29 | } 30 | 31 | public boolean isWhitelistSynced() { 32 | return whitelist; 33 | } 34 | 35 | public boolean isRolesSynced() { 36 | return roles; 37 | } 38 | 39 | public void setPlayerListSynced(boolean playerList) { 40 | this.playerList = playerList; 41 | } 42 | 43 | public void setChatSynced(boolean chat) { 44 | this.chat = chat; 45 | } 46 | 47 | public void setPlayerDataSynced(boolean playerData) { 48 | this.playerData = playerData; 49 | } 50 | 51 | public void setWhitelistSynced(boolean whitelist) { 52 | this.whitelist = whitelist; 53 | } 54 | 55 | public void setRolesSynced(boolean roles) { 56 | this.roles = roles; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/PlayerDisconnectPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import io.github.kgriff0n.packet.Packet; 4 | import io.github.kgriff0n.packet.info.ServersInfoPacket; 5 | import io.github.kgriff0n.server.Settings; 6 | import io.github.kgriff0n.socket.Gateway; 7 | import io.github.kgriff0n.api.ServersLinkApi; 8 | import net.minecraft.network.packet.s2c.play.PlayerRemoveS2CPacket; 9 | import net.minecraft.server.network.ServerPlayerEntity; 10 | 11 | import java.util.List; 12 | import java.util.UUID; 13 | 14 | import static io.github.kgriff0n.ServersLink.SERVER; 15 | 16 | public class PlayerDisconnectPacket implements Packet { 17 | 18 | private final UUID uuid; 19 | 20 | public PlayerDisconnectPacket(UUID uuid) { 21 | this.uuid = uuid; 22 | } 23 | 24 | @Override 25 | public boolean shouldReceive(Settings settings) { 26 | return settings.isPlayerListSynced(); 27 | } 28 | 29 | @Override 30 | public void onReceive() { 31 | List playerList = SERVER.getPlayerManager().getPlayerList(); 32 | /* Delete the fake player */ 33 | ServersLinkApi.getDummyPlayers().removeIf(player -> player.getUuid().equals(uuid)); 34 | 35 | /* Update player list for all players */ 36 | for (ServerPlayerEntity player : playerList) { 37 | player.networkHandler.sendPacket(new PlayerRemoveS2CPacket(List.of(uuid))); 38 | } 39 | } 40 | 41 | @Override 42 | public void onGatewayReceive(String sender) { 43 | Packet.super.onGatewayReceive(sender); 44 | Gateway.getInstance().removePlayer(uuid); 45 | Gateway.getInstance().sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/UpdateRolesPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import net.fabricmc.loader.api.FabricLoader; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | 11 | public class UpdateRolesPacket implements Packet { 12 | 13 | private static final Path OP_PATH = FabricLoader.getInstance().getGameDir().resolve("ops.json"); 14 | private static final Path ROLES_PATH = FabricLoader.getInstance().getGameDir().resolve("playerdata").resolve("player_roles"); 15 | 16 | private final byte[] ops; 17 | private final byte[] playerRoles; 18 | 19 | public UpdateRolesPacket() throws IOException { 20 | this.ops = readOps(); 21 | if (FabricLoader.getInstance().isModLoaded("player-roles") && ROLES_PATH.toFile().exists()) { 22 | this.playerRoles = readPlayerRoles(); 23 | } else { 24 | this.playerRoles = null; 25 | } 26 | } 27 | 28 | @Override 29 | public void onReceive() { 30 | try { 31 | writeOps(); 32 | if (FabricLoader.getInstance().isModLoaded("player-roles")) { 33 | writePlayerRoles(); 34 | } 35 | } catch (IOException e) { 36 | ServersLink.LOGGER.error("Unable to write whitelist"); 37 | } 38 | } 39 | 40 | private byte[] readOps() throws IOException { 41 | return Files.readAllBytes(OP_PATH); 42 | } 43 | 44 | private byte[] readPlayerRoles() throws IOException { 45 | return Files.readAllBytes(ROLES_PATH); 46 | } 47 | 48 | private void writeOps() throws IOException { 49 | Files.write(OP_PATH, ops); 50 | } 51 | 52 | private void writePlayerRoles() throws IOException { 53 | Files.write(ROLES_PATH, playerRoles); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/event/ServerStopping.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.event; 2 | 3 | import io.github.kgriff0n.PlayersInformation; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.info.ServerStatusPacket; 6 | import io.github.kgriff0n.packet.server.PlayerDataSyncPacket; 7 | import io.github.kgriff0n.packet.server.ServerStopPacket; 8 | import io.github.kgriff0n.socket.Gateway; 9 | import io.github.kgriff0n.socket.SubServer; 10 | import io.github.kgriff0n.api.ServersLinkApi; 11 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 12 | import net.minecraft.server.MinecraftServer; 13 | 14 | import static io.github.kgriff0n.ServersLink.*; 15 | 16 | public class ServerStopping implements ServerLifecycleEvents.ServerStopping { 17 | 18 | @SuppressWarnings("StatementWithEmptyBody") 19 | @Override 20 | public void onServerStopping(MinecraftServer server) { 21 | if (!CONFIG_ERROR) { 22 | if (isGateway) { 23 | Gateway.getInstance().sendAll(new PlayerDataSyncPacket()); 24 | try { 25 | LOGGER.info("Begin servers synchronization"); 26 | Thread.sleep(2000); 27 | } catch (InterruptedException e) { 28 | LOGGER.error("Unable to synchronize servers"); 29 | } 30 | Gateway.getInstance().sendAll(new ServerStopPacket()); 31 | /* Wait for all servers to shut down */ 32 | while (ServersLinkApi.getRunningSubServers() > 0); 33 | IS_RUNNING = false; 34 | PlayersInformation.saveNbt(server); 35 | } else { 36 | /* Confirm shutdown */ 37 | SubServer subServer = SubServer.getInstance(); 38 | subServer.send(new ServerStatusPacket(ServersLink.getServerInfo().getName(), 0.0f, true)); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/server/PlayerDataPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.server; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.mixin.PlayerManagerAccessor; 5 | import io.github.kgriff0n.packet.Packet; 6 | import io.github.kgriff0n.server.Settings; 7 | import io.github.kgriff0n.util.PlayerData; 8 | import net.minecraft.server.PlayerManager; 9 | 10 | import java.io.IOException; 11 | import java.util.UUID; 12 | 13 | import static io.github.kgriff0n.ServersLink.SERVER; 14 | 15 | public class PlayerDataPacket implements Packet { 16 | 17 | private final UUID uuid; 18 | 19 | private final byte[] data; 20 | private final byte[] advancements; 21 | private final byte[] stats; 22 | 23 | public PlayerDataPacket(UUID uuid) throws IOException { 24 | this.uuid = uuid; 25 | this.data = PlayerData.readData(uuid); 26 | this.advancements = PlayerData.readAdvancements(uuid); 27 | this.stats = PlayerData.readStats(uuid); 28 | } 29 | 30 | private void writeData() { 31 | SERVER.execute(() -> { 32 | try { 33 | PlayerData.writeData(this.uuid, this.data); 34 | PlayerData.writeAdvancements(this.uuid, this.advancements); 35 | PlayerData.writeStats(this.uuid, this.stats); 36 | } catch (IOException e) { 37 | ServersLink.LOGGER.error("Unable to write player data"); 38 | } 39 | }); 40 | } 41 | 42 | @Override 43 | public boolean shouldReceive(Settings settings) { 44 | return settings.isPlayerDataSynced(); 45 | } 46 | 47 | @Override 48 | public void onReceive() { 49 | /* Remove player data to reload them from file */ 50 | PlayerManager playerManager = SERVER.getPlayerManager(); 51 | ((PlayerManagerAccessor) playerManager).getAdvancementTrackers().remove(this.uuid); 52 | ((PlayerManagerAccessor) playerManager).getStatisticsMap().remove(this.uuid); 53 | writeData(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/EntitySelectorMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import io.github.kgriff0n.api.ServersLinkApi; 4 | import net.minecraft.command.EntitySelector; 5 | import net.minecraft.server.PlayerManager; 6 | import net.minecraft.server.network.ServerPlayerEntity; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Redirect; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.UUID; 14 | 15 | @Mixin(EntitySelector.class) 16 | public class EntitySelectorMixin { 17 | 18 | @Redirect(method = "getPlayers", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;getPlayer(Ljava/util/UUID;)Lnet/minecraft/server/network/ServerPlayerEntity;")) 19 | private ServerPlayerEntity getDummyPlayer(PlayerManager playerManager, UUID uuid) { 20 | ServerPlayerEntity player = playerManager.getPlayer(uuid); 21 | if (player == null) { // check for dummy player 22 | player = ServersLinkApi.getDummyPlayer(uuid); 23 | } 24 | return player; 25 | } 26 | 27 | @Redirect(method = "getPlayers", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;getPlayer(Ljava/lang/String;)Lnet/minecraft/server/network/ServerPlayerEntity;")) 28 | private ServerPlayerEntity getDummyPlayer(PlayerManager playerManager, String name) { 29 | ServerPlayerEntity player = playerManager.getPlayer(name); 30 | if (player == null) { // check for dummy player 31 | player = ServersLinkApi.getDummyPlayer(name); 32 | } 33 | return player; 34 | } 35 | 36 | @Redirect(method = "getPlayers", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;getPlayerList()Ljava/util/List;")) 37 | private List getPlayerList(PlayerManager playerManager) { 38 | List allPlayers = new ArrayList<>(); 39 | allPlayers.addAll(playerManager.getPlayerList()); 40 | allPlayers.addAll(ServersLinkApi.getDummyPlayers()); 41 | return allPlayers; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/TeleportationAcceptPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import io.github.kgriff0n.socket.Gateway; 6 | import io.github.kgriff0n.util.IPlayerServersLink; 7 | import io.github.kgriff0n.api.ServersLinkApi; 8 | import net.minecraft.server.network.ServerPlayerEntity; 9 | import net.minecraft.util.math.Vec3d; 10 | 11 | import java.util.UUID; 12 | 13 | public class TeleportationAcceptPacket implements Packet { 14 | 15 | private final double targetX; 16 | private final double targetY; 17 | private final double targetZ; 18 | private final UUID senderUuid; 19 | 20 | private final String originServer; 21 | private final String destinationServer; 22 | 23 | public TeleportationAcceptPacket(double targetX, double targetY, double targetZ, UUID senderUuid, String originServer, String destinationServer) { 24 | this.targetX = targetX; 25 | this.targetY = targetY; 26 | this.targetZ = targetZ; 27 | this.senderUuid = senderUuid; 28 | 29 | this.originServer = originServer; 30 | this.destinationServer = destinationServer; 31 | } 32 | 33 | @Override 34 | public void onReceive() { 35 | ServerPlayerEntity player = ServersLink.SERVER.getPlayerManager().getPlayer(senderUuid); 36 | if (this.originServer.equals(ServersLink.getServerInfo().getName()) && player != null) { 37 | /* We are in the correct server */ 38 | ((IPlayerServersLink) player).servers_link$setServerPos(this.destinationServer, new Vec3d(targetX, targetY, targetZ)); 39 | ServersLinkApi.transferPlayer(player, this.originServer, this.destinationServer); 40 | } 41 | } 42 | 43 | @Override 44 | public void onGatewayReceive(String sender) { 45 | Packet.super.onGatewayReceive(sender); 46 | if (!this.originServer.equals(ServersLink.getServerInfo().getName())) { 47 | /* Redirect the packet to the other server */ 48 | Gateway.getInstance().sendTo(this, this.destinationServer); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/PlayersInformation.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n; 2 | 3 | import net.minecraft.nbt.*; 4 | import net.minecraft.server.MinecraftServer; 5 | import net.minecraft.util.WorldSavePath; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.HashMap; 13 | import java.util.UUID; 14 | 15 | public class PlayersInformation { 16 | 17 | private static final HashMap lastServer = new HashMap<>(); 18 | 19 | public static void setLastServer(UUID player, String serverName) { 20 | lastServer.put(player, serverName); 21 | } 22 | 23 | public static String getLastServer(UUID player) { 24 | return lastServer.get(player); 25 | } 26 | 27 | public static void saveNbt(MinecraftServer server) { 28 | NbtCompound nbt = new NbtCompound(); 29 | lastServer.forEach((uuid, name) -> nbt.putString(uuid.toString(), name)); 30 | 31 | Path dataFile = server 32 | .getSavePath(WorldSavePath.ROOT) 33 | .resolve("data") 34 | .resolve("servers_link.nbt"); 35 | 36 | try (OutputStream os = Files.newOutputStream(dataFile)) { 37 | NbtIo.writeCompressed(nbt, os); 38 | } catch (IOException e) { 39 | ServersLink.LOGGER.error("Unable to save data"); 40 | } 41 | } 42 | 43 | public static void loadNbt(MinecraftServer server) { 44 | Path dataFile = server 45 | .getSavePath(WorldSavePath.ROOT) 46 | .resolve("data") 47 | .resolve("servers_link.nbt"); 48 | 49 | try (InputStream is = Files.newInputStream(dataFile)) { 50 | NbtCompound nbt = NbtIo.readCompressed(is, NbtSizeTracker.ofUnlimitedBytes()); 51 | for (String uuid : nbt.getKeys()) { 52 | UUID player = UUID.fromString(uuid); 53 | nbt.getString(uuid).ifPresent(string -> lastServer.put(player, string)); 54 | } 55 | } catch (IOException e) { 56 | ServersLink.LOGGER.error("Unable to load data"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/TeleportationRequestPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import io.github.kgriff0n.socket.Gateway; 6 | import io.github.kgriff0n.socket.SubServer; 7 | import net.minecraft.server.network.ServerPlayerEntity; 8 | import net.minecraft.util.math.Vec3d; 9 | 10 | import java.util.UUID; 11 | 12 | public class TeleportationRequestPacket implements Packet { 13 | 14 | private final UUID targetUuid; 15 | private final UUID senderUuid; 16 | 17 | private final String originServer; 18 | private final String destinationServer; 19 | 20 | public TeleportationRequestPacket(UUID targetUuid, UUID senderUuid, String originServer, String destinationServer) { 21 | this.targetUuid = targetUuid; 22 | this.senderUuid = senderUuid; 23 | 24 | this.originServer = originServer; 25 | this.destinationServer = destinationServer; 26 | } 27 | 28 | @Override 29 | public void onReceive() { 30 | ServerPlayerEntity player = ServersLink.SERVER.getPlayerManager().getPlayer(targetUuid); 31 | Vec3d pos = player != null ? player.getEntityPos() : null; 32 | 33 | if (ServersLink.isGateway) { //FIXME 34 | if (this.destinationServer.equals(ServersLink.getServerInfo().getName())) { 35 | /* Execute packet from hub */ 36 | if (pos != null) { 37 | Gateway.getInstance().sendTo(new TeleportationAcceptPacket(pos.getX(), pos.getY(), pos.getZ(), this.senderUuid, this.originServer, this.destinationServer), this.originServer); 38 | } 39 | } else { 40 | /* Redirect the packet to the other server */ 41 | Gateway.getInstance().sendTo(this, this.destinationServer); 42 | } 43 | } else { 44 | /* Sub-server receive the packet */ 45 | if (pos != null) { 46 | SubServer.getInstance().send(new TeleportationAcceptPacket(pos.getX(), pos.getY(), pos.getZ(), this.senderUuid, this.originServer, this.destinationServer)); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/util/PlayerData.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.util; 2 | 3 | import net.fabricmc.loader.api.FabricLoader; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.util.UUID; 10 | 11 | import static io.github.kgriff0n.ServersLink.SERVER; 12 | 13 | public class PlayerData { 14 | 15 | private static final Path PATH = FabricLoader.getInstance().getGameDir().resolve(SERVER.getSaveProperties().getLevelName()); 16 | 17 | public static Path getDataPath(UUID uuid) { 18 | Path playerPath = PATH.resolve("playerdata").resolve(uuid + ".dat"); 19 | return playerPath; 20 | } 21 | 22 | public static Path getAdvancementsPath(UUID uuid) { 23 | File file = PATH.toFile(); 24 | file.mkdir(); 25 | return PATH.resolve("advancements").resolve(uuid + ".json"); 26 | } 27 | 28 | public static Path getStatsPath(UUID uuid) { 29 | return PATH.resolve("stats").resolve(uuid + ".json"); 30 | } 31 | 32 | public static byte[] readData(UUID uuid) throws IOException { 33 | return Files.readAllBytes(getDataPath(uuid)); 34 | } 35 | 36 | public static byte[] readAdvancements(UUID uuid) throws IOException { 37 | return Files.readAllBytes(getAdvancementsPath(uuid)); 38 | } 39 | 40 | public static byte[] readStats(UUID uuid) throws IOException { 41 | return Files.readAllBytes(getStatsPath(uuid)); 42 | } 43 | 44 | public static void writeData(UUID uuid, byte[] data) throws IOException { 45 | Files.write(getDataPath(uuid), data); 46 | } 47 | 48 | public static void writeAdvancements(UUID uuid, byte[] data) throws IOException { 49 | /* If the server is started for the first time, folder doesn't exist */ 50 | File dir = PATH.resolve("advancements").toFile(); 51 | if (!dir.exists()) { 52 | dir.mkdir(); 53 | } 54 | Files.write(getAdvancementsPath(uuid), data); 55 | } 56 | 57 | public static void writeStats(UUID uuid, byte[] data) throws IOException { 58 | /* If the server is started for the first time, folder doesn't exist */ 59 | File dir = PATH.resolve("stats").toFile(); 60 | if (!dir.exists()) { 61 | dir.mkdir(); 62 | } 63 | Files.write(getStatsPath(uuid), data); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/CommandManagerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import com.mojang.brigadier.ParseResults; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.play.CommandPacket; 6 | import io.github.kgriff0n.api.ServersLinkApi; 7 | import net.minecraft.server.command.CommandManager; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import net.minecraft.server.network.ServerPlayerEntity; 10 | import net.minecraft.text.Text; 11 | import net.minecraft.util.Formatting; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | 17 | import java.util.UUID; 18 | 19 | @Mixin(CommandManager.class) 20 | public class CommandManagerMixin { 21 | 22 | @Inject(at = @At("TAIL"), method = "execute") 23 | private void executeCommand(ParseResults parseResults, String command, CallbackInfo ci) { 24 | if (!parseResults.getContext().getSource().getName().equals("do-not-send-back")) { 25 | ServerPlayerEntity player = parseResults.getContext().getSource().getPlayer(); 26 | UUID uuid = null; 27 | if (player != null) uuid = player.getUuid(); 28 | if (command.startsWith("server run ")) { 29 | if (player != null) { 30 | if (command.contains("@r")) { 31 | player.sendMessage(Text.literal("Warning, using @r can cause desync between servers").formatted(Formatting.RED, Formatting.BOLD)); 32 | } 33 | if (command.contains("execute")) { 34 | player.sendMessage(Text.literal("Be careful when using execute, especially with the positions").formatted(Formatting.RED, Formatting.BOLD)); 35 | } 36 | if (command.contains("teleport") || command.contains("tp") || command.contains("whitelist") || command.contains("op")) { 37 | player.sendMessage(Text.literal("You should use native /server commands").formatted(Formatting.RED, Formatting.BOLD)); 38 | } 39 | } 40 | } 41 | ServersLinkApi.send(new CommandPacket(uuid, command), ServersLink.getServerInfo().getName()); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/ServerPlayNetworkHandlerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import com.mojang.serialization.JsonOps; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.play.PlayerChatPacket; 6 | import io.github.kgriff0n.api.ServersLinkApi; 7 | import net.minecraft.network.message.MessageType; 8 | import net.minecraft.network.message.SignedMessage; 9 | import net.minecraft.registry.RegistryOps; 10 | import net.minecraft.server.PlayerManager; 11 | import net.minecraft.server.network.ServerPlayNetworkHandler; 12 | import net.minecraft.server.network.ServerPlayerEntity; 13 | import net.minecraft.text.Text; 14 | import net.minecraft.text.TextCodecs; 15 | import org.spongepowered.asm.mixin.Mixin; 16 | import org.spongepowered.asm.mixin.Shadow; 17 | import org.spongepowered.asm.mixin.injection.At; 18 | import org.spongepowered.asm.mixin.injection.Inject; 19 | import org.spongepowered.asm.mixin.injection.Redirect; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 21 | 22 | import static io.github.kgriff0n.ServersLink.SERVER; 23 | 24 | @Mixin(ServerPlayNetworkHandler.class) 25 | public abstract class ServerPlayNetworkHandlerMixin { 26 | 27 | @Shadow public abstract ServerPlayerEntity getPlayer(); 28 | 29 | @Shadow public ServerPlayerEntity player; 30 | 31 | @Inject(at = @At("HEAD"), method = "sendChatMessage") 32 | private void sendChatMessage(SignedMessage message, MessageType.Parameters params, CallbackInfo ci) { 33 | Text formattedMessage = params.applyChatDecoration(message.getContent()); 34 | PlayerChatPacket packet = new PlayerChatPacket(TextCodecs.CODEC.encodeStart(RegistryOps.of(JsonOps.INSTANCE, SERVER.getRegistryManager()), formattedMessage).getOrThrow().toString(), this.getPlayer().getName().getString()); 35 | ServersLinkApi.send(packet, ServersLink.getServerInfo().getName()); 36 | } 37 | 38 | @Redirect(method = "cleanUp", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V")) 39 | private void preventDisconnectMessage(PlayerManager instance, Text message, boolean overlay) { 40 | if (ServersLinkApi.getPreventDisconnect().contains(player.getUuid())) { 41 | ServersLinkApi.getPreventDisconnect().remove(player.getUuid()); 42 | } else { 43 | getPlayer().getEntityWorld().getServer().getPlayerManager().broadcast(message, overlay); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/event/PlayerDisconnect.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.event; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.play.PlayerDisconnectPacket; 5 | import io.github.kgriff0n.packet.info.ServersInfoPacket; 6 | import io.github.kgriff0n.socket.Gateway; 7 | import io.github.kgriff0n.socket.SubServer; 8 | import io.github.kgriff0n.util.IPlayerServersLink; 9 | import io.github.kgriff0n.api.ServersLinkApi; 10 | import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 11 | import net.minecraft.server.MinecraftServer; 12 | import net.minecraft.server.network.ServerPlayNetworkHandler; 13 | import net.minecraft.server.network.ServerPlayerEntity; 14 | 15 | import java.util.UUID; 16 | 17 | public class PlayerDisconnect implements ServerPlayConnectionEvents.Disconnect { 18 | @Override 19 | public void onPlayDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { 20 | ServerPlayerEntity player = serverPlayNetworkHandler.player; 21 | UUID uuid = player.getUuid(); 22 | PlayerDisconnectPacket packet = new PlayerDisconnectPacket(uuid); 23 | 24 | /* Set player pos, dim & last server */ 25 | ((IPlayerServersLink) player).servers_link$setServerPos(ServersLink.getServerInfo().getName(), player.getEntityPos()); 26 | ((IPlayerServersLink) player).servers_link$setServerDim(ServersLink.getServerInfo().getName(), player.getEntityWorld()); 27 | ((IPlayerServersLink) player).servers_link$setServerRot(ServersLink.getServerInfo().getName(), player.getYaw(), player.getPitch()); 28 | 29 | // Remove player from list 30 | ServersLinkApi.getServer(ServersLink.getServerInfo().getName()).removePlayer(uuid); 31 | 32 | if (ServersLink.isGateway) { 33 | Gateway gateway = Gateway.getInstance(); 34 | /* Delete player from list and send packet ONLY if the player is not transferred */ 35 | if (!ServersLinkApi.getPreventDisconnect().contains(uuid)) { 36 | gateway.sendAll(packet); 37 | gateway.sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 38 | } 39 | } else { 40 | SubServer connection = SubServer.getInstance(); 41 | /* Send packet ONLY if the player is not transferred */ 42 | if (!ServersLinkApi.getPreventDisconnect().contains(uuid)) { 43 | connection.send(packet); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/MinecraftServerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.api.ServersLinkApi; 6 | import io.github.kgriff0n.server.ServerInfo; 7 | import io.github.kgriff0n.socket.Gateway; 8 | import net.minecraft.network.QueryableServer; 9 | import net.minecraft.server.MinecraftServer; 10 | import net.minecraft.server.PlayerConfigEntry; 11 | import net.minecraft.server.ServerMetadata; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.Shadow; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | @Mixin(MinecraftServer.class) 22 | public abstract class MinecraftServerMixin implements QueryableServer { 23 | 24 | @Inject(at = @At("HEAD"), method = "createMetadataPlayers", cancellable = true) 25 | private void customPlayerCount(CallbackInfoReturnable cir) { 26 | if (ServersLink.isGateway && Gateway.getInstance().isGlobalPlayerCountEnabled()) { 27 | int maxPlayers = getMaxPlayerCount(); 28 | int playerCount = 0; 29 | List players = new ArrayList<>(); 30 | List playerConfigEntries = new ArrayList<>(); 31 | for (ServerInfo server : ServersLinkApi.getServerList()) { 32 | playerCount += server.getPlayersList().size(); 33 | for (GameProfile player : players) { 34 | playerConfigEntries.add(new PlayerConfigEntry(player.id(), player.name())); 35 | } 36 | 37 | } 38 | cir.setReturnValue(new ServerMetadata.Players(maxPlayers, playerCount, playerConfigEntries)); 39 | } 40 | } 41 | 42 | @Inject(at = @At("HEAD"), method = "getCurrentPlayerCount", cancellable = true) 43 | private void getCurrentPlayerCount(CallbackInfoReturnable cir) { 44 | if (ServersLink.isGateway && Gateway.getInstance().isGlobalPlayerCountEnabled()) { 45 | int playerCount = 0; 46 | for (ServerInfo server : ServersLinkApi.getServerList()) { 47 | playerCount += server.getPlayersList().size(); 48 | } 49 | cir.setReturnValue(playerCount); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/event/ServerTick.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.event; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.info.ServerStatusPacket; 5 | import io.github.kgriff0n.packet.info.ServersInfoPacket; 6 | import io.github.kgriff0n.socket.Gateway; 7 | import io.github.kgriff0n.socket.SubServer; 8 | import io.github.kgriff0n.api.ServersLinkApi; 9 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; 10 | import net.minecraft.server.MinecraftServer; 11 | import net.minecraft.server.network.ServerPlayerEntity; 12 | import net.minecraft.text.Text; 13 | 14 | import java.util.Iterator; 15 | import java.util.Map; 16 | import java.util.UUID; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | 19 | public class ServerTick implements ServerTickEvents.StartTick { 20 | 21 | private static final ConcurrentHashMap shouldDisconnect = new ConcurrentHashMap<>(); 22 | 23 | public static void scheduleDisconnect(UUID player, int ticks) { 24 | shouldDisconnect.put(player, ticks); 25 | } 26 | 27 | private int count = 0; 28 | 29 | @Override 30 | public void onStartTick(MinecraftServer server) { 31 | count++; 32 | if (count >= 600) { // every 30s 33 | count = 0; 34 | float tps = server.getTickManager().getTickRate(); 35 | /* update self */ 36 | ServersLink.getServerInfo().setTps(tps); 37 | if (ServersLink.isGateway) { 38 | Gateway.getInstance().sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 39 | } else { 40 | SubServer.getInstance().send(new ServerStatusPacket(ServersLink.getServerInfo().getName(), tps, false)); 41 | } 42 | } 43 | 44 | // Disconnect players 45 | Iterator> players = shouldDisconnect.entrySet().iterator(); 46 | while (players.hasNext()) { 47 | Map.Entry playerEntry = players.next(); 48 | int ticksLeft = playerEntry.getValue() - 1; 49 | if (ticksLeft <= 0) { 50 | ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerEntry.getKey()); 51 | if (player != null && !player.isDisconnected()) { 52 | player.networkHandler.disconnect(Text.translatable("connect.transferring")); 53 | } 54 | players.remove(); 55 | } else { 56 | playerEntry.setValue(ticksLeft); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/PlayerTransferPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import io.github.kgriff0n.PlayersInformation; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.Packet; 6 | import io.github.kgriff0n.packet.server.PreventConnectPacket; 7 | import io.github.kgriff0n.packet.server.PreventDisconnectPacket; 8 | import io.github.kgriff0n.server.Settings; 9 | import io.github.kgriff0n.socket.Gateway; 10 | import io.github.kgriff0n.socket.SubServer; 11 | import io.github.kgriff0n.api.ServersLinkApi; 12 | 13 | import java.util.UUID; 14 | 15 | public class PlayerTransferPacket implements Packet { 16 | 17 | private final UUID uuid; 18 | 19 | private final String serverToTransfer; 20 | 21 | public PlayerTransferPacket(UUID uuid, String serverToTransfer) { 22 | this.uuid = uuid; 23 | this.serverToTransfer = serverToTransfer; 24 | } 25 | 26 | @Override 27 | public boolean shouldReceive(Settings settings) { 28 | return false; 29 | } 30 | 31 | @Override 32 | public void onReceive() { 33 | if (!ServersLink.isGateway) { 34 | /* Sub-server receive the packet, add player to the waiting list to allow to connection */ 35 | SubServer.getInstance().addWaitingPlayer(this.uuid); 36 | } 37 | } 38 | 39 | @Override 40 | public void onGatewayReceive(String sender) { 41 | /* The player is sent to the hub, remove from the player list to allowed it to connect */ 42 | /* Add player to transferred list, to block the join message */ 43 | Gateway gateway = Gateway.getInstance(); 44 | Settings settings = gateway.getSettings(ServersLinkApi.getServer(sender).getGroupId(), ServersLinkApi.getServer(serverToTransfer).getGroupId()); 45 | if (this.serverToTransfer.equals(ServersLink.getServerInfo().getName())) { 46 | if (settings.isPlayerListSynced()) { 47 | ServersLinkApi.getPreventConnect().add(uuid); 48 | gateway.sendTo(new PreventDisconnectPacket(uuid), sender); 49 | } 50 | } else { /* Redirect the packet to the other server */ 51 | gateway.sendTo(this, this.serverToTransfer); 52 | if (settings.isPlayerListSynced()) { // prevents messages if both servers have synchronized players 53 | gateway.sendTo(new PreventConnectPacket(uuid), serverToTransfer); 54 | gateway.sendTo(new PreventDisconnectPacket(uuid), sender); 55 | } 56 | } 57 | gateway.removePlayer(this.uuid); 58 | /* Save last server */ 59 | PlayersInformation.setLastServer(uuid, serverToTransfer); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/ServersLink.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import io.github.kgriff0n.command.ServerCommand; 6 | import io.github.kgriff0n.event.*; 7 | import io.github.kgriff0n.server.ServerInfo; 8 | import net.fabricmc.api.ModInitializer; 9 | 10 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; 11 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 12 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; 13 | import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 14 | import net.fabricmc.loader.api.FabricLoader; 15 | import net.minecraft.server.MinecraftServer; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.io.IOException; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | 23 | public class ServersLink implements ModInitializer { 24 | public static final String MOD_ID = "servers-link"; 25 | public static final Path CONFIG = FabricLoader.getInstance().getConfigDir().resolve("servers-link"); 26 | public static boolean isGateway; 27 | 28 | private static ServerInfo serverInfo; 29 | private static String gatewayIp; 30 | private static int gatewayPort; 31 | 32 | public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); 33 | 34 | public static MinecraftServer SERVER; 35 | public static boolean IS_RUNNING = true; 36 | public static boolean CONFIG_ERROR = false; 37 | 38 | @Override 39 | public void onInitialize() { 40 | 41 | loadServerInfo(); 42 | 43 | ServerCommand.register(); 44 | 45 | ServerLifecycleEvents.SERVER_STARTED.register(new ServerStart()); 46 | ServerLifecycleEvents.SERVER_STOPPING.register(new ServerStopping()); 47 | ServerLifecycleEvents.SERVER_STOPPED.register(new ServerStopped()); 48 | ServerPlayConnectionEvents.JOIN.register(new PlayerJoin()); 49 | ServerPlayConnectionEvents.DISCONNECT.register(new PlayerDisconnect()); 50 | ServerTickEvents.START_SERVER_TICK.register(new ServerTick()); 51 | ServerEntityEvents.ENTITY_LOAD.register(new PlayerJoin()); 52 | } 53 | 54 | public static ServerInfo getServerInfo() { 55 | return serverInfo; 56 | } 57 | 58 | public static String getGatewayIp() { 59 | return gatewayIp; 60 | } 61 | 62 | public static int getGatewayPort() { 63 | return gatewayPort; 64 | } 65 | 66 | private void loadServerInfo() { 67 | Path path = CONFIG.resolve("info.json"); 68 | try { 69 | String jsonContent = Files.readString(path); 70 | Gson gson = new Gson(); 71 | JsonObject jsonObject = gson.fromJson(jsonContent, JsonObject.class); 72 | isGateway = jsonObject.get("gateway").getAsBoolean(); 73 | gatewayIp = jsonObject.get("gateway-ip").getAsString(); 74 | gatewayPort = jsonObject.get("gateway-port").getAsInt(); 75 | serverInfo = new ServerInfo( 76 | jsonObject.get("group").getAsString(), 77 | jsonObject.get("server-name").getAsString(), 78 | jsonObject.get("server-ip").getAsString(), 79 | jsonObject.get("server-port").getAsInt() 80 | ); 81 | } catch (IOException e) { 82 | CONFIG_ERROR = true; 83 | ServersLink.LOGGER.error("Unable to read info.json"); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/packet/play/CommandPacket.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.packet.play; 2 | 3 | import io.github.kgriff0n.packet.Packet; 4 | import io.github.kgriff0n.api.ServersLinkApi; 5 | import io.github.kgriff0n.server.Settings; 6 | import net.fabricmc.loader.api.FabricLoader; 7 | import net.minecraft.server.PlayerConfigEntry; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import net.minecraft.server.network.ServerPlayerEntity; 10 | import net.minecraft.server.world.ServerWorld; 11 | import net.minecraft.text.Text; 12 | import net.minecraft.util.math.Vec2f; 13 | import net.minecraft.util.math.Vec3d; 14 | 15 | import java.util.UUID; 16 | 17 | import static io.github.kgriff0n.ServersLink.SERVER; 18 | 19 | public class CommandPacket implements Packet { 20 | 21 | private final UUID uuid; 22 | private final String command; 23 | 24 | public CommandPacket(UUID uuid, String command) { 25 | this.uuid = uuid; 26 | this.command = command; 27 | } 28 | 29 | @Override 30 | public boolean shouldReceive(Settings settings) { 31 | return command.startsWith("server run ") 32 | || settings.isWhitelistSynced() && command.startsWith("whitelist") 33 | || settings.isRolesSynced() && 34 | (command.startsWith("op") || command.startsWith("deop") 35 | || (FabricLoader.getInstance().isModLoaded("player-roles") 36 | && command.startsWith("role"))); 37 | } 38 | 39 | @Override 40 | public void onReceive() { 41 | String cmd; 42 | if (command.startsWith("server run ")) { 43 | cmd = command.substring(11); 44 | } else { 45 | cmd = command; 46 | } 47 | ServerCommandSource source; 48 | 49 | ServerPlayerEntity player = null; 50 | if (uuid != null) { 51 | player = ServersLinkApi.getDummyPlayer(uuid); 52 | } 53 | 54 | if (player != null) { 55 | source = new ServerCommandSource( 56 | player.getCommandOutput(), 57 | player.getEntityPos(), 58 | player.getRotationClient(), 59 | player.getEntityWorld() instanceof ServerWorld ? (ServerWorld)player.getEntityWorld() : null, 60 | SERVER.getPermissionLevel(new PlayerConfigEntry(player.getUuid(), player.getName().getString())), 61 | "do-not-send-back", 62 | player.getDisplayName(), 63 | player.getEntityWorld().getServer(), 64 | player 65 | ); 66 | } else { 67 | source = new ServerCommandSource( 68 | SERVER, 69 | SERVER.getOverworld() == null ? Vec3d.ZERO : Vec3d.of(SERVER.getOverworld().getSpawnPoint().getPos()), 70 | Vec2f.ZERO, 71 | SERVER.getOverworld(), 72 | 4, 73 | "do-not-send-back", 74 | Text.literal("Server"), 75 | SERVER, 76 | null 77 | ); 78 | } 79 | SERVER.getCommandManager().parseAndExecute(source, cmd); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/socket/SubServer.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.socket; 2 | 3 | import io.github.kgriff0n.ServersLink; 4 | import io.github.kgriff0n.packet.Packet; 5 | import io.github.kgriff0n.packet.info.NewServerPacket; 6 | import io.github.kgriff0n.packet.info.ServerStatusPacket; 7 | 8 | import java.io.*; 9 | import java.net.Socket; 10 | import java.util.ArrayList; 11 | import java.util.UUID; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | 15 | import static io.github.kgriff0n.ServersLink.IS_RUNNING; 16 | import static io.github.kgriff0n.ServersLink.SERVER; 17 | 18 | public class SubServer extends Thread { 19 | 20 | private static SubServer connection; 21 | 22 | private ExecutorService executor; 23 | 24 | /** List of player UUIDs that can connect */ 25 | private ArrayList waitingPlayers; 26 | 27 | public static SubServer getInstance() { 28 | return connection; 29 | } 30 | 31 | private Socket clientSocket; 32 | private ObjectInputStream in; 33 | private ObjectOutputStream out; 34 | 35 | public SubServer(String ip, int port) { 36 | if (connection == null) { 37 | waitingPlayers = new ArrayList<>(); 38 | 39 | try { 40 | clientSocket = new Socket(ip, port); 41 | 42 | out = new ObjectOutputStream(clientSocket.getOutputStream()); 43 | out.flush(); 44 | 45 | in = new ObjectInputStream(clientSocket.getInputStream()); 46 | } catch (IOException e) { 47 | ServersLink.LOGGER.error("Unable to establish connection"); 48 | } 49 | connection = this; 50 | executor = Executors.newSingleThreadExecutor(); 51 | } else { 52 | ServersLink.LOGGER.error("Connection already established"); 53 | } 54 | } 55 | 56 | public synchronized void send(Packet packet) { 57 | if (executor.isShutdown()) { 58 | ServersLink.LOGGER.warn("Can't send {}", packet.getClass().getName()); 59 | } else { 60 | executor.submit(() -> { 61 | try { 62 | out.writeObject(packet); 63 | out.flush(); 64 | out.reset(); 65 | } catch (IOException e) { 66 | ServersLink.LOGGER.error("Unable to send {}", packet.getClass().getName()); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | public ArrayList getWaitingPlayers() { 73 | return this.waitingPlayers; 74 | } 75 | 76 | public void addWaitingPlayer(UUID uuid) { 77 | this.waitingPlayers.add(uuid); 78 | } 79 | 80 | public void removeWaitingPlayer(UUID uuid) { 81 | this.waitingPlayers.remove(uuid); 82 | } 83 | 84 | @Override 85 | public void run() { 86 | try { 87 | send(new NewServerPacket(ServersLink.getServerInfo())); 88 | send(new ServerStatusPacket(ServersLink.getServerInfo().getName(), 20.0f, false)); 89 | while (IS_RUNNING) { 90 | try { 91 | Packet pkt = ((Packet)in.readObject()); 92 | SERVER.execute(pkt::onReceive); 93 | } catch (ClassNotFoundException e) { 94 | ServersLink.LOGGER.error("Receive invalid data"); 95 | } 96 | } 97 | } catch (IOException e) { 98 | ServersLink.LOGGER.error("Gateway disconnected"); 99 | SERVER.stop(true); 100 | } 101 | } 102 | 103 | @Override 104 | public void interrupt() { 105 | super.interrupt(); 106 | executor.shutdown(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/server/ServerInfo.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.server; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonParser; 5 | import com.mojang.authlib.GameProfile; 6 | import com.mojang.authlib.properties.PropertyMap; 7 | 8 | import java.io.Serializable; 9 | import java.util.*; 10 | 11 | public class ServerInfo implements Serializable { 12 | 13 | private final String groupId; 14 | private final String name; 15 | private final String ip; 16 | private final int port; 17 | 18 | private final int randomValue; 19 | 20 | private float tps; 21 | private boolean down; 22 | 23 | private final HashMap playersList; 24 | private final HashMap playersPropertiesList; 25 | 26 | public ServerInfo(String groupId, String name, String ip, int port) { 27 | this.groupId = groupId; 28 | this.name = name; 29 | this.ip = ip; 30 | this.port = port; 31 | 32 | this.tps = 20.0f; 33 | this.down = false; 34 | 35 | this.playersList = new HashMap<>(); 36 | this.playersPropertiesList = new HashMap<>(); 37 | 38 | this.randomValue = new Random().nextInt(); 39 | } 40 | 41 | public String getGroupId() { 42 | return groupId; 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public String getIp() { 50 | return ip; 51 | } 52 | 53 | public int getPort() { 54 | return port; 55 | } 56 | 57 | public HashMap getPlayersList() { 58 | return playersList; 59 | } 60 | 61 | public List getGameProfile() { 62 | List list = new ArrayList<>(); 63 | for (Map.Entry entry : playersList.entrySet()) { 64 | PropertyMap properties = new PropertyMap.Serializer().deserialize(JsonParser.parseString(playersPropertiesList.get(entry.getKey())), null, null); 65 | GameProfile profile = new GameProfile(entry.getKey(), entry.getValue()); 66 | /* Initialize game profile */ 67 | PropertyMap gameProfileProperties = profile.properties(); 68 | properties.forEach(gameProfileProperties::put); 69 | list.add(profile); 70 | } 71 | return list; 72 | } 73 | 74 | public void addPlayer(GameProfile profile) { 75 | this.playersList.put(profile.id(), profile.name()); 76 | this.playersPropertiesList.put(profile.id(), new Gson().toJson(new PropertyMap.Serializer().serialize(profile.properties(), null, null))); 77 | } 78 | 79 | public void addPlayer(UUID uuid, String name, String properties) { 80 | this.playersList.put(uuid, name); 81 | this.playersPropertiesList.put(uuid, properties); 82 | } 83 | 84 | public void removePlayer(UUID uuid) { 85 | this.playersList.remove(uuid); 86 | this.playersPropertiesList.remove(uuid); 87 | } 88 | 89 | public boolean isDown() { 90 | return down; 91 | } 92 | 93 | public void setDown(boolean down) { 94 | this.down = down; 95 | } 96 | 97 | public float getTps() { 98 | return tps; 99 | } 100 | 101 | public void setTps(float tps) { 102 | this.tps = tps; 103 | } 104 | 105 | @Override 106 | public boolean equals(Object obj) { 107 | if (obj instanceof ServerInfo serverInfo) { 108 | return serverInfo.name.equals(this.name); 109 | } 110 | return false; 111 | } 112 | 113 | @Override 114 | public String toString() { 115 | return String.format("%s[group=%s,player_list=%s,rng=%s]", getName(), getGroupId(), new ArrayList<>(playersList.keySet()), randomValue); 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | return this.name.hashCode(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/PlayerManagerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import com.mojang.serialization.JsonOps; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.play.SystemChatPacket; 6 | import io.github.kgriff0n.packet.server.PlayerDataPacket; 7 | import io.github.kgriff0n.util.DummyPlayer; 8 | import io.github.kgriff0n.api.ServersLinkApi; 9 | import net.minecraft.network.ClientConnection; 10 | import net.minecraft.network.message.MessageType; 11 | import net.minecraft.network.message.SentMessage; 12 | import net.minecraft.network.message.SignedMessage; 13 | import net.minecraft.network.packet.Packet; 14 | import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; 15 | import net.minecraft.registry.RegistryOps; 16 | import net.minecraft.server.PlayerManager; 17 | import net.minecraft.server.network.ConnectedClientData; 18 | import net.minecraft.server.network.ServerPlayerEntity; 19 | import net.minecraft.text.Text; 20 | import net.minecraft.text.TextCodecs; 21 | import org.jetbrains.annotations.Nullable; 22 | import org.spongepowered.asm.mixin.Final; 23 | import org.spongepowered.asm.mixin.Mixin; 24 | import org.spongepowered.asm.mixin.Shadow; 25 | import org.spongepowered.asm.mixin.Unique; 26 | import org.spongepowered.asm.mixin.injection.At; 27 | import org.spongepowered.asm.mixin.injection.Inject; 28 | import org.spongepowered.asm.mixin.injection.Redirect; 29 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 30 | 31 | import java.io.IOException; 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | import java.util.function.Predicate; 35 | 36 | import static io.github.kgriff0n.ServersLink.SERVER; 37 | 38 | @Mixin(PlayerManager.class) 39 | public abstract class PlayerManagerMixin { 40 | 41 | @Shadow public abstract void broadcast(Text message, boolean overlay); 42 | 43 | @Shadow public abstract void sendToAll(Packet packet); 44 | 45 | @Shadow @Final private List players; 46 | 47 | @Unique 48 | private ServerPlayerEntity player; 49 | 50 | @Inject(at = @At("HEAD"), method = "broadcast(Lnet/minecraft/text/Text;Z)V") 51 | private void sendSystemPacket(Text message, boolean overlay, CallbackInfo ci) { 52 | SystemChatPacket packet = new SystemChatPacket(TextCodecs.CODEC.encodeStart(RegistryOps.of(JsonOps.INSTANCE, SERVER.getRegistryManager()), message).getOrThrow().toString()); 53 | ServersLinkApi.send(packet, ServersLink.getServerInfo().getName()); 54 | } 55 | 56 | @Inject(at = @At("HEAD"), method = "onPlayerConnect") 57 | private void getPlayer(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, CallbackInfo ci) { 58 | this.player = player; 59 | } 60 | 61 | @Inject(at = @At("TAIL"), method = "savePlayerData") 62 | private void sendPlayerData(ServerPlayerEntity player, CallbackInfo ci) { 63 | try { 64 | ServersLinkApi.send(new PlayerDataPacket(player.getUuid()), ServersLink.getServerInfo().getName()); 65 | } catch (IOException e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | @Redirect(method = "onPlayerConnect", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V")) 71 | private void preventConnectMessage(PlayerManager instance, Text message, boolean overlay) { 72 | if (ServersLinkApi.getPreventConnect().contains(player.getUuid())) { 73 | ServersLinkApi.getPreventConnect().remove(player.getUuid()); 74 | } else { 75 | this.broadcast(message, overlay); 76 | } 77 | } 78 | 79 | @Redirect(method = "onPlayerConnect", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;sendToAll(Lnet/minecraft/network/packet/Packet;)V")) 80 | private void sendPlayerList(PlayerManager instance, Packet packet) { 81 | List allPlayers = new ArrayList<>(); 82 | allPlayers.addAll(players); 83 | allPlayers.addAll(ServersLinkApi.getDummyPlayers()); 84 | this.sendToAll(PlayerListS2CPacket.entryFromPlayer(allPlayers)); 85 | } 86 | 87 | @Inject(at = @At("HEAD"), method = "savePlayerData", cancellable = true) 88 | private void savePlayerDataThreadSafe(ServerPlayerEntity player, CallbackInfo ci) { 89 | if (player instanceof DummyPlayer) { 90 | ci.cancel(); 91 | } 92 | } 93 | 94 | @Inject(at = @At("HEAD"), method = "broadcast(Lnet/minecraft/network/message/SignedMessage;Ljava/util/function/Predicate;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/network/message/MessageType$Parameters;)V") 95 | private void broadcastDummy(SignedMessage message, Predicate shouldSendFiltered, @Nullable ServerPlayerEntity sender, MessageType.Parameters params, CallbackInfo ci) { 96 | SentMessage sentMessage = SentMessage.of(message); 97 | for (ServerPlayerEntity serverPlayerEntity : ServersLinkApi.getDummyPlayers()) { 98 | boolean bl3 = shouldSendFiltered.test(serverPlayerEntity); 99 | serverPlayerEntity.sendChatMessage(sentMessage, bl3, params); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/socket/G2SConnection.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.socket; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.Packet; 6 | import io.github.kgriff0n.packet.info.NewPlayerPacket; 7 | import io.github.kgriff0n.packet.info.NewServerPacket; 8 | import io.github.kgriff0n.server.Group; 9 | import io.github.kgriff0n.server.ServerInfo; 10 | import io.github.kgriff0n.api.ServersLinkApi; 11 | import io.github.kgriff0n.server.Settings; 12 | import net.minecraft.server.network.ServerPlayerEntity; 13 | import net.minecraft.text.Text; 14 | import net.minecraft.util.Formatting; 15 | 16 | import java.io.*; 17 | import java.net.Socket; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.RejectedExecutionException; 23 | 24 | import static io.github.kgriff0n.ServersLink.IS_RUNNING; 25 | import static io.github.kgriff0n.ServersLink.SERVER; 26 | 27 | public class G2SConnection extends Thread { 28 | 29 | private ServerInfo server; 30 | 31 | private final ExecutorService executor; 32 | 33 | private final Socket socket; 34 | private ObjectInputStream in; 35 | private ObjectOutputStream out; 36 | 37 | public G2SConnection(Socket socket) { 38 | this.socket = socket; 39 | this.executor = Executors.newSingleThreadExecutor(); 40 | } 41 | 42 | public synchronized void send(Packet packet) { 43 | if (executor.isShutdown()) { 44 | ServersLink.LOGGER.warn("Can't send {}", packet.getClass().getName()); 45 | } else { 46 | try { 47 | executor.submit(() -> { 48 | try { 49 | if (Gateway.getInstance().isDebugEnabled()) { 50 | ServersLink.LOGGER.info("\u001B[34mPacket sent class {}", packet.getClass().getName()); 51 | } 52 | out.writeObject(packet); 53 | out.flush(); 54 | out.reset(); 55 | } catch (IOException e) { 56 | ServersLink.LOGGER.error("Unable to send {} - {}", packet.getClass().getName(), e.getMessage()); 57 | } 58 | }); 59 | } catch (RejectedExecutionException e) { 60 | ServersLink.LOGGER.error("Can't send {}, executor already closed", packet.getClass().getName()); 61 | } 62 | } 63 | } 64 | 65 | @Override 66 | public void run() { 67 | try { 68 | out = new ObjectOutputStream(socket.getOutputStream()); 69 | out.flush(); 70 | 71 | in = new ObjectInputStream(socket.getInputStream()); 72 | 73 | while (IS_RUNNING) { 74 | Packet packet = (Packet) in.readObject(); 75 | if (Gateway.getInstance().isDebugEnabled()) ServersLink.LOGGER.info("\u001B[95mPacket received {}", packet.getClass()); 76 | if (packet instanceof NewServerPacket pkt) { 77 | this.server = pkt.getServer(); 78 | this.setName(String.format("%s thread", server.getName())); 79 | ServersLinkApi.addServer(server, this); 80 | Gateway.getInstance().getGroup(server.getGroupId()).addServer(server); 81 | ServersLink.LOGGER.info("Add {} sub-server", server.getName()); 82 | /* Adds all players to the new server */ 83 | Settings globalSettings = Gateway.getInstance().getGroup("global").getSettings(); 84 | Settings serverSettings = Gateway.getInstance().getGroup(server.getGroupId()).getSettings(); 85 | if (globalSettings.isPlayerListSynced() && serverSettings.isPlayerListSynced()) { 86 | // Send real players 87 | for (ServerPlayerEntity player : SERVER.getPlayerManager().getPlayerList()) { 88 | send(new NewPlayerPacket(player.getGameProfile())); 89 | } 90 | // Send fake players 91 | for (ServerPlayerEntity player : ServersLinkApi.getDummyPlayers()) { 92 | send(new NewPlayerPacket(player.getGameProfile())); 93 | } 94 | } else if (!globalSettings.isPlayerListSynced() && serverSettings.isPlayerListSynced()) { 95 | Group serverGroup = Gateway.getInstance().getGroup(server.getGroupId()); 96 | List syncPlayers = new ArrayList<>(serverGroup.getServersList()); 97 | for (Group otherGroup : Gateway.getInstance().getGroups()) { 98 | if (Gateway.getInstance().getSettings(serverGroup.getName(), otherGroup.getName()).isPlayerListSynced()) { 99 | syncPlayers.addAll(otherGroup.getServersList()); 100 | } 101 | } 102 | 103 | for (ServerInfo serverInfo : syncPlayers) { 104 | if (!serverInfo.getName().equals(server.getName())) { 105 | for (GameProfile profile : serverInfo.getGameProfile()) { 106 | send(new NewPlayerPacket(profile)); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | SERVER.execute(() -> packet.onGatewayReceive(server.getName())); 113 | } 114 | socket.close(); 115 | } catch (IOException e) { 116 | if (e.getMessage() != null) { 117 | ServersLink.LOGGER.error("Error {} in sub-server {}", e.getMessage(), server.getName()); 118 | ServersLink.LOGGER.info(this.server.toString()); 119 | } 120 | ServersLinkApi.disconnectServer(this.server); 121 | ServersLinkApi.broadcastToOp(Text.literal("Sub-server " + server.getName() + " has disconnected").formatted(Formatting.RED)); 122 | this.interrupt(); 123 | } catch (ClassNotFoundException e) { 124 | ServersLink.LOGGER.error("Receive invalid data: {}", e.getMessage()); 125 | } 126 | } 127 | 128 | @Override 129 | public void interrupt() { 130 | super.interrupt(); 131 | executor.shutdown(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/mixin/PlayerEntityMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.mixin; 2 | 3 | import com.mojang.serialization.Codec; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.util.IPlayerServersLink; 6 | import net.minecraft.entity.player.PlayerEntity; 7 | import net.minecraft.registry.RegistryKey; 8 | import net.minecraft.registry.RegistryKeys; 9 | import net.minecraft.server.world.ServerWorld; 10 | import net.minecraft.storage.ReadView; 11 | import net.minecraft.storage.WriteView; 12 | import net.minecraft.util.Identifier; 13 | import net.minecraft.util.math.Vec3d; 14 | import net.minecraft.world.World; 15 | import org.spongepowered.asm.mixin.Mixin; 16 | import org.spongepowered.asm.mixin.Unique; 17 | import org.spongepowered.asm.mixin.injection.At; 18 | import org.spongepowered.asm.mixin.injection.Inject; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 20 | 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Objects; 25 | 26 | @Mixin(PlayerEntity.class) 27 | public class PlayerEntityMixin implements IPlayerServersLink { 28 | 29 | @Unique 30 | private HashMap serversPos = new HashMap<>(); 31 | 32 | @Unique 33 | private HashMap> serversRot = new HashMap<>(); 34 | 35 | @Unique 36 | private HashMap serversDim = new HashMap<>(); 37 | 38 | @Inject(at = @At("HEAD"), method = "writeCustomData") 39 | private void writeNbt(WriteView view, CallbackInfo ci) { 40 | WriteView serversLink = view.get("ServersLink"); 41 | WriteView posView = serversLink.get("Position"); 42 | WriteView rotView = serversLink.get("Rotation"); 43 | WriteView dimView = serversLink.get("Dimension"); 44 | 45 | for (Map.Entry entry : serversPos.entrySet()) { 46 | String name = entry.getKey(); 47 | Vec3d pos = entry.getValue(); 48 | 49 | WriteView.ListAppender posAppender = posView.getListAppender(name, Codec.DOUBLE); 50 | posAppender.add(pos.getX()); 51 | posAppender.add(pos.getY()); 52 | posAppender.add(pos.getZ()); 53 | } 54 | 55 | for (Map.Entry> entry : serversRot.entrySet()) { 56 | String name = entry.getKey(); 57 | List rot = entry.getValue(); 58 | 59 | WriteView.ListAppender rotAppender = rotView.getListAppender(name, Codec.FLOAT); 60 | rotAppender.add(rot.get(0)); 61 | rotAppender.add(rot.get(1)); 62 | } 63 | 64 | for (Map.Entry entry : serversDim.entrySet()) { 65 | String name = entry.getKey(); 66 | ServerWorld dim = entry.getValue(); 67 | 68 | dimView.putString(name, dim.getRegistryKey().getValue().toString().split(":")[1]); 69 | } 70 | } 71 | 72 | @Inject(at = @At("HEAD"), method = "readCustomData") 73 | private void readNbt(ReadView view, CallbackInfo ci) { 74 | Codec>> posMapCodec = 75 | Codec.unboundedMap(Codec.STRING, Codec.list(Codec.DOUBLE)); 76 | Codec> dimMapCodec = 77 | Codec.unboundedMap(Codec.STRING, Codec.STRING); 78 | Codec>> rotMapCodec = 79 | Codec.unboundedMap(Codec.STRING, Codec.list(Codec.FLOAT)); 80 | 81 | 82 | view.getOptionalReadView("ServersLink") 83 | .flatMap(v -> v.read("Position", posMapCodec)) 84 | .ifPresent(posMap -> { 85 | this.serversPos = new HashMap<>(); 86 | posMap.forEach((server, coords) -> { 87 | if (coords.size() >= 3) { 88 | serversPos.put(server, new Vec3d(coords.get(0), coords.get(1), coords.get(2))); 89 | } 90 | }); 91 | }); 92 | 93 | view.getOptionalReadView("ServersLink") 94 | .flatMap(v -> v.read("Rotation", rotMapCodec)) 95 | .ifPresent(rotMap -> { 96 | this.serversRot = new HashMap<>(); 97 | rotMap.forEach((server, rotations) -> { 98 | if (rotations.size() >= 2) { 99 | serversRot.put(server, List.of(rotations.get(0), rotations.get(1))); 100 | } 101 | }); 102 | }); 103 | 104 | view.getOptionalReadView("ServersLink") 105 | .flatMap(v -> v.read("Dimension", dimMapCodec)) 106 | .ifPresent(dimMap -> { 107 | this.serversDim = new HashMap<>(); 108 | dimMap.forEach((server, dimId) -> { 109 | RegistryKey key = RegistryKey.of(RegistryKeys.WORLD, Identifier.ofVanilla(dimId)); 110 | World world = (PlayerEntity) (Object) this instanceof PlayerEntity player ? Objects.requireNonNull(player.getEntityWorld().getServer()).getWorld(key) : null; 111 | if (world instanceof ServerWorld serverWorld) { 112 | serversDim.put(server, serverWorld); 113 | } 114 | }); 115 | }); 116 | } 117 | 118 | @Override 119 | public void servers_link$setServerPos(String name, Vec3d pos) { 120 | this.serversPos.put(name, pos); 121 | } 122 | 123 | @Override 124 | public Vec3d servers_link$getServerPos(String name) { 125 | return this.serversPos.get(name); 126 | } 127 | 128 | @Override 129 | public void servers_link$removeServerPos(String name) { 130 | this.serversPos.remove(name); 131 | } 132 | 133 | @Override 134 | public void servers_link$setServerRot(String name, float yaw, float pitch) { 135 | List rot = List.of(yaw, pitch); 136 | this.serversRot.put(name, rot); 137 | } 138 | 139 | @Override 140 | public List servers_link$getServerRot(String name) { 141 | return this.serversRot.get(name); 142 | } 143 | 144 | @Override 145 | public void servers_link$removeServerRot(String name) { 146 | this.serversRot.remove(name); 147 | } 148 | 149 | @Override 150 | public void servers_link$setServerDim(String name, ServerWorld dim) { 151 | this.serversDim.put(name, dim); 152 | } 153 | 154 | @Override 155 | public ServerWorld servers_link$getServerDim(String name) { 156 | return this.serversDim.get(name); 157 | } 158 | 159 | @Override 160 | public void servers_link$removeServerDim(String name) { 161 | this.serversDim.remove(name); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/event/PlayerJoin.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.event; 2 | 3 | import io.github.kgriff0n.PlayersInformation; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.packet.info.NewPlayerPacket; 6 | import io.github.kgriff0n.packet.server.PlayerAcknowledgementPacket; 7 | import io.github.kgriff0n.packet.info.ServersInfoPacket; 8 | import io.github.kgriff0n.socket.Gateway; 9 | import io.github.kgriff0n.socket.SubServer; 10 | import io.github.kgriff0n.util.IPlayerServersLink; 11 | import io.github.kgriff0n.server.ServerInfo; 12 | import io.github.kgriff0n.api.ServersLinkApi; 13 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; 14 | import net.fabricmc.fabric.api.networking.v1.PacketSender; 15 | import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 16 | import net.minecraft.entity.Entity; 17 | import net.minecraft.network.listener.ServerPlayPacketListener; 18 | import net.minecraft.network.packet.s2c.play.PositionFlag; 19 | import net.minecraft.server.MinecraftServer; 20 | import net.minecraft.server.network.ServerPlayNetworkHandler; 21 | import net.minecraft.server.network.ServerPlayerEntity; 22 | import net.minecraft.server.world.ServerWorld; 23 | import net.minecraft.text.Text; 24 | import net.minecraft.util.Formatting; 25 | import net.minecraft.util.math.Vec3d; 26 | import net.minecraft.world.TeleportTarget; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | import java.util.Set; 31 | import java.util.function.Consumer; 32 | 33 | import static io.github.kgriff0n.ServersLink.LOGGER; 34 | 35 | public class PlayerJoin implements ServerPlayConnectionEvents.Join, ServerEntityEvents.Load { 36 | 37 | private static ArrayList joinedPlayers = new ArrayList<>(); 38 | 39 | @Override 40 | public void onPlayReady(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender packetSender, MinecraftServer minecraftServer) { 41 | 42 | ServerPlayerEntity newPlayer = serverPlayNetworkHandler.player; 43 | 44 | if (!joinedPlayers.contains(newPlayer)) joinedPlayers.add(newPlayer); 45 | 46 | /* Dummy player packet */ 47 | NewPlayerPacket dummyPlayer = new NewPlayerPacket(newPlayer.getGameProfile()); 48 | 49 | /* Players can only connect from the hub */ 50 | if (ServersLink.isGateway) { 51 | Gateway gateway = Gateway.getInstance(); 52 | if (gateway.isConnectedPlayer(newPlayer.getUuid()) && !ServersLinkApi.getPreventConnect().contains(newPlayer.getUuid())) { 53 | ServersLinkApi.transferPlayer(newPlayer, ServersLink.getServerInfo().getName(), ServersLinkApi.whereIs(newPlayer.getUuid())); 54 | ServersLinkApi.getPreventConnect().add(newPlayer.getUuid()); 55 | ServersLinkApi.getPreventDisconnect().add(newPlayer.getUuid()); 56 | } else { 57 | String lastServer = PlayersInformation.getLastServer(newPlayer.getUuid()); 58 | ServerInfo lastServerInfo = ServersLinkApi.getServer(lastServer); 59 | if (lastServer == null || lastServer.equals(ServersLink.getServerInfo().getName()) 60 | || lastServerInfo == null || lastServerInfo.isDown() || !gateway.shouldReconnectToLastServer()) { 61 | ServersLinkApi.getServer(ServersLink.getServerInfo().getName()).addPlayer(newPlayer.getGameProfile()); 62 | /* Delete the fake player */ 63 | ServersLinkApi.getDummyPlayers().removeIf(player -> player.getName().equals(newPlayer.getName())); 64 | 65 | /* Send player information to other servers */ 66 | gateway.forward(dummyPlayer, ServersLink.getServerInfo().getName()); 67 | gateway.sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 68 | 69 | if (gateway.shouldReconnectToLastServer() && lastServer != null && !lastServer.isEmpty() && (lastServerInfo == null || lastServerInfo.isDown())) { 70 | newPlayer.sendMessage(Text.literal("An unexpected error occurred while attempting to reconnect you to your previous server").formatted(Formatting.RED)); 71 | } 72 | } else { 73 | ServersLinkApi.transferPlayer(newPlayer, ServersLink.getServerInfo().getName(), lastServer); 74 | } 75 | } 76 | } else { 77 | SubServer connection = SubServer.getInstance(); 78 | if (!connection.getWaitingPlayers().contains(newPlayer.getUuid())) { 79 | serverPlayNetworkHandler.disconnect(Text.translatable("multiplayer.status.cannot_connect").formatted(Formatting.RED)); 80 | /* Used to prevent the logout message in ServerPlayNetworkHandlerMixin#preventDisconnectMessage */ 81 | ServersLinkApi.getPreventConnect().add(serverPlayNetworkHandler.player.getUuid()); 82 | ServersLinkApi.getPreventDisconnect().add(serverPlayNetworkHandler.player.getUuid()); 83 | } else { 84 | /* The player logs in and is removed from the list of waiting players */ 85 | connection.removeWaitingPlayer(newPlayer.getUuid()); 86 | /* Delete the fake player */ 87 | ServersLinkApi.getDummyPlayers().removeIf(player -> player.getName().equals(newPlayer.getName())); 88 | /* Send player information to other servers */ 89 | connection.send(dummyPlayer); 90 | connection.send(new PlayerAcknowledgementPacket(ServersLink.getServerInfo().getName(), newPlayer.getGameProfile())); 91 | } 92 | } 93 | 94 | } 95 | 96 | @Override 97 | public void onLoad(Entity entity, ServerWorld serverWorld) { 98 | if (!(entity instanceof ServerPlayerEntity newPlayer)){ 99 | return; 100 | } 101 | 102 | if (!joinedPlayers.contains(newPlayer)) { 103 | return; 104 | } else joinedPlayers.remove(newPlayer); 105 | 106 | Vec3d pos = ((IPlayerServersLink) newPlayer).servers_link$getServerPos(ServersLink.getServerInfo().getName()); 107 | ServerWorld dim = ((IPlayerServersLink) newPlayer).servers_link$getServerDim(ServersLink.getServerInfo().getName()); 108 | List rot = ((IPlayerServersLink) newPlayer).servers_link$getServerRot(ServersLink.getServerInfo().getName()); 109 | 110 | if (pos == null || dim == null || rot == null) { 111 | // Player data not found, probably first join - teleport to world spawn 112 | pos = new Vec3d(newPlayer.getEntityWorld().getSpawnPoint().getPos().getX() + 0.5, newPlayer.getEntityWorld().getSpawnPoint().getPos().getY(), newPlayer.getEntityWorld().getSpawnPoint().getPos().getZ() + 0.5); 113 | dim = newPlayer.getEntityWorld().getServer().getOverworld(); // Change in 1.21.9 because world spawn can be in any dimension 114 | rot = List.of(newPlayer.getYaw(), newPlayer.getPitch()); 115 | } 116 | 117 | TeleportTarget.PostDimensionTransition enableFlight = (flyingEntity) -> { 118 | if (!(flyingEntity instanceof ServerPlayerEntity player)) return; 119 | if (!player.getAbilities().allowFlying) return; 120 | player.getAbilities().flying = true; 121 | player.sendAbilitiesUpdate(); 122 | }; 123 | 124 | TeleportTarget teleportTarget = new TeleportTarget( 125 | dim, pos, Vec3d.ZERO, rot.get(0), rot.get(1), enableFlight); 126 | 127 | LOGGER.info("Player " + newPlayer.getName().getString() + " position: " + newPlayer.getX() + ", " + newPlayer.getY() + ", " + newPlayer.getZ() + " in dimension " + newPlayer.getEntityWorld().getRegistryKey().getValue().toString()); 128 | LOGGER.info("Teleporting player " + newPlayer.getName().getString() + " to " + pos.x + ", " + pos.y + ", " + pos.z + " in dimension " + (dim != null ? dim.getRegistryKey().getValue().toString() : "null")); 129 | //if (pos != null && dim != null) newPlayer.teleport(dim, posX, posY, posZ, posFlags, yaw, pitch, true); 130 | newPlayer.teleportTo(teleportTarget); 131 | LOGGER.info("Player " + newPlayer.getName().getString() + " position: " + newPlayer.getX() + ", " + newPlayer.getY() + ", " + newPlayer.getZ() + " in dimension " + newPlayer.getEntityWorld().getRegistryKey().getValue().toString()); 132 | 133 | 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/api/ServersLinkApi.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.api; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import io.github.kgriff0n.ServersLink; 5 | import io.github.kgriff0n.event.ServerTick; 6 | import io.github.kgriff0n.packet.Packet; 7 | import io.github.kgriff0n.packet.info.ServersInfoPacket; 8 | import io.github.kgriff0n.packet.play.PlayerDisconnectPacket; 9 | import io.github.kgriff0n.packet.play.PlayerTransferPacket; 10 | import io.github.kgriff0n.socket.Gateway; 11 | import io.github.kgriff0n.socket.G2SConnection; 12 | import io.github.kgriff0n.socket.SubServer; 13 | import io.github.kgriff0n.util.DummyPlayer; 14 | import io.github.kgriff0n.server.ServerInfo; 15 | import net.minecraft.network.packet.s2c.common.ServerTransferS2CPacket; 16 | import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; 17 | import net.minecraft.server.network.ServerPlayerEntity; 18 | import net.minecraft.text.Text; 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.util.*; 22 | 23 | import static io.github.kgriff0n.ServersLink.SERVER; 24 | 25 | public class ServersLinkApi { 26 | 27 | private static final HashMap serverList = new HashMap<>(); 28 | 29 | private static final HashSet preventConnect = new HashSet<>(); 30 | private static final HashSet preventDisconnect = new HashSet<>(); 31 | 32 | public static List dummyPlayers = new ArrayList<>(); 33 | 34 | public static HashSet getPreventConnect() { 35 | return preventConnect; 36 | } 37 | 38 | public static HashSet getPreventDisconnect() { 39 | return preventDisconnect; 40 | } 41 | 42 | public static HashMap getServerMap() { 43 | return serverList; 44 | } 45 | 46 | public static ArrayList getServerList() { 47 | return new ArrayList<>(serverList.keySet()); 48 | } 49 | 50 | public static void setServerList(ArrayList list) { 51 | serverList.clear(); 52 | for (ServerInfo server : list) { 53 | serverList.put(server, null); 54 | } 55 | } 56 | 57 | /** 58 | * Retrieves the list of server names 59 | * @return a list containing all the server names 60 | */ 61 | public static ArrayList getServerNames() { 62 | ArrayList names = new ArrayList<>(); 63 | for (ServerInfo server : serverList.keySet()) { 64 | names.add(server.getName()); 65 | } 66 | return names; 67 | } 68 | 69 | /** 70 | * @param groupId id of the group 71 | * @return the list of server from a specified group 72 | */ 73 | public static ArrayList getServers(String groupId) { 74 | ArrayList list = new ArrayList<>(); 75 | for (ServerInfo server : serverList.keySet()) { 76 | if (server.getGroupId().equals(groupId)) { 77 | list.add(server); 78 | } 79 | } 80 | return list; 81 | } 82 | 83 | /** 84 | * @param serverName the name of the server 85 | * @return the server with this name 86 | */ 87 | public static ServerInfo getServer(String serverName) { 88 | for (ServerInfo server : serverList.keySet()) { 89 | if (server.getName().equals(serverName)) { 90 | return server; 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | /** 97 | * Adds a new server to the list of sub-servers 98 | * @param server a new server 99 | * @param connection used from the hub for packet transfer 100 | */ 101 | public static void addServer(ServerInfo server, @Nullable G2SConnection connection) { 102 | SERVER.execute(() -> { 103 | serverList.remove(server); // remove old one 104 | serverList.put(server, connection); 105 | }); 106 | } 107 | 108 | /** 109 | * Disconnects a server from the hub and prevents packets 110 | * from being sent to that server 111 | * @param server the server to be disconnected 112 | */ 113 | public static void disconnectServer(ServerInfo server) { 114 | SERVER.execute(() -> { 115 | Gateway gateway = Gateway.getInstance(); 116 | server.getPlayersList().forEach((uuid, name) -> { 117 | gateway.sendAll(new PlayerDisconnectPacket(uuid)); 118 | dummyPlayers.removeIf(player -> player.getUuid().equals(uuid)); 119 | }); 120 | gateway.sendAll(new ServersInfoPacket(ServersLinkApi.getServerList())); 121 | server.getPlayersList().clear(); 122 | server.getGameProfile().clear(); 123 | serverList.put(server, null); 124 | }); 125 | } 126 | 127 | /** 128 | * @return the total number of sub-servers 129 | * connected to the hub 130 | */ 131 | public static int getRunningSubServers() { 132 | int count = 0; 133 | for (G2SConnection connection : serverList.values()) { 134 | if (connection != null) count++; 135 | } 136 | return count; 137 | } 138 | 139 | /** 140 | * Find out which server a player is connected to 141 | * @param uuid the player uuid 142 | * @return the name of the server 143 | */ 144 | public static String whereIs(UUID uuid) { 145 | for (ServerInfo serverInfo : ServersLinkApi.getServerList()) { 146 | if (serverInfo.getPlayersList().containsKey(uuid)) { 147 | return serverInfo.getName(); 148 | } 149 | } 150 | return null; 151 | } 152 | 153 | /** 154 | * Sends a packet. If called from the hub, the packet is 155 | * sent to all other sub-servers, otherwise it is sent to the hub. 156 | * @param packet the packet to send 157 | */ 158 | public static void send(Packet packet, String source) { 159 | if (ServersLink.isGateway) { 160 | Gateway.getInstance().forward(packet, source); 161 | } else { 162 | SubServer.getInstance().send(packet); 163 | } 164 | } 165 | 166 | /** 167 | * Sends a message to all operator players (ops). 168 | * @param text the text to send 169 | */ 170 | public static void broadcastToOp(Text text) { 171 | for (String playerName : SERVER.getPlayerManager().getOpList().getNames()) { 172 | ServerPlayerEntity player = SERVER.getPlayerManager().getPlayer(playerName); 173 | if (player != null && !(player instanceof DummyPlayer)) { 174 | player.sendMessage(text); 175 | } 176 | } 177 | } 178 | 179 | /** 180 | * Adds a dummy player to the list of players, 181 | * allowing it to be displayed in the list and in the command auto-completion. 182 | * If a player or a dummy player with the same uuid is 183 | * already present in the list, the dummy player will not be added. 184 | * @param profile profile of the player, must contain his uuid, name and textures properties 185 | */ 186 | public static void addDummyPlayer(GameProfile profile) { 187 | List playerList = SERVER.getPlayerManager().getPlayerList(); 188 | 189 | boolean alreadyPresent = false; 190 | for (DummyPlayer player : dummyPlayers) { 191 | if (player.getUuid().equals(profile.id())) { 192 | alreadyPresent = true; 193 | } 194 | } 195 | 196 | if (!alreadyPresent) { 197 | dummyPlayers.add(new DummyPlayer(profile)); 198 | 199 | /* Update player list for all players */ 200 | List allPlayers = new ArrayList<>(); 201 | allPlayers.addAll(playerList); 202 | allPlayers.addAll(dummyPlayers); 203 | for (ServerPlayerEntity player : playerList) { 204 | player.networkHandler.sendPacket(PlayerListS2CPacket.entryFromPlayer(allPlayers)); 205 | } 206 | } 207 | } 208 | 209 | public static List getDummyPlayers() { 210 | return dummyPlayers; 211 | } 212 | 213 | /** 214 | * Returns the player with the given UUID, used to retrieve dummy players. 215 | * @param uuid the UUID of the player 216 | * @return the player with this UUID 217 | */ 218 | public static ServerPlayerEntity getDummyPlayer(UUID uuid) { 219 | for (DummyPlayer player : dummyPlayers) { 220 | if (player.getUuid().equals(uuid)) return player; 221 | } 222 | return null; 223 | } 224 | 225 | /** 226 | * Returns the player with the given username, used to retrieve dummy players. 227 | * @param playerName the name of the player 228 | * @return the player with this UUID 229 | */ 230 | public static ServerPlayerEntity getDummyPlayer(String playerName) { 231 | for (DummyPlayer player : dummyPlayers) { 232 | if (player.getNameForScoreboard().equals(playerName)) return player; 233 | } 234 | return null; 235 | } 236 | 237 | /** 238 | * Transfers a player to another server. 239 | * @param player the player to transfer 240 | * @param originServer name of the current server 241 | * @param serverName the name of the server to which the player will be transferred 242 | */ 243 | public static void transferPlayer(ServerPlayerEntity player, String originServer, String serverName) { 244 | ServerInfo server = ServersLinkApi.getServer(serverName); 245 | 246 | if (ServersLink.isGateway) { 247 | /* add player to other server list and send packet */ 248 | PlayerTransferPacket transferPacket = new PlayerTransferPacket(player.getUuid(), serverName); 249 | transferPacket.onGatewayReceive(originServer); 250 | } else { 251 | /* send packet, add player to transferred list and transfer the player */ 252 | SubServer connection = SubServer.getInstance(); 253 | connection.send(new PlayerTransferPacket(player.getUuid(), serverName)); 254 | } 255 | 256 | player.networkHandler.sendPacket(new ServerTransferS2CPacket(server.getIp(), server.getPort())); 257 | ServerTick.scheduleDisconnect(player.getUuid(), 20); // delay 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/socket/Gateway.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.socket; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonArray; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import io.github.kgriff0n.ServersLink; 8 | import io.github.kgriff0n.packet.Packet; 9 | import io.github.kgriff0n.server.Group; 10 | import io.github.kgriff0n.server.ServerInfo; 11 | import io.github.kgriff0n.api.ServersLinkApi; 12 | import io.github.kgriff0n.server.Settings; 13 | 14 | import java.io.IOException; 15 | import java.net.ServerSocket; 16 | import java.net.Socket; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.*; 20 | 21 | import static io.github.kgriff0n.ServersLink.IS_RUNNING; 22 | import static io.github.kgriff0n.ServersLink.SERVER; 23 | 24 | public class Gateway extends Thread { 25 | 26 | public static Gateway gateway; 27 | 28 | private HashMap groups; 29 | private ServerSocket serverSocket; 30 | 31 | private boolean debug; 32 | private boolean globalPlayerCount; 33 | private boolean whitelistIp; 34 | private final List whitelistedIp = new ArrayList<>(); 35 | private boolean reconnectLastServer; 36 | 37 | public Gateway(int port) { 38 | if (gateway != null) { 39 | ServersLink.LOGGER.info("Gateway server already started"); 40 | } 41 | try { 42 | serverSocket = new ServerSocket(port); 43 | gateway = this; 44 | groups = new HashMap<>(); 45 | loadGroups(); 46 | ServersLinkApi.addServer(ServersLink.getServerInfo(), null); 47 | } catch (IOException e) { 48 | ServersLink.LOGGER.info("Unable to start central server"); 49 | } 50 | } 51 | 52 | public static Gateway getInstance() { 53 | return gateway; 54 | } 55 | 56 | public void sendAll(Packet packet) { 57 | for (G2SConnection sub : ServersLinkApi.getServerMap().values()) { 58 | if (sub != null) sub.send(packet); 59 | } 60 | } 61 | 62 | public void sendTo(Packet packet, String serverName) { 63 | if (serverName.equals(ServersLink.getServerInfo().getName())) { 64 | SERVER.execute(packet::onReceive); 65 | } else { 66 | for (ServerInfo server : ServersLinkApi.getServerList()) { 67 | if (server.getName().equals(serverName)) { 68 | ServersLinkApi.getServerMap().get(server).send(packet); 69 | } 70 | } 71 | } 72 | } 73 | 74 | public void forward(Packet packet, String sourceServer) { 75 | String sourceGroup = ServersLinkApi.getServer(sourceServer).getGroupId(); 76 | for (ServerInfo server : ServersLinkApi.getServerList()) { 77 | G2SConnection sub = ServersLinkApi.getServerMap().get(server); 78 | if (sub != null && !server.getName().equals(sourceServer)) { 79 | if (isDebugEnabled()) ServersLink.LOGGER.info("\u001B[33mForward packet {} to {}?", packet.getClass().getName(), server.getName()); 80 | if (packet.shouldReceive(getSettings(sourceGroup, server.getGroupId()))) { 81 | if (isDebugEnabled()) ServersLink.LOGGER.info("\u001B[32mYes"); 82 | sub.send(packet); 83 | } else { 84 | if (isDebugEnabled()) ServersLink.LOGGER.info("\u001B[31mNo"); 85 | } 86 | } 87 | } 88 | } 89 | 90 | public void removePlayer(UUID uuid) { 91 | for (ServerInfo server : ServersLinkApi.getServerList()) { 92 | server.removePlayer(uuid); 93 | } 94 | } 95 | 96 | public boolean isConnectedPlayer(UUID uuid) { 97 | for (ServerInfo server : ServersLinkApi.getServerList()) { 98 | if (server.getPlayersList().containsKey(uuid)) { 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | 105 | public Settings getSettings(String sourceGroup, String destinationGroup) { 106 | Group a = groups.get(sourceGroup); 107 | if (a.getRules().containsKey(destinationGroup)) { 108 | return a.getRules().get(destinationGroup); 109 | } else if (sourceGroup.equals(destinationGroup)) { 110 | return a.getSettings(); 111 | } else { 112 | return groups.get("global").getSettings(); 113 | } 114 | } 115 | 116 | public Group getGroup(String groupId) { 117 | return groups.get(groupId); 118 | } 119 | 120 | public Collection getGroups() { 121 | return groups.values(); 122 | } 123 | 124 | public void loadConfig() { 125 | Path path = ServersLink.CONFIG.resolve("config.json"); 126 | try { 127 | String jsonContent = Files.readString(path); 128 | Gson gson = new Gson(); 129 | JsonObject jsonObject = gson.fromJson(jsonContent, JsonObject.class); 130 | debug = jsonObject.get("debug").getAsBoolean(); 131 | globalPlayerCount = jsonObject.get("global_player_count").getAsBoolean(); 132 | whitelistIp = jsonObject.get("whitelist_ip").getAsBoolean(); 133 | for (JsonElement element : jsonObject.getAsJsonArray("whitelisted_ip")) { 134 | whitelistedIp.add(element.getAsString()); 135 | } 136 | reconnectLastServer = jsonObject.get("reconnect_last_server").getAsBoolean(); 137 | } catch (IOException e) { 138 | ServersLink.LOGGER.error("Unable to read config.json"); 139 | } 140 | } 141 | 142 | public boolean isDebugEnabled() { 143 | return debug; 144 | } 145 | 146 | public boolean isGlobalPlayerCountEnabled() { 147 | return globalPlayerCount; 148 | } 149 | 150 | public boolean hasWhitelistIp() { 151 | return whitelistIp; 152 | } 153 | 154 | public List getWhitelistedIp() { 155 | return whitelistedIp; 156 | } 157 | 158 | public boolean shouldReconnectToLastServer() { 159 | return reconnectLastServer; 160 | } 161 | 162 | private void loadGroups() { 163 | Path path = ServersLink.CONFIG.resolve("groups.json"); 164 | try { 165 | String jsonContent = Files.readString(path); 166 | Gson gson = new Gson(); 167 | JsonObject jsonObject = gson.fromJson(jsonContent, JsonObject.class); 168 | // GROUPS 169 | JsonObject jsonGroups = jsonObject.getAsJsonObject("groups"); 170 | // Global 171 | JsonObject global = jsonGroups.getAsJsonObject("global"); 172 | Settings globalSettings = new Settings( 173 | global.get("player_list").getAsBoolean(), 174 | global.get("chat").getAsBoolean(), 175 | global.get("player_data").getAsBoolean(), 176 | global.get("whitelist").getAsBoolean(), 177 | global.get("roles").getAsBoolean() 178 | ); 179 | groups.put("global", new Group("global", globalSettings)); 180 | // Others 181 | for (Map.Entry entry : jsonGroups.entrySet()) { 182 | JsonObject otherGroup = entry.getValue().getAsJsonObject(); 183 | Settings otherSettings = new Settings( 184 | otherGroup.has("player_list") ? otherGroup.get("player_list").getAsBoolean() : globalSettings.isPlayerListSynced(), 185 | otherGroup.has("chat") ? otherGroup.get("chat").getAsBoolean() : globalSettings.isChatSynced(), 186 | otherGroup.has("player_data") ? otherGroup.get("player_data").getAsBoolean() : globalSettings.isPlayerDataSynced(), 187 | otherGroup.has("whitelist") ? otherGroup.get("whitelist").getAsBoolean() : globalSettings.isWhitelistSynced(), 188 | otherGroup.has("roles") ? otherGroup.get("roles").getAsBoolean() : globalSettings.isRolesSynced() 189 | ); 190 | if (!entry.getKey().equals("global")) { // Doesn't re-add global group 191 | groups.put(entry.getKey(), new Group(entry.getKey(), otherSettings)); 192 | } 193 | } 194 | 195 | // RULES 196 | JsonArray jsonRules = jsonObject.get("rules").getAsJsonArray(); 197 | for (JsonElement element : jsonRules) { 198 | JsonObject rule = element.getAsJsonObject(); 199 | JsonArray ruleGroups = rule.getAsJsonArray("groups"); 200 | Settings ruleSettings = new Settings( 201 | rule.has("player_list") ? rule.get("player_list").getAsBoolean() : globalSettings.isPlayerListSynced(), 202 | rule.has("chat") ? rule.get("chat").getAsBoolean() : globalSettings.isChatSynced(), 203 | rule.has("player_data") ? rule.get("player_data").getAsBoolean() : globalSettings.isPlayerDataSynced(), 204 | rule.has("whitelist") ? rule.get("whitelist").getAsBoolean() : globalSettings.isWhitelistSynced(), 205 | rule.has("roles") ? rule.get("roles").getAsBoolean() : globalSettings.isRolesSynced() 206 | ); 207 | for (int i = 0; i < ruleGroups.size(); i++) { 208 | String groupId = ruleGroups.get(i).getAsString(); 209 | for (int j = 0; j < ruleGroups.size(); j++) { 210 | if (i != j) { 211 | groups.get(groupId).addRule(ruleGroups.get(j).getAsString(), ruleSettings); 212 | } 213 | } 214 | } 215 | } 216 | } catch (IOException e) { 217 | ServersLink.LOGGER.error("Unable to read groups.json"); 218 | } 219 | } 220 | 221 | @Override 222 | public void run() { 223 | while (IS_RUNNING) { 224 | try { 225 | Socket socket = serverSocket.accept(); 226 | if (whitelistIp) { 227 | if (whitelistedIp.contains(socket.getInetAddress().getHostAddress())) { 228 | G2SConnection connection = new G2SConnection(socket); 229 | connection.start(); 230 | } else { 231 | ServersLink.LOGGER.warn("Unauthorized connection received from {}", socket.getInetAddress().getHostAddress()); 232 | socket.close(); 233 | } 234 | } else { 235 | G2SConnection connection = new G2SConnection(socket); 236 | connection.start(); 237 | } 238 | } catch (IOException e) { 239 | ServersLink.LOGGER.info("Unable to accept connection"); 240 | } 241 | 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/io/github/kgriff0n/command/ServerCommand.java: -------------------------------------------------------------------------------- 1 | package io.github.kgriff0n.command; 2 | 3 | import com.mojang.brigadier.Command; 4 | import com.mojang.brigadier.arguments.StringArgumentType; 5 | import io.github.kgriff0n.ServersLink; 6 | import io.github.kgriff0n.packet.play.TeleportationAcceptPacket; 7 | import io.github.kgriff0n.packet.play.TeleportationRequestPacket; 8 | import io.github.kgriff0n.socket.Gateway; 9 | import io.github.kgriff0n.socket.SubServer; 10 | import io.github.kgriff0n.util.DummyPlayer; 11 | import io.github.kgriff0n.util.IPlayerServersLink; 12 | import io.github.kgriff0n.api.ServersLinkApi; 13 | import io.github.kgriff0n.server.ServerInfo; 14 | import me.lucko.fabric.api.permissions.v0.Permissions; 15 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 16 | import net.minecraft.command.argument.EntityArgumentType; 17 | import net.minecraft.command.argument.Vec3ArgumentType; 18 | import net.minecraft.network.packet.s2c.play.PositionFlag; 19 | import net.minecraft.server.command.CommandManager; 20 | import net.minecraft.server.command.ServerCommandSource; 21 | import net.minecraft.server.network.ServerPlayerEntity; 22 | import net.minecraft.text.MutableText; 23 | import net.minecraft.text.Text; 24 | import net.minecraft.util.Formatting; 25 | import net.minecraft.util.math.Vec3d; 26 | 27 | import java.util.EnumSet; 28 | import java.util.Locale; 29 | 30 | import static net.minecraft.server.command.CommandManager.argument; 31 | import static net.minecraft.server.command.CommandManager.literal; 32 | 33 | public class ServerCommand { 34 | public static void register() { 35 | CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("server") 36 | .then(literal("list") 37 | .requires(Permissions.require("server.list", 2)) 38 | .executes(context -> list(context.getSource())) 39 | ) 40 | .then(literal("join") 41 | .requires(Permissions.require("server.join", 2)) 42 | .then(argument("server", StringArgumentType.string()) 43 | .suggests((context, builder) -> { 44 | for (String serverName : ServersLinkApi.getServerNames()) { 45 | builder.suggest(serverName); 46 | } 47 | return builder.buildFuture(); 48 | }) 49 | .executes(context -> join(context.getSource().getPlayer(), StringArgumentType.getString(context, "server"))) 50 | .then(argument("player", EntityArgumentType.player()) 51 | .requires(Permissions.require("server.join.other", 2)) 52 | .executes(context -> join(EntityArgumentType.getPlayer(context, "player"), StringArgumentType.getString(context, "server"))) 53 | .then(argument("position", Vec3ArgumentType.vec3()) 54 | .requires(Permissions.require("server.join.position", 2)) 55 | .executes(context -> joinPos(EntityArgumentType.getPlayer(context, "player"), StringArgumentType.getString(context, "server"), Vec3ArgumentType.getVec3(context, "position"))) 56 | ) 57 | ) 58 | ) 59 | 60 | ) 61 | .then(literal("whereis") 62 | .requires(Permissions.require("server.whereis", 2)) 63 | .then(argument("player", EntityArgumentType.player()) 64 | .executes(context -> whereis(context.getSource(), EntityArgumentType.getPlayer(context, "player"))) 65 | ) 66 | ) 67 | .then(literal("tpto") 68 | .requires(Permissions.require("server.tpto", 2)) 69 | .then(argument("player", EntityArgumentType.player()) 70 | .executes(context -> teleportTo(context.getSource(), EntityArgumentType.getPlayer(context, "player"))) 71 | ) 72 | ) 73 | .then(literal("tphere") 74 | .requires(Permissions.require("server.tphere", 2)) 75 | .then(argument("player", EntityArgumentType.player()) 76 | .executes(context -> teleportHere(context.getSource(), EntityArgumentType.getPlayer(context, "player"))) 77 | ) 78 | ) 79 | .then(literal("dummyplayerlist") 80 | .requires(Permissions.require("server.dummyplayerlist", 2)) 81 | .executes(context -> dummyPlayerList(context.getSource())) 82 | ) 83 | .then(CommandManager.literal("run") 84 | .requires(Permissions.require("server.run", 2)) 85 | .redirect(dispatcher.getRoot())) 86 | )); 87 | } 88 | 89 | private static int list(ServerCommandSource source) { 90 | ServerPlayerEntity player = source.getPlayer(); 91 | if (player != null) { 92 | player.sendMessage(Text.literal("Server List").formatted(Formatting.BOLD, Formatting.DARK_GRAY)); 93 | 94 | for (ServerInfo server : ServersLinkApi.getServerList()) { 95 | MutableText status = Text.literal("●"); 96 | if (server.isDown()) { 97 | status.formatted(Formatting.RED); 98 | } else { 99 | status.formatted(Formatting.GREEN); 100 | } 101 | 102 | MutableText players = Text.literal(String.valueOf(server.getPlayersList().size())).formatted(Formatting.WHITE); 103 | 104 | MutableText tps = Text.literal(String.format(Locale.ENGLISH, "%.1f", server.getTps())); 105 | if (server.getTps() > 15) { 106 | tps.formatted(Formatting.GREEN); 107 | } else if (server.getTps() > 10) { 108 | tps.formatted(Formatting.YELLOW); 109 | } else if (server.getTps() > 0) { 110 | tps.formatted(Formatting.RED); 111 | } else { 112 | tps.formatted(Formatting.DARK_RED); 113 | } 114 | player.sendMessage( 115 | Text.literal("[").append(status).append("] " + server.getName()) 116 | .append(" | ").append(players).append(" player(s)") 117 | .append(" (").append(tps).append(" TPS)") 118 | .formatted(Formatting.GRAY)); 119 | } 120 | } else { 121 | for (ServerInfo server : ServersLinkApi.getServerList()) { 122 | ServersLink.LOGGER.info("{} | {} | {} TPS | {} players", server.getName(), server.isDown() ? "Closed" : "Running", server.getTps(), server.getPlayersList().size()); 123 | } 124 | } 125 | 126 | return Command.SINGLE_SUCCESS; 127 | } 128 | 129 | private static int join(ServerPlayerEntity player, String serverName) { 130 | if (player != null) { 131 | /* Save player pos */ 132 | String name = ServersLink.getServerInfo().getName(); 133 | ((IPlayerServersLink) player).servers_link$setServerPos(name, player.getEntityPos()); 134 | 135 | if (name.equals(serverName)) { 136 | player.sendMessage(Text.literal("You are already connected to this server").formatted(Formatting.RED)); 137 | } else if (ServersLinkApi.getServer(serverName) == null) { 138 | player.sendMessage(Text.literal("This server does not exist").formatted(Formatting.RED)); 139 | } else { 140 | ServersLinkApi.transferPlayer(player, ServersLink.getServerInfo().getName(), serverName); 141 | } 142 | } 143 | return Command.SINGLE_SUCCESS; 144 | } 145 | 146 | private static int joinPos(ServerPlayerEntity player, String serverName, Vec3d pos) { 147 | ((IPlayerServersLink) player).servers_link$setServerPos(serverName, pos); 148 | return join(player, serverName); 149 | } 150 | 151 | private static int whereis(ServerCommandSource source, ServerPlayerEntity player) { 152 | ServerPlayerEntity sender = source.getPlayer(); 153 | if (sender != null) { 154 | sender.sendMessage(Text.literal(player.getName().getString() + " is on " + ServersLinkApi.whereIs(player.getUuid()))); 155 | } else { 156 | ServersLink.LOGGER.info("{} is on {}", player.getName().getString(), ServersLinkApi.whereIs(player.getUuid())); 157 | } 158 | return Command.SINGLE_SUCCESS; 159 | } 160 | 161 | private static int teleportTo(ServerCommandSource source, ServerPlayerEntity player) { 162 | ServerPlayerEntity sender = source.getPlayer(); 163 | String server = ServersLinkApi.whereIs(player.getUuid()); 164 | if (sender == null) return 0; 165 | if (server.equals(ServersLink.getServerInfo().getName())) { 166 | sender.teleport(player.getEntityWorld(), player.getX(), player.getY(), player.getZ(), EnumSet.noneOf(PositionFlag.class), player.getYaw(), player.getPitch(), false); 167 | } else { 168 | TeleportationRequestPacket request = new TeleportationRequestPacket(player.getUuid(), sender.getUuid(), ServersLink.getServerInfo().getName(), server); 169 | if (ServersLink.isGateway) { 170 | Gateway.getInstance().sendTo(request, server); 171 | } else { 172 | SubServer.getInstance().send(request); 173 | } 174 | } 175 | return Command.SINGLE_SUCCESS; 176 | } 177 | 178 | private static int teleportHere(ServerCommandSource source, ServerPlayerEntity player) { 179 | ServerPlayerEntity sender = source.getPlayer(); 180 | String server = ServersLinkApi.whereIs(player.getUuid()); 181 | if (sender == null) return 0; 182 | if (server.equals(ServersLink.getServerInfo().getName())) { 183 | player.teleport(sender.getEntityWorld(), sender.getX(), sender.getY(), sender.getZ(), EnumSet.noneOf(PositionFlag.class), sender.getYaw(), sender.getPitch(), false); 184 | } else { 185 | TeleportationAcceptPacket accept = new TeleportationAcceptPacket(sender.getX(), sender.getY(), sender.getZ(), player.getUuid(), server, ServersLink.getServerInfo().getName()); 186 | if (ServersLink.isGateway) { 187 | Gateway.getInstance().sendTo(accept, server); 188 | } else { 189 | SubServer.getInstance().send(accept); 190 | } 191 | } 192 | return Command.SINGLE_SUCCESS; 193 | } 194 | 195 | private static int dummyPlayerList(ServerCommandSource source) { 196 | ServerPlayerEntity player = source.getPlayer(); 197 | for (DummyPlayer dummy : ServersLinkApi.getDummyPlayers()) { 198 | if (player == null) { 199 | ServersLink.LOGGER.info(dummy.getNameForScoreboard()); 200 | } else { 201 | player.sendMessage(dummy.getName()); 202 | } 203 | } 204 | 205 | return Command.SINGLE_SUCCESS; 206 | } 207 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Servers Link 2 | 3 | Servers Link is a `server-side` mod that lets you link multiple servers together. 4 | With this mod, players' inventory, achievements, chat *(including private messages)* and the player list are synchronized between the servers. 5 | 6 | If you want to see this mod in action, check out this video: https://www.youtube.com/watch?v=-_P2IAu5Y0A 7 | A better video with more details may come in the future. 8 | 9 | > [!IMPORTANT] 10 | > This mod is still in beta, so please report any bugs you find. 11 | 12 | If you want to install and configure this mod, continue reading this [README](README.md). If you want a more detailed explanation of how this mod works, or if you're a developer, take a look at the [EXPLANATIONS](EXPLANATIONS.md) and [USAGE](USAGE.md). 13 | If you have any questions, feel free to ask on [discord](https://discord.com/invite/ZeHm57BEyt)! 14 | 15 | --- 16 | 17 | ## Installation 18 | 19 | As this mod is only `server-side` you first need to set up a Fabric server. After your server is set up, put the `.jar` file downloaded from [Modrinth](https://modrinth.com/mod/servers-link) in the `./mods` folder (you also need to install [Fabric API](https://modrinth.com/mod/fabric-api)). 20 | Once you have put everything in the folder, start the server. It will immediately close with an error and the next thing to do will be to configure everything. 21 | 22 | ## Configuration 23 | 24 | ### Properties 25 | As this mod uses the transfer system added in 1.20.5, you need to configure your server to accept transfers. In the `server.properties` file, set the following line: 26 | 27 | ```properties 28 | accepts-transfers=true 29 | ``` 30 | 31 | Next, open the `config` folder on your server and create a new folder named `servers-link`. Inside this folder, you must always have a file named `info.json`. This file is used to describe all the information related to the current server. 32 | The following options must be configured: 33 | 34 | | Option | Description | Value | 35 | |--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------:| 36 | | group | The group in which the server will be located. Groups are explained in the next section. | String | 37 | | gateway | If the server is the gateway or not. Only one of your servers can be the gateway.
This is the server the players will use to connect. | True / False | 38 | | gateway-ip | This is not the IP used by players to connect, but the IP used to communicate between the servers.
If all your servers are in local you can set this to `127.0.0.1`. | IP Address | 39 | | gateway-port | Same as above. If you want to connect servers located in another network, you must allow connections to this port. | Port | 40 | | server-name | Name of the server (multiple servers can't have the same name). | String | 41 | | server-ip | IP of the Minecraft server. | IP Address | 42 | | server-port | Port of the Minecraft server. | Port | 43 | | command-name | (optional) Name of the added command. If not specified defaults to /server. | String | 44 | Here is an example file: 45 | ```json 46 | { 47 | "group": "global", 48 | "gateway": true, 49 | "gateway-ip": "127.0.0.1", 50 | "gateway-port": 59001, 51 | "server-name": "Hub", 52 | "server-ip": "127.0.0.1", 53 | "server-port": 25565, 54 | "command-name": "network" 55 | } 56 | ``` 57 | 58 | > [!IMPORTANT] 59 | > All ports specified in `server-port` must remain open. 60 | > When you stop the gateway server, all other servers are stopped. 61 | > 62 | > [!TIP] 63 | > To get Servers Link to work in a Velocity proxy setup, you need to set the `command-name` option in the `info.json` file to something other than `server` (for example, `network`). 64 | > 65 | > This is because Velocity uses the `/server` command by default. 66 | 67 | If the server is your gateway, you must add another file named `config.json`. This file contains the general configuration settings for the gateway. 68 | 69 | | Option | Description | Value | 70 | |-----------------------|---------------------------------------------------------------------------------------------------|:--------------------:| 71 | | debug | Enables debug messages to be displayed in the console. | True / False | 72 | | global_player_count | Set to true if you want the gateway player count to be the sum of each sub-server's player count. | | 73 | | whitelist_ip | Set to true if you don't want all IPs to be able to connect a server to the gateway. | True / False | 74 | | whitelisted_ip | A list of allowed IPs (eg: ["192.168.0.1","192.168.0.2"]). | List of IP Addresses | 75 | | reconnect_last_server | Indicates whether players should be reconnected to the last server from which they disconnected. | True / False | 76 | 77 | Here is an example file: 78 | ```json 79 | { 80 | "debug": false, 81 | "global_player_count": true, 82 | "whitelist_ip": false, 83 | "whitelisted_ip": [], 84 | "reconnect_last_server": true 85 | } 86 | ``` 87 | 88 | > [!CAUTION] 89 | > If `whitelist-ip` is set to `false` and the **gateway's port is open**, anyone can install this mod and connect their server to your gateway. 90 | 91 | ### Groups 92 | 93 | If the server is the gateway, you must add another file named `groups.json` to define the groups. 94 | For each group, you can configure the following options: 95 | 96 | | Option | Description | Value | 97 | |-------------|----------------------------------------------------------------------------------------|:------------:| 98 | | chat | If chat messages are shared between all servers. | True / False | 99 | | player-data | If inventory, achievements and statistics of players are synchronized between servers. | True / False | 100 | | player-list | If player list is synchronized between servers. | True / False | 101 | | roles | If roles are synchronized between servers (support Player Roles). | True / False | 102 | | whitelist | If whitelist is synchronized between servers. | True / False | 103 | 104 | 105 | > [!WARNING] 106 | > If you set chat to true, you must also set player-list to true. 107 | 108 | The default group is named `global`. For this group, you must configure all the options listed above. 109 | Then, you can add as many groups as you want. For each new group, you only need to configure the options that differ from those in the `global` group. 110 | 111 | ### Example 112 | 113 | Let’s imagine we want to set up a hub server that will be the gateway, the entry point to all our servers. 114 | In addition to the hub, we have two survival servers and one creative server. 115 | The player list and chat should not be synchronized between the hub and the other servers. However, they should be synchronized between the survival servers and the creative server. 116 | Player data should be synchronized only between the two survival servers, and not with the creative server. 117 | 118 | The following `groups.json` correspond to this situation: 119 | 120 | ```json 121 | { 122 | "groups": { 123 | "global": { 124 | "chat": false, 125 | "player_data": false, 126 | "player_list": false, 127 | "roles": false, 128 | "whitelist": false 129 | }, 130 | "survival": { 131 | "player_data": true, 132 | "player_list": true, 133 | "chat": true 134 | }, 135 | "creative": { 136 | "player_data": true, 137 | "player_list": true, 138 | "chat": true 139 | } 140 | }, 141 | "rules": [ 142 | { 143 | "groups": ["survival", "creative"], 144 | "player_list": true, 145 | "chat": true 146 | } 147 | ] 148 | } 149 | ``` 150 | 151 | The `rules` section allow us to enable some settings between a defined set of servers. 152 | 153 | And these are the `info.json` files for each server: 154 | 155 | `Hub` 156 | ```json 157 | { 158 | "group": "global", 159 | "gateway": true, 160 | "gateway-ip": "127.0.0.1", 161 | "gateway-port": 59001, 162 | "server-name": "Hub", 163 | "server-ip": "127.0.0.1", 164 | "server-port": 25565 165 | } 166 | ``` 167 | `Creative` 168 | ```json 169 | { 170 | "group": "creative", 171 | "gateway": false, 172 | "gateway-ip": "127.0.0.1", 173 | "gateway-port": 59001, 174 | "server-name": "Creative", 175 | "server-ip": "127.0.0.1", 176 | "server-port": 25566 177 | } 178 | ``` 179 | 180 | `Survival 1` 181 | ```json 182 | { 183 | "group": "survival", 184 | "gateway": false, 185 | "gateway-ip": "127.0.0.1", 186 | "gateway-port": 59001, 187 | "server-name": "Survival-1", 188 | "server-ip": "127.0.0.1", 189 | "server-port": 25567 190 | } 191 | ``` 192 | 193 | `Survival 2` 194 | ```json 195 | { 196 | "group": "survival", 197 | "gateway": false, 198 | "gateway-ip": "127.0.0.1", 199 | "gateway-port": 59001, 200 | "server-name": "Survival-2", 201 | "server-ip": "127.0.0.1", 202 | "server-port": 25568 203 | } 204 | ``` 205 | 206 | Here is an example schema of this situation. You can find the configuration folder for each server [here](example). 207 | 208 | > [!IMPORTANT] 209 | > The configuration files must be placed in the `config/servers-link` folder and the server IP addresses and ports must be the same as those specified in `info.json` files. 210 | 211 | ![Schema](img/schema.png) 212 | 213 | ## Commands 214 | 215 | This mod adds the `server` command (or the command specified in `info.json`) and the following sub-commands. 216 | 217 | | Sub-command | Description | Permissions | 218 | |:--------------------------------------------:|--------------------------------------------------------|-------------------------:| 219 | | list | Lists all connected servers. | `server.list` | 220 | | join `[server-name]` | Joins the server. | `server.join` | 221 | | join `[server-name]` `[player]` | Makes the player join the server. | `server.join.other` | 222 | | join `[server-name]` `[player]` `[position]` | Makes the player join the server at the given position | `server.join.position` | 223 | | whereis `[player]` | Indicates on which server the player is. | `server.whereis` | 224 | | tpto `[player]` | Teleports you to the player. | `server.tpto` | 225 | | tphere `[player]` | Teleports the player to your position. | `server.tphere` | 226 | | run `[command]` | Allows you to run a command on all servers. | `server.run` | --------------------------------------------------------------------------------