diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index ab9bc74c3b1..8bd0270d105 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -505,167 +505,9 @@ private void startInstance() { } } - MetricsPlatform metricsPlatform = bootstrap.createMetricsPlatform(); - if (metricsPlatform != null && metricsPlatform.enabled()) { - metrics = new MetricsBase( - "server-implementation", - metricsPlatform.serverUuid(), - Constants.BSTATS_ID, - true, // Already checked above. - builder -> { - // OS specific data - String osName = System.getProperty("os.name"); - String osArch = System.getProperty("os.arch"); - String osVersion = System.getProperty("os.version"); - int coreCount = Runtime.getRuntime().availableProcessors(); - - builder.appendField("osName", osName); - builder.appendField("osArch", osArch); - builder.appendField("osVersion", osVersion); - builder.appendField("coreCount", coreCount); - }, - builder -> {}, - null, - () -> true, - logger::error, - logger::info, - metricsPlatform.logFailedRequests(), - metricsPlatform.logSentData(), - metricsPlatform.logResponseStatusText(), - metricsPlatform.disableRelocateCheck() - ); - metrics.addCustomChart(new SingleLineChart("players", sessionManager::size)); - // Prevent unwanted words best we can - metrics.addCustomChart(new SimplePie("authMode", () -> config.java().authType().toString().toLowerCase(Locale.ROOT))); - - Map> platformTypeMap = new HashMap<>(); - Map serverPlatform = new HashMap<>(); - serverPlatform.put(bootstrap.getServerPlatform(), 1); - platformTypeMap.put(platformType().platformName(), serverPlatform); - - metrics.addCustomChart(new DrilldownPie("platform", () -> { - // By the end, we should return, for example: - // Geyser-Spigot => (Paper, 1) - return platformTypeMap; - })); - - metrics.addCustomChart(new SimplePie("defaultLocale", GeyserLocale::getDefaultLocale)); - metrics.addCustomChart(new SimplePie("version", () -> GeyserImpl.VERSION)); - metrics.addCustomChart(new SimplePie("javaHaProxyProtocol", () -> String.valueOf(config.advanced().java().useHaproxyProtocol()))); - metrics.addCustomChart(new SimplePie("bedrockHaProxyProtocol", () -> String.valueOf(config.advanced().bedrock().useHaproxyProtocol()))); - metrics.addCustomChart(new AdvancedPie("playerPlatform", () -> { - Map valueMap = new HashMap<>(); - for (GeyserSession session : sessionManager.getAllSessions()) { - if (session == null) continue; - if (session.getClientData() == null) continue; - String os = session.getClientData().getDeviceOs().toString(); - if (!valueMap.containsKey(os)) { - valueMap.put(os, 1); - } else { - valueMap.put(os, valueMap.get(os) + 1); - } - } - return valueMap; - })); - metrics.addCustomChart(new AdvancedPie("playerVersion", () -> { - Map valueMap = new HashMap<>(); - for (GeyserSession session : sessionManager.getAllSessions()) { - if (session == null) continue; - if (session.getClientData() == null) continue; - String version = session.getClientData().getGameVersion(); - if (!valueMap.containsKey(version)) { - valueMap.put(version, 1); - } else { - valueMap.put(version, valueMap.get(version) + 1); - } - } - return valueMap; - })); - - String minecraftVersion = bootstrap.getMinecraftServerVersion(); - if (minecraftVersion != null) { - Map> versionMap = new HashMap<>(); - Map platformMap = new HashMap<>(); - platformMap.put(bootstrap.getServerPlatform(), 1); - versionMap.put(minecraftVersion, platformMap); - - metrics.addCustomChart(new DrilldownPie("minecraftServerVersion", () -> { - // By the end, we should return, for example: - // 1.16.5 => (Spigot, 1) - return versionMap; - })); - } - - // The following code can be attributed to the PaperMC project - // https://github.com/PaperMC/Paper/blob/master/Spigot-Server-Patches/0005-Paper-Metrics.patch#L614 - metrics.addCustomChart(new DrilldownPie("javaVersion", () -> { - Map> map = new HashMap<>(); - String javaVersion = System.getProperty("java.version"); - Map entry = new HashMap<>(); - entry.put(javaVersion, 1); - - // http://openjdk.java.net/jeps/223 - // Java decided to change their versioning scheme and in doing so modified the - // java.version system property to return $major[.$minor][.$security][-ea], as opposed to - // 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1", - // otherwise, 9+ - String majorVersion = javaVersion.split("\\.")[0]; - String release; - - int indexOf = javaVersion.lastIndexOf('.'); - - if (majorVersion.equals("1")) { - release = "Java " + javaVersion.substring(0, indexOf); - } else { - // of course, it really wouldn't be all that simple if they didn't add a quirk, now - // would it valid strings for the major may potentially include values such as -ea to - // denote a pre release - Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); - if (versionMatcher.find()) { - majorVersion = versionMatcher.group(0); - } - release = "Java " + majorVersion; - } - map.put(release, entry); - return map; - })); - } else { - metrics = null; - } + setupMetrics(config, logger); - if (config.java().authType() == AuthType.ONLINE) { - // May be written/read to on multiple threads from each GeyserSession as well as writing the config - savedAuthChains = new ConcurrentHashMap<>(); - Type type = new TypeToken>() { }.getType(); - - File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); - if (authChainsFile.exists()) { - Map authChainFile = null; - try (FileReader reader = new FileReader(authChainsFile)) { - authChainFile = GSON.fromJson(reader, type); - } catch (IOException e) { - logger.error("Cannot load saved user tokens!", e); - } - if (authChainFile != null) { - List validUsers = config.savedUserLogins(); - boolean doWrite = false; - for (Map.Entry entry : authChainFile.entrySet()) { - String user = entry.getKey(); - if (!validUsers.contains(user)) { - // Perform a write to this file to purge the now-unused name - doWrite = true; - continue; - } - savedAuthChains.put(user, entry.getValue()); - } - if (doWrite) { - scheduleAuthChainsWrite(); - } - } - } - } else { - savedAuthChains = null; - } + loadSavedAuthChains(config, logger); newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); @@ -949,4 +791,176 @@ private void scheduleAuthChainsWrite() { } }); } + + /** + * Initializes bStats metrics collection with platform, player, and version charts. + */ + private void setupMetrics(GeyserConfig config, GeyserLogger logger) { + MetricsPlatform metricsPlatform = bootstrap.createMetricsPlatform(); + if (metricsPlatform != null && metricsPlatform.enabled()) { + metrics = new MetricsBase( + "server-implementation", + metricsPlatform.serverUuid(), + Constants.BSTATS_ID, + true, // Already checked above. + builder -> { + // OS specific data + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + String osVersion = System.getProperty("os.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + builder.appendField("osName", osName); + builder.appendField("osArch", osArch); + builder.appendField("osVersion", osVersion); + builder.appendField("coreCount", coreCount); + }, + builder -> {}, + null, + () -> true, + logger::error, + logger::info, + metricsPlatform.logFailedRequests(), + metricsPlatform.logSentData(), + metricsPlatform.logResponseStatusText(), + metricsPlatform.disableRelocateCheck() + ); + metrics.addCustomChart(new SingleLineChart("players", sessionManager::size)); + // Prevent unwanted words best we can + metrics.addCustomChart(new SimplePie("authMode", () -> config.java().authType().toString().toLowerCase(Locale.ROOT))); + + Map> platformTypeMap = new HashMap<>(); + Map serverPlatform = new HashMap<>(); + serverPlatform.put(bootstrap.getServerPlatform(), 1); + platformTypeMap.put(platformType().platformName(), serverPlatform); + + metrics.addCustomChart(new DrilldownPie("platform", () -> { + // By the end, we should return, for example: + // Geyser-Spigot => (Paper, 1) + return platformTypeMap; + })); + + metrics.addCustomChart(new SimplePie("defaultLocale", GeyserLocale::getDefaultLocale)); + metrics.addCustomChart(new SimplePie("version", () -> GeyserImpl.VERSION)); + metrics.addCustomChart(new SimplePie("javaHaProxyProtocol", () -> String.valueOf(config.advanced().java().useHaproxyProtocol()))); + metrics.addCustomChart(new SimplePie("bedrockHaProxyProtocol", () -> String.valueOf(config.advanced().bedrock().useHaproxyProtocol()))); + metrics.addCustomChart(new AdvancedPie("playerPlatform", () -> { + Map valueMap = new HashMap<>(); + for (GeyserSession session : sessionManager.getAllSessions()) { + if (session == null) continue; + if (session.getClientData() == null) continue; + String os = session.getClientData().getDeviceOs().toString(); + if (!valueMap.containsKey(os)) { + valueMap.put(os, 1); + } else { + valueMap.put(os, valueMap.get(os) + 1); + } + } + return valueMap; + })); + metrics.addCustomChart(new AdvancedPie("playerVersion", () -> { + Map valueMap = new HashMap<>(); + for (GeyserSession session : sessionManager.getAllSessions()) { + if (session == null) continue; + if (session.getClientData() == null) continue; + String version = session.getClientData().getGameVersion(); + if (!valueMap.containsKey(version)) { + valueMap.put(version, 1); + } else { + valueMap.put(version, valueMap.get(version) + 1); + } + } + return valueMap; + })); + + String minecraftVersion = bootstrap.getMinecraftServerVersion(); + if (minecraftVersion != null) { + Map> versionMap = new HashMap<>(); + Map platformMap = new HashMap<>(); + platformMap.put(bootstrap.getServerPlatform(), 1); + versionMap.put(minecraftVersion, platformMap); + + metrics.addCustomChart(new DrilldownPie("minecraftServerVersion", () -> { + // By the end, we should return, for example: + // 1.16.5 => (Spigot, 1) + return versionMap; + })); + } + + // The following code can be attributed to the PaperMC project + // https://github.com/PaperMC/Paper/blob/master/Spigot-Server-Patches/0005-Paper-Metrics.patch#L614 + metrics.addCustomChart(new DrilldownPie("javaVersion", () -> { + Map> map = new HashMap<>(); + String javaVersion = System.getProperty("java.version"); + Map entry = new HashMap<>(); + entry.put(javaVersion, 1); + + // http://openjdk.java.net/jeps/223 + // Java decided to change their versioning scheme and in doing so modified the + // java.version system property to return $major[.$minor][.$security][-ea], as opposed to + // 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1", + // otherwise, 9+ + String majorVersion = javaVersion.split("\\.")[0]; + String release; + + int indexOf = javaVersion.lastIndexOf('.'); + + if (majorVersion.equals("1")) { + release = "Java " + javaVersion.substring(0, indexOf); + } else { + // of course, it really wouldn't be all that simple if they didn't add a quirk, now + // would it valid strings for the major may potentially include values such as -ea to + // denote a pre release + Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); + if (versionMatcher.find()) { + majorVersion = versionMatcher.group(0); + } + release = "Java " + majorVersion; + } + map.put(release, entry); + return map; + })); + } else { + metrics = null; + } + } + + /** + * Loads saved authentication chains from disk for online-mode session resumption. + */ + private void loadSavedAuthChains(GeyserConfig config, GeyserLogger logger) { + if (config.java().authType() == AuthType.ONLINE) { + // May be written/read to on multiple threads from each GeyserSession as well as writing the config + savedAuthChains = new ConcurrentHashMap<>(); + Type type = new TypeToken>() { }.getType(); + + File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); + if (authChainsFile.exists()) { + Map authChainFile = null; + try (FileReader reader = new FileReader(authChainsFile)) { + authChainFile = GSON.fromJson(reader, type); + } catch (IOException e) { + logger.error("Cannot load saved user tokens!", e); + } + if (authChainFile != null) { + List validUsers = config.savedUserLogins(); + boolean doWrite = false; + for (Map.Entry entry : authChainFile.entrySet()) { + String user = entry.getKey(); + if (!validUsers.contains(user)) { + // Perform a write to this file to purge the now-unused name + doWrite = true; + continue; + } + savedAuthChains.put(user, entry.getValue()); + } + if (doWrite) { + scheduleAuthChainsWrite(); + } + } + } + } else { + savedAuthChains = null; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 708d91cf3a4..e495740aa47 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -879,6 +879,15 @@ public void connect() { ChunkUtils.sendEmptyChunks(this, playerEntity.getPosition().toInt(), 0, false); + sendRegistryDefinitions(); + sendInitialPlayerState(); + sendInitialGameRules(); + } + + /** + * Sends biome definitions, entity identifiers, camera presets, and creative content to the client. + */ + private void sendRegistryDefinitions() { BiomeDefinitionListPacket biomeDefinitionListPacket = new BiomeDefinitionListPacket(); biomeDefinitionListPacket.setBiomes(Registries.BIOMES.get()); upstream.sendPacket(biomeDefinitionListPacket); @@ -895,7 +904,12 @@ public void connect() { creativePacket.getContents().addAll(this.itemMappings.getCreativeItems()); creativePacket.getGroups().addAll(this.itemMappings.getCreativeItemGroups()); upstream.sendPacket(creativePacket); + } + /** + * Sends the initial player spawn status, command settings, and default movement attributes. + */ + private void sendInitialPlayerState() { PlayStatusPacket playStatusPacket = new PlayStatusPacket(); playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); upstream.sendPacket(playStatusPacket); @@ -911,7 +925,12 @@ public void connect() { attributesPacket.setAttributes(Collections.singletonList( GeyserAttributeType.MOVEMENT_SPEED.getAttribute())); upstream.sendPacket(attributesPacket); + } + /** + * Sends the initial set of game rules to the Bedrock client during connection setup. + */ + private void sendInitialGameRules() { GameRulesChangedPacket gamerulePacket = new GameRulesChangedPacket(); // Only allow the server to send health information // Setting this to false allows natural regeneration to work false but doesn't break it being true @@ -1791,6 +1810,25 @@ private void startGame() { this.upstream.getCodecHelper().setBlockDefinitions(this.blockMappings); this.upstream.getCodecHelper().setCameraPresetDefinitions(CameraDefinitions.CAMERA_DEFINITIONS); + StartGamePacket startGamePacket = buildStartGamePacket(); + configureExperiments(startGamePacket); + + if (playerEntity.getPropertyManager() != null) { + startGamePacket.setPlayerPropertyData(playerEntity.getPropertyManager().toNbtMap("minecraft:player")); + } + + startGamePacket.setServerId(""); + startGamePacket.setWorldId(""); + startGamePacket.setScenarioId(""); + startGamePacket.setOwnerId(""); + + upstream.sendPacket(startGamePacket); + } + + /** + * Builds and configures the initial StartGamePacket with world settings, player info, and item/block data. + */ + private StartGamePacket buildStartGamePacket() { StartGamePacket startGamePacket = new StartGamePacket(); startGamePacket.setUniqueEntityId(playerEntity.geyserId()); startGamePacket.setRuntimeEntityId(playerEntity.geyserId()); @@ -1846,14 +1884,6 @@ private void startGame() { // Needed for custom block mappings and custom skulls system startGamePacket.getBlockProperties().addAll(this.blockMappings.getBlockProperties()); - // See https://learn.microsoft.com/en-us/minecraft/creator/documents/experimentalfeaturestoggle for info on each experiment - // data_driven_items (Holiday Creator Features) is needed for blocks and items - startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true)); - // Needed for block properties for states - startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true)); - // Needed for certain molang queries used in blocks and items - startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true)); - startGamePacket.setVanillaVersion("*"); startGamePacket.setInventoriesServerAuthoritative(true); startGamePacket.setServerEngine(""); // Do we want to fill this in? @@ -1870,16 +1900,20 @@ private void startGame() { // It does *not* mean we can dictate the break speed server-sided :( startGamePacket.setServerAuthoritativeBlockBreaking(true); - if (playerEntity.getPropertyManager() != null) { - startGamePacket.setPlayerPropertyData(playerEntity.getPropertyManager().toNbtMap("minecraft:player")); - } - - startGamePacket.setServerId(""); - startGamePacket.setWorldId(""); - startGamePacket.setScenarioId(""); - startGamePacket.setOwnerId(""); + return startGamePacket; + } - upstream.sendPacket(startGamePacket); + /** + * Adds required experimental feature toggles to the start game packet. + */ + private void configureExperiments(StartGamePacket startGamePacket) { + // See https://learn.microsoft.com/en-us/minecraft/creator/documents/experimentalfeaturestoggle for info on each experiment + // data_driven_items (Holiday Creator Features) is needed for blocks and items + startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true)); + // Needed for block properties for states + startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true)); + // Needed for certain molang queries used in blocks and items + startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true)); } private void syncEntityProperties() { @@ -2131,28 +2165,7 @@ public void sendAdventureSettings() { } if (spectator) { - AbilityLayer spectatorLayer = new AbilityLayer(); - spectatorLayer.setLayerType(AbilityLayer.Type.SPECTATOR); - // Setting all abilitySet causes the zoom issue... BDS only sends these, so ig we will too - Set abilitySet = spectatorLayer.getAbilitiesSet(); - abilitySet.add(Ability.BUILD); - abilitySet.add(Ability.MINE); - abilitySet.add(Ability.DOORS_AND_SWITCHES); - abilitySet.add(Ability.OPEN_CONTAINERS); - abilitySet.add(Ability.ATTACK_PLAYERS); - abilitySet.add(Ability.ATTACK_MOBS); - abilitySet.add(Ability.INVULNERABLE); - abilitySet.add(Ability.FLYING); - abilitySet.add(Ability.MAY_FLY); - abilitySet.add(Ability.INSTABUILD); - abilitySet.add(Ability.NO_CLIP); - - Set abilityValues = spectatorLayer.getAbilityValues(); - abilityValues.add(Ability.INVULNERABLE); - abilityValues.add(Ability.FLYING); - abilityValues.add(Ability.NO_CLIP); - - updateAbilitiesPacket.getAbilityLayers().add(spectatorLayer); + updateAbilitiesPacket.getAbilityLayers().add(buildSpectatorAbilityLayer()); } abilityLayer.setLayerType(AbilityLayer.Type.BASE); @@ -2166,6 +2179,34 @@ public void sendAdventureSettings() { sendUpstreamPacket(updateAbilitiesPacket); } + /** + * Builds the ability layer for spectator mode with the appropriate ability set and values. + */ + private AbilityLayer buildSpectatorAbilityLayer() { + AbilityLayer spectatorLayer = new AbilityLayer(); + spectatorLayer.setLayerType(AbilityLayer.Type.SPECTATOR); + // Setting all abilitySet causes the zoom issue... BDS only sends these, so ig we will too + Set abilitySet = spectatorLayer.getAbilitiesSet(); + abilitySet.add(Ability.BUILD); + abilitySet.add(Ability.MINE); + abilitySet.add(Ability.DOORS_AND_SWITCHES); + abilitySet.add(Ability.OPEN_CONTAINERS); + abilitySet.add(Ability.ATTACK_PLAYERS); + abilitySet.add(Ability.ATTACK_MOBS); + abilitySet.add(Ability.INVULNERABLE); + abilitySet.add(Ability.FLYING); + abilitySet.add(Ability.MAY_FLY); + abilitySet.add(Ability.INSTABUILD); + abilitySet.add(Ability.NO_CLIP); + + Set abilityValues = spectatorLayer.getAbilityValues(); + abilityValues.add(Ability.INVULNERABLE); + abilityValues.add(Ability.FLYING); + abilityValues.add(Ability.NO_CLIP); + + return spectatorLayer; + } + private int getRenderDistance() { if (clientRenderDistance != -1) { // The client has sent a render distance