commit 4a3119f730c874a31f03d610cc2b8351dba519d4 Author: rtm516 Date: Sun Jun 14 02:07:00 2020 +0100 Initial commit Added base server and a static server list diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9acce91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,222 @@ + +# Created by https://www.gitignore.io/api/git,java,maven,eclipse,netbeans,jetbrains+all +# Edit at https://www.gitignore.io/?templates=git,java,maven,eclipse,netbeans,jetbrains+all + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +### Eclipse Patch ### +# Eclipse Core +.project + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Annotation Processing +.apt_generated + +.sts4-cache/ + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +### NetBeans ### +**/nbproject/private/ +**/nbproject/Makefile-*.mk +**/nbproject/Package-*.bash +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +# End of https://www.gitignore.io/api/git,java,maven,eclipse,netbeans,jetbrains+all diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ec04a54 --- /dev/null +++ b/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.geysermc + geyser-multi + 1.0-SNAPSHOT + + GeyserMulti + Allows for players from Minecraft Bedrock Edition to join Minecraft Java Edition servers via a masterServer list. + https://geysermc.org + + + GeyserMulti + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + + jitpack.io + https://jitpack.io + + + nukkitx-release-repo + https://repo.nukkitx.com/maven-releases/ + + + nukkitx-snapshot-repo + https://repo.nukkitx.com/maven-snapshots/ + + + + + + org.projectlombok + lombok + 1.18.4 + compile + + + org.geysermc + connector + 1.0-SNAPSHOT + compile + + + com.nukkitx.protocol + bedrock-v390 + 2.5.6 + compile + + + net.minecrell + terminalconsoleappender + 1.1.1 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.geysermc.multi.GeyserMulti + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/geysermc/multi/GeyserMulti.java b/src/main/java/org/geysermc/multi/GeyserMulti.java new file mode 100644 index 0000000..ea45703 --- /dev/null +++ b/src/main/java/org/geysermc/multi/GeyserMulti.java @@ -0,0 +1,16 @@ +package org.geysermc.multi; + +import com.nukkitx.protocol.bedrock.BedrockPacketCodec; +import com.nukkitx.protocol.bedrock.v390.Bedrock_v390; + +public class GeyserMulti { + + public static final BedrockPacketCodec CODEC = Bedrock_v390.V390_CODEC; + + private static MasterServer masterServer; + + + public static void main(String[] args) { + new MasterServer(); + } +} diff --git a/src/main/java/org/geysermc/multi/Logger.java b/src/main/java/org/geysermc/multi/Logger.java new file mode 100644 index 0000000..933994c --- /dev/null +++ b/src/main/java/org/geysermc/multi/Logger.java @@ -0,0 +1,75 @@ +package org.geysermc.multi; + +import lombok.extern.log4j.Log4j2; +import net.minecrell.terminalconsole.SimpleTerminalConsole; +import org.apache.logging.log4j.core.config.Configurator; +import org.geysermc.common.ChatColor; + +@Log4j2 +public class Logger extends SimpleTerminalConsole { + + private boolean colored = true; + + @Override + protected boolean isRunning() { + return !MasterServer.getInstance().isShuttingDown(); + } + + @Override + protected void runCommand(String line) { + // Dont do anything rn + } + + @Override + protected void shutdown() { + MasterServer.getInstance().shutdown(); + } + + public void severe(String message) { + log.fatal(printConsole(ChatColor.DARK_RED + message, colored)); + } + + public void severe(String message, Throwable error) { + log.fatal(printConsole(ChatColor.DARK_RED + message, colored), error); + } + + public void error(String message) { + log.error(printConsole(ChatColor.RED + message, colored)); + } + + public void error(String message, Throwable error) { + log.error(printConsole(ChatColor.RED + message, colored), error); + } + + public void warning(String message) { + log.warn(printConsole(ChatColor.YELLOW + message, colored)); + } + + public void info(String message) { + log.info(printConsole(ChatColor.WHITE + message, colored)); + } + + public void debug(String message) { + log.debug(printConsole(ChatColor.GRAY + message, colored)); + } + + public static String printConsole(String message, boolean colors) { + return colors ? ChatColor.toANSI(message + ChatColor.RESET) : ChatColor.stripColors(message + ChatColor.RESET); + } + + public void setDebug(boolean debug) { + Configurator.setLevel(log.getName(), debug ? org.apache.logging.log4j.Level.DEBUG : log.getLevel()); + } + + public String getName() { + return "CONSOLE"; + } + + public void sendMessage(String message) { + info(message); + } + + public boolean isConsole() { + return true; + } +} diff --git a/src/main/java/org/geysermc/multi/MasterServer.java b/src/main/java/org/geysermc/multi/MasterServer.java new file mode 100644 index 0000000..277527a --- /dev/null +++ b/src/main/java/org/geysermc/multi/MasterServer.java @@ -0,0 +1,97 @@ +package org.geysermc.multi; + +import com.nukkitx.protocol.bedrock.BedrockPong; +import com.nukkitx.protocol.bedrock.BedrockServer; +import com.nukkitx.protocol.bedrock.BedrockServerEventHandler; +import com.nukkitx.protocol.bedrock.BedrockServerSession; +import lombok.Getter; + +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +public class MasterServer { + + private final Timer timer; + private BedrockServer bdServer; + private BedrockPong bdPong; + + @Getter + private boolean shuttingDown = false; + + @Getter + private static MasterServer instance; + + @Getter + private final Logger logger; + + @Getter + private final ScheduledExecutorService generalThreadPool; + + @Getter + private final Map players = new HashMap<>(); + + public MasterServer() { + logger = new Logger(); + logger.setDebug(true); + + this.instance = this; + this.generalThreadPool = Executors.newScheduledThreadPool(32); + + // Start a timer to keep the thread running + timer = new Timer(); + TimerTask task = new TimerTask() { public void run() { } }; + timer.scheduleAtFixedRate(task, 0L, 1000L); + + start(19132); + + logger.start(); + } + + private void start(int port) { + logger.info("Starting..."); + + InetSocketAddress bindAddress = new InetSocketAddress("0.0.0.0", port); + bdServer = new BedrockServer(bindAddress); + + bdPong = new BedrockPong(); + bdPong.setEdition("MCPE"); + bdPong.setMotd("My Server"); + bdPong.setPlayerCount(0); + bdPong.setMaximumPlayerCount(1337); + bdPong.setGameType("Survival"); + bdPong.setIpv4Port(port); + bdPong.setProtocolVersion(GeyserMulti.CODEC.getProtocolVersion()); + bdPong.setVersion(GeyserMulti.CODEC.getMinecraftVersion()); + + bdServer.setHandler(new BedrockServerEventHandler() { + @Override + public boolean onConnectionRequest(InetSocketAddress address) { + return true; // Connection will be accepted + } + + @Override + public BedrockPong onQuery(InetSocketAddress address) { + return bdPong; + } + + @Override + public void onSessionCreation(BedrockServerSession session) { + session.setPacketHandler(new PacketHandler(session, instance)); + } + }); + + // Start server up + bdServer.bind().join(); + logger.info("Server started on 0.0.0.0:" + port); + } + + public void shutdown() { + shuttingDown = true; + generalThreadPool.shutdown(); + } +} diff --git a/src/main/java/org/geysermc/multi/PacketHandler.java b/src/main/java/org/geysermc/multi/PacketHandler.java new file mode 100644 index 0000000..5c702b3 --- /dev/null +++ b/src/main/java/org/geysermc/multi/PacketHandler.java @@ -0,0 +1,154 @@ +package org.geysermc.multi; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +import com.nukkitx.network.util.DisconnectReason; +import com.nukkitx.protocol.bedrock.BedrockServerSession; +import com.nukkitx.protocol.bedrock.handler.BedrockPacketHandler; +import com.nukkitx.protocol.bedrock.packet.*; +import com.nukkitx.protocol.bedrock.util.EncryptionUtils; +import org.geysermc.multi.UI.UIHandler; + +import java.io.IOException; +import java.security.interfaces.ECPublicKey; +import java.util.ArrayList; +import java.util.List; + +public class PacketHandler implements BedrockPacketHandler { + + private BedrockServerSession session; + private MasterServer masterServer; + + private Player player; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);; + + public PacketHandler(BedrockServerSession session, MasterServer masterServer) { + this.session = session; + this.masterServer = masterServer; + + session.addDisconnectHandler((reason) -> disconnect(reason)); + } + + public void disconnect(DisconnectReason reason) { + if (player != null) { + masterServer.getLogger().info(player.getDisplayName() + " has disconnected (" + reason + ")"); + masterServer.getPlayers().remove(session.getAddress()); + } + } + + @Override + public boolean handle(LoginPacket packet) { + masterServer.getLogger().debug("Login: " + packet.toString()); + + // Check the protocol version is correct + int protocol = packet.getProtocolVersion(); + if (protocol != GeyserMulti.CODEC.getProtocolVersion()) { + PlayStatusPacket status = new PlayStatusPacket(); + if (protocol > GeyserMulti.CODEC.getProtocolVersion()) { + status.setStatus(PlayStatusPacket.Status.FAILED_SERVER); + } else { + status.setStatus(PlayStatusPacket.Status.FAILED_CLIENT); + } + session.sendPacket(status); + } + + // Set the session codec + session.setPacketCodec(GeyserMulti.CODEC); + + JsonNode rawChainData; + try { + rawChainData = OBJECT_MAPPER.readTree(packet.getChainData().toByteArray()); + } catch (IOException e) { + throw new AssertionError("Unable to read chain data!"); + } + + JsonNode chainData = rawChainData.get("chain"); + if (chainData.getNodeType() != JsonNodeType.ARRAY) { + throw new AssertionError("Invalid chain data!"); + } + + try { + // Parse the signed jws object + JWSObject jwsObject; + jwsObject = JWSObject.parse(chainData.get(chainData.size() - 1).asText()); + + JsonNode payload = OBJECT_MAPPER.readTree(jwsObject.getPayload().toBytes()); + + if (payload.get("identityPublicKey").getNodeType() != JsonNodeType.STRING) { + throw new AssertionError("Missing identity public key!"); + } + + ECPublicKey identityPublicKey = EncryptionUtils.generateKey(payload.get("identityPublicKey").textValue()); + + JWSObject skinData = JWSObject.parse(packet.getSkinData().toString()); + if (skinData.verify(new DefaultJWSVerifierFactory().createJWSVerifier(skinData.getHeader(), identityPublicKey))) { + if (payload.get("extraData").getNodeType() != JsonNodeType.OBJECT) { + throw new AssertionError("Missing client data"); + } + + JsonNode extraData = payload.get("extraData"); + + player = new Player(extraData, session); + masterServer.getPlayers().put(session.getAddress(), player); + + // Tell the client we have logged in successfully + PlayStatusPacket playStatusPacket = new PlayStatusPacket(); + playStatusPacket.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS); + session.sendPacket(playStatusPacket); + + // Tell the client there are no resourcepacks + ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); + session.sendPacket(resourcePacksInfo); + } else { + throw new AssertionError("Invalid identity public key!"); + } + } catch (Exception e) { + // Disconnect the client + session.disconnect("disconnectionScreen.internalError.cantConnect"); + throw new AssertionError("Failed to login", e); + } + + return false; + } + + @Override + public boolean handle(ResourcePackClientResponsePacket packet) { + switch (packet.getStatus()) { + case COMPLETED: + masterServer.getLogger().info("Logged in " + player.getDisplayName() + " (" + player.getXuid() + ")"); + player.sendStartGame(); + break; + case HAVE_ALL_PACKS: + ResourcePackStackPacket stack = new ResourcePackStackPacket(); + stack.setExperimental(false); + stack.setForcedToAccept(false); + stack.setGameVersion("*"); + session.sendPacket(stack); + break; + default: + session.disconnect("disconnectionScreen.resourcePack"); + break; + } + + return true; + } + + @Override + public boolean handle(SetLocalPlayerAsInitializedPacket packet) { + masterServer.getLogger().debug("Player initialized: " + player.getDisplayName()); + + player.sendServerList(); + + /*TransferPacket transferPacket = new TransferPacket(); + transferPacket.setAddress("81.174.164.211"); + transferPacket.setPort(27040); + session.sendPacket(transferPacket);*/ + + return false; + } +} diff --git a/src/main/java/org/geysermc/multi/PalleteManger.java b/src/main/java/org/geysermc/multi/PalleteManger.java new file mode 100644 index 0000000..fb6e0d6 --- /dev/null +++ b/src/main/java/org/geysermc/multi/PalleteManger.java @@ -0,0 +1,62 @@ +package org.geysermc.multi; + +import com.nukkitx.nbt.CompoundTagBuilder; +import com.nukkitx.nbt.NbtUtils; +import com.nukkitx.nbt.stream.NBTInputStream; +import com.nukkitx.nbt.stream.NBTOutputStream; +import com.nukkitx.nbt.tag.CompoundTag; +import com.nukkitx.nbt.tag.ListTag; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.utils.FileUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class is mostly copied from core Geyser + */ +public class PalleteManger { + public static final ListTag BLOCK_PALLETE; + public static final CompoundTag BIOMES_PALLETE; + public static final byte[] EMPTY_LEVEL_CHUNK_DATA; + + private static final com.nukkitx.nbt.tag.CompoundTag EMPTY_TAG = CompoundTagBuilder.builder().buildRootTag(); + + static { + /* Load block palette */ + InputStream stream = FileUtils.getResource("runtime_block_states.dat"); + + try (NBTInputStream nbtInputStream = NbtUtils.createNetworkReader(stream)) { + BLOCK_PALLETE = (ListTag) nbtInputStream.readTag(); + } catch (Exception e) { + throw new AssertionError("Unable to get blocks from runtime block states", e); + } + + /* Load biomes */ + stream = FileUtils.getResource("biome_definitions.dat"); + + try (NBTInputStream nbtInputStream = NbtUtils.createNetworkReader(stream)){ + BIOMES_PALLETE = (CompoundTag) nbtInputStream.readTag(); + } catch (Exception e) { + throw new AssertionError("Failed to get biomes from biome definitions", e); + } + + /* Create empty chunk data */ + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size + + try (NBTOutputStream nbtOutputStream = NbtUtils.createNetworkWriter(outputStream)) { + nbtOutputStream.write(EMPTY_TAG); + } + + EMPTY_LEVEL_CHUNK_DATA = outputStream.toByteArray(); + }catch (IOException e) { + throw new AssertionError("Unable to generate empty level chunk data"); + } + } + + public static void init() { + // no-op + } +} diff --git a/src/main/java/org/geysermc/multi/Player.java b/src/main/java/org/geysermc/multi/Player.java new file mode 100644 index 0000000..8330f34 --- /dev/null +++ b/src/main/java/org/geysermc/multi/Player.java @@ -0,0 +1,120 @@ +package org.geysermc.multi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.nukkitx.math.vector.Vector2f; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.protocol.bedrock.BedrockServerSession; +import com.nukkitx.protocol.bedrock.data.*; +import com.nukkitx.protocol.bedrock.packet.*; +import lombok.Getter; +import org.geysermc.multi.UI.UIHandler; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Getter +public class Player { + + private String xuid; + private UUID identity; + private String displayName; + + private BedrockServerSession session; + + private final List servers = new ArrayList<>(); + + public Player(JsonNode extraData, BedrockServerSession session) { + this.xuid = extraData.get("XUID").asText(); + this.identity = UUID.fromString(extraData.get("identity").asText()); + this.displayName = extraData.get("displayName").asText(); + + this.session = session; + + // Should fetch the servers from some form of db + servers.add(new Server("mc.hypixel.net")); + servers.add(new Server("81.174.164.211", 25580)); + } + + public void sendStartGame() { + StartGamePacket startGamePacket = new StartGamePacket(); + startGamePacket.setUniqueEntityId(1); + startGamePacket.setRuntimeEntityId(1); + startGamePacket.setPlayerGamemode(0); + startGamePacket.setPlayerPosition(Vector3f.from(0, 64 + 2, 0)); + startGamePacket.setRotation(Vector2f.ONE); + + startGamePacket.setSeed(-1); + startGamePacket.setDimensionId(2); + startGamePacket.setGeneratorId(1); + startGamePacket.setLevelGamemode(1); + startGamePacket.setDifficulty(0); + startGamePacket.setDefaultSpawn(Vector3i.ZERO); + startGamePacket.setAchievementsDisabled(true); + startGamePacket.setTime(-1); + startGamePacket.setEduEditionOffers(0); + startGamePacket.setEduFeaturesEnabled(false); + startGamePacket.setRainLevel(0); + startGamePacket.setLightningLevel(0); + startGamePacket.setMultiplayerGame(true); + startGamePacket.setBroadcastingToLan(true); + startGamePacket.getGamerules().add(new GameRuleData<>("showcoordinates", true)); + startGamePacket.setPlatformBroadcastMode(GamePublishSetting.PUBLIC); + startGamePacket.setXblBroadcastMode(GamePublishSetting.PUBLIC); + startGamePacket.setCommandsEnabled(true); + startGamePacket.setTexturePacksRequired(false); + startGamePacket.setBonusChestEnabled(false); + startGamePacket.setStartingWithMap(false); + startGamePacket.setTrustingPlayers(true); + startGamePacket.setDefaultPlayerPermission(PlayerPermission.VISITOR); + startGamePacket.setServerChunkTickRange(4); + startGamePacket.setBehaviorPackLocked(false); + startGamePacket.setResourcePackLocked(false); + startGamePacket.setFromLockedWorldTemplate(false); + startGamePacket.setUsingMsaGamertagsOnly(false); + startGamePacket.setFromWorldTemplate(false); + startGamePacket.setWorldTemplateOptionLocked(false); + + startGamePacket.setLevelId(""); + startGamePacket.setWorldName("GeyserMulti"); + startGamePacket.setPremiumWorldTemplateId(""); + startGamePacket.setCurrentTick(0); + startGamePacket.setEnchantmentSeed(0); + startGamePacket.setMultiplayerCorrelationId(""); + startGamePacket.setBlockPalette(PalleteManger.BLOCK_PALLETE); + //startGamePacket.setItemEntries(ItemRegistry.ITEMS); + startGamePacket.setVanillaVersion("*"); + // startGamePacket.setMovementServerAuthoritative(true); + session.sendPacket(startGamePacket); + + // Send an empty chunk + LevelChunkPacket data = new LevelChunkPacket(); + data.setChunkX(0); + data.setChunkZ(0); + data.setSubChunksLength(0); + data.setData(PalleteManger.EMPTY_LEVEL_CHUNK_DATA); + data.setCachingEnabled(false); + session.sendPacket(data); + + // Send the biomes + BiomeDefinitionListPacket biomeDefinitionListPacket = new BiomeDefinitionListPacket(); + biomeDefinitionListPacket.setTag(PalleteManger.BIOMES_PALLETE); + session.sendPacket(biomeDefinitionListPacket); + + // Let the client know the player can spawn + PlayStatusPacket playStatusPacket = new PlayStatusPacket(); + playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); + session.sendPacket(playStatusPacket); + + // Freeze the player + SetEntityMotionPacket setEntityMotionPacket = new SetEntityMotionPacket(); + setEntityMotionPacket.setRuntimeEntityId(1); + setEntityMotionPacket.setMotion(Vector3f.ZERO); + session.sendPacket(setEntityMotionPacket); + } + + public void sendServerList() { + session.sendPacket(UIHandler.getServerListFormPacket(servers)); + } +} diff --git a/src/main/java/org/geysermc/multi/Server.java b/src/main/java/org/geysermc/multi/Server.java new file mode 100644 index 0000000..e317562 --- /dev/null +++ b/src/main/java/org/geysermc/multi/Server.java @@ -0,0 +1,16 @@ +package org.geysermc.multi; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@AllArgsConstructor +public class Server { + private String address; + private int port; + + public Server(String address) { + this(address, 25565); + } +} diff --git a/src/main/java/org/geysermc/multi/UI/FormID.java b/src/main/java/org/geysermc/multi/UI/FormID.java new file mode 100644 index 0000000..6e5fff6 --- /dev/null +++ b/src/main/java/org/geysermc/multi/UI/FormID.java @@ -0,0 +1,9 @@ +package org.geysermc.multi.UI; + +public enum FormID { + MAIN, + DIRECT_CONNECT, + ADD_SERVER, + REMOVE_SERVER, + ERROR; +} \ No newline at end of file diff --git a/src/main/java/org/geysermc/multi/UI/UIHandler.java b/src/main/java/org/geysermc/multi/UI/UIHandler.java new file mode 100644 index 0000000..c38c775 --- /dev/null +++ b/src/main/java/org/geysermc/multi/UI/UIHandler.java @@ -0,0 +1,29 @@ +package org.geysermc.multi.UI; + +import com.nukkitx.protocol.bedrock.packet.ModalFormRequestPacket; +import org.geysermc.common.window.FormWindow; +import org.geysermc.common.window.SimpleFormWindow; +import org.geysermc.common.window.button.FormButton; +import org.geysermc.common.window.button.FormImage; +import org.geysermc.multi.Server; + +import java.util.List; + +public class UIHandler { + public static ModalFormRequestPacket getServerListFormPacket(List servers) { + SimpleFormWindow window = new SimpleFormWindow("Servers", ""); + + for (Server server : servers) { + window.getButtons().add(new FormButton(server.getAddress(), new FormImage(FormImage.FormImageType.URL, "https://eu.mc-api.net/v3/server/favicon/" + server.getAddress() + ":" + server.getPort() + ".png"))); + } + + return generatePacket(FormID.MAIN, window); + } + + private static ModalFormRequestPacket generatePacket(FormID id, FormWindow form) { + ModalFormRequestPacket modalFormRequestPacket = new ModalFormRequestPacket(); + modalFormRequestPacket.setFormId(id.ordinal()); + modalFormRequestPacket.setFormData(form.getJSONData()); // This fixes a bug in Geyser + return modalFormRequestPacket; + } +} diff --git a/src/main/resources/biome_definitions.dat b/src/main/resources/biome_definitions.dat new file mode 100644 index 0000000..b8c6df4 Binary files /dev/null and b/src/main/resources/biome_definitions.dat differ diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..9574154 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/runtime_block_states.dat b/src/main/resources/runtime_block_states.dat new file mode 100644 index 0000000..2732f6a Binary files /dev/null and b/src/main/resources/runtime_block_states.dat differ