From 4a3119f730c874a31f03d610cc2b8351dba519d4 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Sun, 14 Jun 2020 02:07:00 +0100 Subject: [PATCH] Initial commit Added base server and a static server list --- .gitignore | 222 ++++++++++++++++++ pom.xml | 79 +++++++ .../java/org/geysermc/multi/GeyserMulti.java | 16 ++ src/main/java/org/geysermc/multi/Logger.java | 75 ++++++ .../java/org/geysermc/multi/MasterServer.java | 97 ++++++++ .../org/geysermc/multi/PacketHandler.java | 154 ++++++++++++ .../org/geysermc/multi/PalleteManger.java | 62 +++++ src/main/java/org/geysermc/multi/Player.java | 120 ++++++++++ src/main/java/org/geysermc/multi/Server.java | 16 ++ .../java/org/geysermc/multi/UI/FormID.java | 9 + .../java/org/geysermc/multi/UI/UIHandler.java | 29 +++ src/main/resources/biome_definitions.dat | Bin 0 -> 27359 bytes src/main/resources/log4j2.xml | 21 ++ src/main/resources/runtime_block_states.dat | Bin 0 -> 302699 bytes 14 files changed, 900 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/org/geysermc/multi/GeyserMulti.java create mode 100644 src/main/java/org/geysermc/multi/Logger.java create mode 100644 src/main/java/org/geysermc/multi/MasterServer.java create mode 100644 src/main/java/org/geysermc/multi/PacketHandler.java create mode 100644 src/main/java/org/geysermc/multi/PalleteManger.java create mode 100644 src/main/java/org/geysermc/multi/Player.java create mode 100644 src/main/java/org/geysermc/multi/Server.java create mode 100644 src/main/java/org/geysermc/multi/UI/FormID.java create mode 100644 src/main/java/org/geysermc/multi/UI/UIHandler.java create mode 100644 src/main/resources/biome_definitions.dat create mode 100644 src/main/resources/log4j2.xml create mode 100644 src/main/resources/runtime_block_states.dat 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 0000000000000000000000000000000000000000..b8c6df4a686d41f62381547c6439d4e53552e616 GIT binary patch literal 27359 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 pOW + + + + + + + + + + + + + + + + + + + + \ 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 0000000000000000000000000000000000000000..2732f6a7e65039a454c3425c1bf9adcd335746ca GIT binary patch literal 302699 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