1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

Switch config system to Configurate (#5010)

* Start implementing Configurate config system

* More development

* Start migrating to Gson only

* Progress

* Update usage of WebUtils

* Most things now use Gson for JSON

* Allow tests to succeed by using new Gson version

* Use slightly cleaner version for Version deserializer

* Work around older Gson versions without record support

* GeyserCustomSkullConfiguration uses Configurate

* Fix regression in properties get

* New config used in core

* The configuration is gone. Long live the config.

* More changes and migrations away from Jackson

* Improve node ordering when updating configs

* typo

* Better check for ignoring non-configurate configs for considering comment moving

* Ensure metrics UUID is valid

* Initial advanced config

* Remove Jackson; finish config value placements

* Remove duplicate relocate declarations

* Let annotations work

* Renaming to PluginSpecific

* Use global bStats config where possible

* Fix test

* Re-introduce asterisk behavior in configs

* Remove GeyserPluginBootstrap as it's no longer necessary

* Remove old config.yml file

* Update Xbox achievement comment

* Apply suggestions from code review

Co-authored-by: chris <github@onechris.mozmail.com>

* No need to remove values anymore

* Fix: disable bstats relocation on platforms where it is not needed

* ensure it builds

* Update custom unavailable slot comment

Co-authored-by: chris <github@onechris.mozmail.com>

* Update cooldown image

* Logger message for direct-compression still being enabled

* oops

* More explicit RuntimeException message

Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com>

* Constant for 'system' locale

* Better config JSON encoding (something is broken with Cloudflare; we'll figure it out

* Fix broadcast port default

* Add this file too

* Update configurate branch

* fix build

* Fix: Allow using custom config file on Standalone, add relocation comment

* Move config loading to GeyserBootstrap interface

* Add and rename some config options, add section notes (#5390)

* Add and rename some config options, add section notes

* adjust message

* Update core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java

Co-authored-by: Eclipse <eclipse@eclipseisoffline.xyz>

* Update core/src/main/java/org/geysermc/geyser/configuration/GeyserConfig.java

Co-authored-by: Eclipse <eclipse@eclipseisoffline.xyz>

* Update ConfigLoader.java

* Update AdvancedConfig.java

* clarify that we're talking about the HAProxy protocol

* rename config option to use-haproxy-protocol

* remove ominous warning sign on xbox auth warning

* adjust wording

---------

Co-authored-by: Eclipse <eclipse@eclipseisoffline.xyz>

* Back to one config file

* Some minor touchups

* Configurate: Sectionification (#5904)

* Init: config sections

* Start on adding tests, move migrations over to ConfigMigrations class

* Get rid of auth section again, rename that one config option, fix mtu migration

* Move custom skulls config options to the bottom of the gameplay settings

* Add more tests

* Rename and migrate proxy-protocol-whitelisted-ips to haproxy-protocol-whitelisted-ips

* Add automatic downloading of the GeyserOptionalPack

* Revert "Add automatic downloading of the GeyserOptionalPack"

This reverts commit 65b96208fb.

* Add more invalid config tests

* Warn about emote-offhand-workaround removal

* Add automatic loading of the GeyserOptionalPack (feature/configurate) (#5964)

* Add automatic downloading of the GeyserOptionalPack

* Warn about including the OptionalPack from extensions when Geyser is already including it instead of throwing.

* Copy optional pack instead of downloading

---------

Co-authored-by: onebeastchris <github@onechris.mozmail.com>

* Remove unused variable

* Start warning users not running Java 21

* Update tests, temporarily remove NumericRanges test

* Remove duplicate advanced section from Geyser dump

* Address some "reviews"

* yeet md5 hash from geyser dump

* Add info about number of resource packs / amount of mappings into Geyser dump

* Re-enable invalid config loading test

* Fix: allow-custom-skulls migration

* Fix test

* Add "enable-emotes" configuration option

* Rename "emotes-enabled" to "show-emotes"

* Only enable integrated pack when optional pack isn't present

* Update integrated pack

* Exclude jackson annotations, remove leftover debug print

* Remove one-time config migration warnings as we don't have access to the logger at that stage

* Throw more detailed descriptive error when loading resource packs from the "packs" folder, add another legacy config test

* Fix NeoForge's fun module conflict

* Re-add warning about moved functionality, fix Geyser-ViaProxy

This reverts commit fbadfa574a.

* oops

* Move GeyserLegacyPingPassthrough to separate thread to avoid Standalone command locking issues

---------

Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com>
Co-authored-by: chris <github@onechris.mozmail.com>
Co-authored-by: Eclipse <eclipse@eclipseisoffline.xyz>
Co-authored-by: Aurora <auroranova8756@gmail.com>
This commit is contained in:
Camotoy
2025-11-18 11:55:12 -05:00
committed by GitHub
parent 48bf0942e9
commit 765128ce42
157 changed files with 8006 additions and 2670 deletions

View File

@@ -14,19 +14,29 @@ dependencies {
api(projects.common)
api(projects.api)
// Jackson JSON and YAML serialization
api(libs.bundles.jackson)
api(libs.yaml) // Used for extensions
annotationProcessor(libs.configurate.`interface`.ap)
api(libs.configurate.`interface`)
implementation(libs.configurate.yaml)
api(libs.guava)
compileOnly(libs.gson.record.factory) {
isTransitive = false
}
// Fastutil Maps
implementation(libs.bundles.fastutil)
// Network libraries
implementation(libs.websocket)
api(libs.bundles.protocol)
api(libs.bundles.protocol) {
exclude("com.fasterxml.jackson.core", "jackson-annotations")
}
api(libs.minecraftauth)
api(libs.minecraftauth) {
exclude("com.google.code.gson", "gson")
}
api(libs.mcprotocollib) {
exclude("io.netty", "netty-all")
exclude("net.raphimc", "MinecraftAuth")
@@ -60,6 +70,7 @@ dependencies {
// Test
testImplementation(libs.junit)
testImplementation(libs.gson.runtime) // Record support
testImplementation(libs.mockito)
// Annotation Processors
@@ -68,6 +79,8 @@ dependencies {
annotationProcessor(projects.ap)
api(libs.events)
api(libs.bstats)
}
tasks.processResources {

View File

@@ -42,6 +42,10 @@ public final class Constants {
public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/";
public static final int CONFIG_VERSION = 5;
public static final int BSTATS_ID = 5273;
static {
URI wsUri = null;
try {

View File

@@ -25,14 +25,14 @@
package org.geysermc.geyser;
import org.geysermc.geyser.configuration.GeyserJacksonConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.text.GeyserLocale;
import java.nio.file.Files;
import java.nio.file.Path;
public class FloodgateKeyLoader {
public static Path getKeyPath(GeyserJacksonConfiguration config, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) {
public static Path getKeyPath(GeyserConfig config, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) {
// Always prioritize Floodgate's key, if it is installed.
// This mostly prevents people from trying to copy the key and corrupting it in the process
if (floodgateDataFolder != null) {
@@ -45,13 +45,7 @@ public class FloodgateKeyLoader {
}
}
Path floodgateKey;
if (config.getFloodgateKeyFile().equals("public-key.pem")) {
logger.debug("Floodgate 2.0 doesn't use a public/private key system anymore. We'll search for key.pem instead");
floodgateKey = geyserDataFolder.resolve("key.pem");
} else {
floodgateKey = geyserDataFolder.resolve(config.getFloodgateKeyFile());
}
Path floodgateKey = geyserDataFolder.resolve(config.advanced().floodgateKeyFile());
if (!Files.exists(floodgateKey)) {
logger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed"));

View File

@@ -27,12 +27,16 @@ package org.geysermc.geyser;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.ConfigLoader;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.level.GeyserWorldManager;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import org.geysermc.geyser.util.metrics.MetricsPlatform;
import org.geysermc.geyser.util.metrics.ProvidedMetricsPlatform;
import java.io.InputStream;
import java.net.SocketAddress;
@@ -68,11 +72,19 @@ public interface GeyserBootstrap {
void onGeyserShutdown();
/**
* Returns the current GeyserConfiguration
* Returns the platform type this Geyser instance is running on.
*
* @return The current GeyserConfiguration
* @return the PlatformType this Geyser instance is running on.
*/
GeyserConfiguration getGeyserConfig();
@NonNull
PlatformType platformType();
/**
* Returns the current GeyserConfig
*
* @return The current GeyserConfig
*/
GeyserConfig config();
/**
* Returns the current GeyserLogger
@@ -194,4 +206,18 @@ public interface GeyserBootstrap {
* Tests if Floodgate is installed, loads the Floodgate key if so, and returns the result of Floodgate installed.
*/
boolean testFloodgatePluginPresent();
/**
* TEMPORARY - will be removed after The Merge:tm:.
*/
Path getFloodgateKeyPath();
@Nullable
default MetricsPlatform createMetricsPlatform() {
return new ProvidedMetricsPlatform();
}
default <T extends GeyserConfig> T loadConfig(Class<T> configClass) {
return new ConfigLoader(this).createFolder().load(configClass);
}
}

View File

@@ -25,10 +25,8 @@
package org.geysermc.geyser;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.netty.channel.epoll.Epoll;
import io.netty.util.NettyRuntime;
import io.netty.util.concurrent.DefaultThreadFactory;
@@ -40,6 +38,11 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.raphimc.minecraftauth.msa.data.MsaConstants;
import net.raphimc.minecraftauth.msa.model.MsaApplicationConfig;
import org.bstats.MetricsBase;
import org.bstats.charts.AdvancedPie;
import org.bstats.charts.DrilldownPie;
import org.bstats.charts.SimplePie;
import org.bstats.charts.SingleLineChart;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -68,7 +71,8 @@ import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.api.util.MinecraftVersion;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.configuration.GeyserPluginConfig;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.erosion.UnixSocketClientListener;
import org.geysermc.geyser.event.GeyserEventBus;
@@ -79,6 +83,7 @@ import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.GeyserServer;
import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.loader.ResourcePackLoader;
@@ -97,15 +102,17 @@ import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.AssetUtils;
import org.geysermc.geyser.util.CodeOfConductManager;
import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.geyser.util.Metrics;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.geyser.util.NewsHandler;
import org.geysermc.geyser.util.VersionCheckUtils;
import org.geysermc.geyser.util.WebUtils;
import org.geysermc.geyser.util.metrics.MetricsPlatform;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
@@ -129,12 +136,7 @@ import java.util.regex.Pattern;
@Getter
public class GeyserImpl implements GeyserApi, EventRegistrar {
public static final ObjectMapper JSON_MAPPER = new ObjectMapper()
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
public static final Gson GSON = JsonUtils.createGson();
public static final String NAME = "Geyser";
public static final String GIT_VERSION = BuildData.GIT_VERSION;
@@ -167,13 +169,12 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
private ScheduledExecutorService scheduledThread;
private GeyserServer geyserServer;
private final PlatformType platformType;
private final GeyserBootstrap bootstrap;
private final GeyserEventBus eventBus;
private final GeyserExtensionManager extensionManager;
private Metrics metrics;
private MetricsBase metrics;
private PendingMicrosoftAuthentication pendingMicrosoftAuthentication;
@Getter(AccessLevel.NONE)
@@ -194,12 +195,11 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
@Setter
private boolean isEnabled;
private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) {
private GeyserImpl(GeyserBootstrap bootstrap) {
instance = this;
Geyser.set(this);
this.platformType = platformType;
this.bootstrap = bootstrap;
/* Initialize event bus */
@@ -218,10 +218,12 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
public void initialize() {
// Setup encryption early so we don't start if we can't auth
try {
EncryptionUtils.getMojangPublicKey();
} catch (Throwable e) {
throw new RuntimeException("Cannot setup authentication! Are you offline? ", e);
if (config().advanced().bedrock().validateBedrockLogin()) {
try {
EncryptionUtils.getMojangPublicKey();
} catch (Throwable t) {
GeyserImpl.getInstance().getLogger().error("Unable to set up encryption! This can be caused by your internet connection or the Minecraft api being unreachable. ", t);
}
}
long startupTime = System.currentTimeMillis();
@@ -278,19 +280,19 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
startInstance();
GeyserConfiguration config = bootstrap.getGeyserConfig();
GeyserConfig config = bootstrap.config();
double completeTime = (System.currentTimeMillis() - startupTime) / 1000D;
String message = GeyserLocale.getLocaleStringLog("geyser.core.finish.done", new DecimalFormat("#.###").format(completeTime));
message += " " + GeyserLocale.getLocaleStringLog("geyser.core.finish.console");
logger.info(message);
if (platformType == PlatformType.STANDALONE) {
if (config.getRemote().authType() != AuthType.FLOODGATE) {
if (platformType() == PlatformType.STANDALONE) {
if (config.java().authType() != AuthType.FLOODGATE) {
// If the auth-type is Floodgate, then this Geyser instance is probably owned by the Java server
logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn"));
}
} else if (config.getRemote().authType() == AuthType.FLOODGATE) {
} else if (config.java().authType() == AuthType.FLOODGATE) {
VersionCheckUtils.checkForOutdatedFloodgate(logger);
}
@@ -308,7 +310,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
GeyserLogger logger = bootstrap.getGeyserLogger();
GeyserConfiguration config = bootstrap.getGeyserConfig();
GeyserConfig config = bootstrap.config();
ScoreboardUpdater.init();
@@ -316,6 +318,23 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
Registries.RESOURCE_PACKS.load();
// Warnings to users who enable options that they might not need.
if (config.advanced().bedrock().useHaproxyProtocol()) {
logger.warning("Geyser is configured to expect HAProxy protocol for incoming Bedrock connections.");
logger.warning("If you do not know what this is, open the Geyser config, and set \"use-haproxy-protocol\" under the \"advanced/bedrock\" section to \"false\".");
}
if (config.advanced().java().useHaproxyProtocol()) {
logger.warning("Geyser is configured to use proxy protocol when connecting to the Java server.");
logger.warning("If you do not know what this is, open the Geyser config, and set \"use-haproxy-protocol\" under the \"advanced/java\" section to \"false\".");
}
if (!config.advanced().bedrock().validateBedrockLogin()) {
logger.error("XBOX AUTHENTICATION IS DISABLED ON THIS GEYSER INSTANCE!");
logger.error("While this allows using Bedrock edition proxies, it also opens up the ability for hackers to connect with any username they choose.");
logger.error("To change this, set \"disable-xbox-auth\" to \"false\" in Geyser's config file.");
}
String geyserUdpPort = System.getProperty("geyserUdpPort", "");
String pluginUdpPort = geyserUdpPort.isEmpty() ? System.getProperty("pluginUdpPort", "") : geyserUdpPort;
if ("-1".equals(pluginUdpPort)) {
@@ -324,33 +343,30 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
boolean portPropertyApplied = false;
String pluginUdpAddress = System.getProperty("geyserUdpAddress", System.getProperty("pluginUdpAddress", ""));
if (platformType != PlatformType.STANDALONE) {
if (platformType() != PlatformType.STANDALONE) {
int javaPort = bootstrap.getServerPort();
if (config.getRemote().address().equals("auto")) {
config.setAutoconfiguredRemote(true);
String serverAddress = bootstrap.getServerBindAddress();
if (!serverAddress.isEmpty() && !"0.0.0.0".equals(serverAddress)) {
config.getRemote().setAddress(serverAddress);
} else {
// Set the remote address to localhost since that is where we are always connecting
try {
config.getRemote().setAddress(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException ex) {
logger.debug("Unknown host when trying to find localhost.");
if (config.isDebugMode()) {
ex.printStackTrace();
}
config.getRemote().setAddress(InetAddress.getLoopbackAddress().getHostAddress());
String serverAddress = bootstrap.getServerBindAddress();
if (!serverAddress.isEmpty() && !"0.0.0.0".equals(serverAddress)) {
config.java().address(serverAddress);
} else {
// Set the remote address to localhost since that is where we are always connecting
try {
config.java().address(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException ex) {
logger.debug("Unknown host when trying to find localhost.");
if (config.debugMode()) {
ex.printStackTrace();
}
config.java().address(InetAddress.getLoopbackAddress().getHostAddress());
}
if (javaPort != -1) {
config.getRemote().setPort(javaPort);
}
}
if (javaPort != -1) {
config.java().port(javaPort);
}
boolean forceMatchServerPort = "server".equals(pluginUdpPort);
if ((config.getBedrock().isCloneRemotePort() || forceMatchServerPort) && javaPort != -1) {
config.getBedrock().setPort(javaPort);
if ((config.bedrock().cloneRemotePort() || forceMatchServerPort) && javaPort != -1) {
config.bedrock().port(javaPort);
if (forceMatchServerPort) {
if (geyserUdpPort.isEmpty()) {
logger.info("Port set from system generic property to match Java server.");
@@ -364,15 +380,15 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
if ("server".equals(pluginUdpAddress)) {
String address = bootstrap.getServerBindAddress();
if (!address.isEmpty()) {
config.getBedrock().setAddress(address);
config.bedrock().address(address);
}
} else if (!pluginUdpAddress.isEmpty()) {
config.getBedrock().setAddress(pluginUdpAddress);
config.bedrock().address(pluginUdpAddress);
}
if (!portPropertyApplied && !pluginUdpPort.isEmpty()) {
int port = Integer.parseInt(pluginUdpPort);
config.getBedrock().setPort(port);
config.bedrock().port(port);
if (geyserUdpPort.isEmpty()) {
logger.info("Port set from generic system property: " + port);
} else {
@@ -380,16 +396,17 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
}
if (platformType != PlatformType.VIAPROXY) {
if (platformType() != PlatformType.VIAPROXY) {
boolean floodgatePresent = bootstrap.testFloodgatePluginPresent();
if (config.getRemote().authType() == AuthType.FLOODGATE && !floodgatePresent) {
if (config.java().authType() == AuthType.FLOODGATE && !floodgatePresent) {
logger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " "
+ GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
return;
} else if (config.isAutoconfiguredRemote() && floodgatePresent) {
} else if (floodgatePresent) {
// Floodgate installed means that the user wants Floodgate authentication
logger.debug("Auto-setting to Floodgate authentication.");
config.getRemote().setAuthType(AuthType.FLOODGATE);
config.java().authType(AuthType.FLOODGATE);
}
}
}
@@ -402,7 +419,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
if (parsedPort < 1 || parsedPort > 65535) {
throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!");
}
config.getBedrock().setBroadcastPort(parsedPort);
config.advanced().bedrock().broadcastPort(parsedPort);
logger.info("Broadcast port set from system property: " + parsedPort);
} catch (NumberFormatException e) {
logger.error(String.format("Invalid broadcast port from system property: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")"));
@@ -410,23 +427,27 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
// It's set to 0 only if no system property or manual config value was set
if (config.getBedrock().broadcastPort() == 0) {
config.getBedrock().setBroadcastPort(config.getBedrock().port());
if (config.advanced().bedrock().broadcastPort() == 0) {
config.advanced().bedrock().broadcastPort(config.bedrock().port());
}
String remoteAddress = config.getRemote().address();
// Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry.
if (!IP_REGEX.matcher(remoteAddress).matches() && !remoteAddress.equalsIgnoreCase("localhost")) {
String[] record = WebUtils.findSrvRecord(this, remoteAddress);
if (record != null) {
int remotePort = Integer.parseInt(record[2]);
config.getRemote().setAddress(remoteAddress = record[3]);
config.getRemote().setPort(remotePort);
logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\"");
if (!(config instanceof GeyserPluginConfig)) {
String remoteAddress = config.java().address();
// Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry.
if (!IP_REGEX.matcher(remoteAddress).matches() && !remoteAddress.equalsIgnoreCase("localhost")) {
String[] record = WebUtils.findSrvRecord(this, remoteAddress);
if (record != null) {
int remotePort = Integer.parseInt(record[2]);
config.java().address(remoteAddress = record[3]);
config.java().port(remotePort);
logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\"");
}
}
} else if (!config.advanced().java().useDirectConnection()) {
logger.warning("The use-direct-connection config option is deprecated. Please reach out to us on Discord if there's a reason it needs to be disabled.");
}
pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout());
pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.pendingAuthenticationTimeout());
this.newsHandler = new NewsHandler(BRANCH, this.buildNumber());
@@ -438,20 +459,19 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
logger.debug("Epoll is not available; Erosion's Unix socket handling will not work.");
}
CooldownUtils.setDefaultShowCooldown(config.getShowCooldown());
BedrockDimension.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
BedrockDimension.changeBedrockNetherId(config.gameplay().netherRoofWorkaround()); // Apply End dimension ID workaround to Nether
Integer bedrockThreadCount = Integer.getInteger("Geyser.BedrockNetworkThreads");
if (bedrockThreadCount == null) {
int bedrockThreadCount = Integer.getInteger("Geyser.BedrockNetworkThreads", -1);
if (bedrockThreadCount == -1) {
// Copy the code from Netty's default thread count fallback
bedrockThreadCount = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
}
this.geyserServer = new GeyserServer(this, bedrockThreadCount);
this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port()))
this.geyserServer.bind(new InetSocketAddress(config.bedrock().address(), config.bedrock().port()))
.whenComplete((avoid, throwable) -> {
String address = config.getBedrock().address();
String port = String.valueOf(config.getBedrock().port()); // otherwise we get commas
String address = config.bedrock().address();
String port = String.valueOf(config.bedrock().port()); // otherwise we get commas
if (throwable == null) {
if ("0.0.0.0".equals(address)) {
@@ -469,9 +489,9 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
}).join();
if (config.getRemote().authType() == AuthType.FLOODGATE) {
if (config.java().authType() == AuthType.FLOODGATE) {
try {
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
Key key = new AesKeyProducer().produceFrom(bootstrap.getFloodgateKeyPath());
cipher = new AesCipher(new Base64Topping());
cipher.init(key);
logger.debug("Loaded Floodgate key!");
@@ -483,28 +503,55 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
}
if (config.getMetrics().isEnabled()) {
metrics = new Metrics(this, "GeyserMC", config.getMetrics().getUniqueId(), false, java.util.logging.Logger.getLogger(""));
metrics.addCustomChart(new Metrics.SingleLineChart("players", sessionManager::size));
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 Metrics.SimplePie("authMode", () -> config.getRemote().authType().toString().toLowerCase(Locale.ROOT)));
metrics.addCustomChart(new SimplePie("authMode", () -> config.java().authType().toString().toLowerCase(Locale.ROOT)));
Map<String, Map<String, Integer>> platformTypeMap = new HashMap<>();
Map<String, Integer> serverPlatform = new HashMap<>();
serverPlatform.put(bootstrap.getServerPlatform(), 1);
platformTypeMap.put(platformType().platformName(), serverPlatform);
metrics.addCustomChart(new Metrics.DrilldownPie("platform", () -> {
metrics.addCustomChart(new DrilldownPie("platform", () -> {
// By the end, we should return, for example:
// Geyser-Spigot => (Paper, 1)
return platformTypeMap;
}));
metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", GeyserLocale::getDefaultLocale));
metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserImpl.VERSION));
metrics.addCustomChart(new Metrics.SimplePie("javaHaProxyProtocol", () -> String.valueOf(config.getRemote().isUseProxyProtocol())));
metrics.addCustomChart(new Metrics.SimplePie("bedrockHaProxyProtocol", () -> String.valueOf(config.getBedrock().isEnableProxyProtocol())));
metrics.addCustomChart(new Metrics.AdvancedPie("playerPlatform", () -> {
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<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : sessionManager.getAllSessions()) {
if (session == null) continue;
@@ -518,7 +565,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
return valueMap;
}));
metrics.addCustomChart(new Metrics.AdvancedPie("playerVersion", () -> {
metrics.addCustomChart(new AdvancedPie("playerVersion", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : sessionManager.getAllSessions()) {
if (session == null) continue;
@@ -540,7 +587,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
platformMap.put(bootstrap.getServerPlatform(), 1);
versionMap.put(minecraftVersion, platformMap);
metrics.addCustomChart(new Metrics.DrilldownPie("minecraftServerVersion", () -> {
metrics.addCustomChart(new DrilldownPie("minecraftServerVersion", () -> {
// By the end, we should return, for example:
// 1.16.5 => (Spigot, 1)
return versionMap;
@@ -549,7 +596,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
// 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 Metrics.DrilldownPie("javaVersion", () -> {
metrics.addCustomChart(new DrilldownPie("javaVersion", () -> {
Map<String, Map<String, Integer>> map = new HashMap<>();
String javaVersion = System.getProperty("java.version");
Map<String, Integer> entry = new HashMap<>();
@@ -584,22 +631,21 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
metrics = null;
}
if (config.getRemote().authType() == AuthType.ONLINE) {
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<Map<String, String>>() { }.getType();
File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile();
if (authChainsFile.exists()) {
TypeReference<Map<String, String>> type = new TypeReference<>() { };
Map<String, String> authChainFile = null;
try {
authChainFile = JSON_MAPPER.readValue(authChainsFile, type);
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<String> validUsers = config.getSavedUserLogins();
List<String> validUsers = config.savedUserLogins();
boolean doWrite = false;
for (Map.Entry<String, String> entry : authChainFile.entrySet()) {
String user = entry.getKey();
@@ -627,7 +673,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
this.eventBus.fire(new GeyserPostInitializeEvent(this.extensionManager, this.eventBus));
}
if (config.isNotifyOnNewBedrockUpdate()) {
if (config.notifyOnNewBedrockUpdate()) {
VersionCheckUtils.checkForGeyserUpdate(this::getLogger);
}
}
@@ -703,6 +749,10 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
runIfNonNull(newsHandler, NewsHandler::shutdown);
runIfNonNull(erosionUnixListener, UnixSocketClientListener::close);
if (bootstrap.getGeyserPingPassthrough() instanceof GeyserLegacyPingPassthrough legacyPingPassthrough) {
legacyPingPassthrough.interrupt();
}
ResourcePackLoader.clear();
CodeOfConductManager.getInstance().save();
@@ -777,13 +827,13 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
@NonNull
public RemoteServer defaultRemoteServer() {
return getConfig().getRemote();
return config().java();
}
@Override
@NonNull
public BedrockListener bedrockListener() {
return getConfig().getBedrock();
return config().bedrock();
}
@Override
@@ -801,7 +851,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
@Override
@NonNull
public PlatformType platformType() {
return platformType;
return bootstrap.platformType();
}
@Override
@@ -828,9 +878,9 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
return Integer.parseInt(BUILD_NUMBER);
}
public static GeyserImpl load(PlatformType platformType, GeyserBootstrap bootstrap) {
public static GeyserImpl load(GeyserBootstrap bootstrap) {
if (instance == null) {
return new GeyserImpl(platformType, bootstrap);
return new GeyserImpl(bootstrap);
}
return instance;
@@ -853,8 +903,8 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
return bootstrap.getGeyserLogger();
}
public GeyserConfiguration getConfig() {
return bootstrap.getGeyserConfig();
public GeyserConfig config() {
return bootstrap.config();
}
public WorldManager getWorldManager() {
@@ -867,7 +917,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
}
public void saveAuthChain(@NonNull String bedrockName, @NonNull String authChain) {
if (!getConfig().getSavedUserLogins().contains(bedrockName)) {
if (!config().savedUserLogins().contains(bedrockName)) {
// Do not save this login
return;
}
@@ -889,11 +939,9 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
scheduledThread.execute(() -> {
// Ensure all writes are handled on the same thread
File savedAuthChains = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile();
TypeReference<Map<String, String>> type = new TypeReference<>() { };
Type type = new TypeToken<Map<String, String>>() { }.getType();
try (FileWriter writer = new FileWriter(savedAuthChains)) {
JSON_MAPPER.writerFor(type)
.withDefaultPrettyPrinter()
.writeValue(writer, this.savedAuthChains);
GSON.toJson(this.savedAuthChains, type, writer);
} catch (IOException e) {
getLogger().error("Unable to write saved refresh tokens!", e);
}

View File

@@ -101,7 +101,10 @@ public interface GeyserLogger extends GeyserCommandSource {
* @param object the object to log
*/
default void debug(@Nullable Object object) {
debug(String.valueOf(object));
if (isDebug()) {
// Don't create String object by default
info(String.valueOf(object));
}
}
/**

View File

@@ -99,8 +99,8 @@ public class CommandRegistry implements EventRegistrar {
private static final String GEYSER_ROOT_PERMISSION = "geyser.command";
public final static boolean STANDALONE_COMMAND_MANAGER = GeyserImpl.getInstance().getPlatformType() == PlatformType.STANDALONE ||
GeyserImpl.getInstance().getPlatformType() == PlatformType.VIAPROXY;
public final static boolean STANDALONE_COMMAND_MANAGER = GeyserImpl.getInstance().platformType() == PlatformType.STANDALONE ||
GeyserImpl.getInstance().platformType() == PlatformType.VIAPROXY;
protected final GeyserImpl geyser;
private final CommandManager<GeyserCommandSource> cloud;
@@ -171,7 +171,7 @@ public class CommandRegistry implements EventRegistrar {
registerBuiltInCommand(new CustomOptionsCommand("options", "geyser.commands.options.desc", "geyser.command.options"));
registerBuiltInCommand(new QuickActionsCommand("quickactions", "geyser.commands.quickactions.desc", "geyser.command.quickactions"));
if (this.geyser.getPlatformType() == PlatformType.STANDALONE) {
if (this.geyser.platformType() == PlatformType.STANDALONE) {
registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
}

View File

@@ -25,12 +25,13 @@
package org.geysermc.geyser.command.defaults;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.util.LoopbackUtil;
import org.geysermc.geyser.util.WebUtils;
import org.incendo.cloud.CommandManager;
@@ -79,7 +80,7 @@ public class ConnectionTestCommand extends GeyserCommand {
// Replace "<" and ">" symbols if they are present to avoid the common issue of people including them
final String ip = ipArgument.replace("<", "").replace(">", "");
final int port = portArgument != null ? portArgument : geyser.getConfig().getBedrock().broadcastPort(); // default bedrock port
final int port = portArgument != null ? portArgument : geyser.config().advanced().bedrock().broadcastPort(); // default bedrock port
// Issue: people commonly checking placeholders
if (ip.equals("ip")) {
@@ -105,42 +106,42 @@ public class ConnectionTestCommand extends GeyserCommand {
return;
}
GeyserConfiguration config = geyser.getConfig();
GeyserConfig config = geyser.config();
// Issue: do the ports not line up? We only check this if players don't override the broadcast port - if they do, they (hopefully) know what they're doing
if (config.getBedrock().broadcastPort() == config.getBedrock().port()) {
if (port != config.getBedrock().port()) {
if (config.advanced().bedrock().broadcastPort() == config.bedrock().port()) {
if (port != config.bedrock().port()) {
if (portArgument != null) {
source.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration ("
+ config.getBedrock().port() + ")");
+ config.bedrock().port() + ")");
source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config.");
if (config.getBedrock().isCloneRemotePort()) {
if (config.bedrock().cloneRemotePort()) {
source.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead.");
}
} else {
source.sendMessage("You did not specify the port to check (add it with \":<port>\"), " +
"and the default port 19132 does not match the port in your Geyser configuration ("
+ config.getBedrock().port() + ")!");
+ config.bedrock().port() + ")!");
source.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`.");
}
}
} else {
if (config.getBedrock().broadcastPort() != port) {
if (config.advanced().bedrock().broadcastPort() != port) {
source.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration ("
+ config.getBedrock().broadcastPort() + "). ");
+ config.advanced().bedrock().broadcastPort() + "). ");
source.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on.");
source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config.");
}
}
// Issue: is the `bedrock` `address` in the config different?
if (!config.getBedrock().address().equals("0.0.0.0")) {
if (!config.bedrock().address().equals("0.0.0.0")) {
source.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional.");
}
// Issue: did someone turn on enable-proxy-protocol, and they didn't mean it?
if (config.getBedrock().isEnableProxyProtocol()) {
source.sendMessage("You have the `enable-proxy-protocol` setting enabled. " +
if (config.advanced().bedrock().useHaproxyProtocol()) {
source.sendMessage("You have the `use-haproxy-protocol` setting enabled. " +
"Unless you're deliberately using additional software that REQUIRES this setting, you may not need it enabled.");
}
@@ -171,7 +172,7 @@ public class ConnectionTestCommand extends GeyserCommand {
CONNECTION_TEST_MOTD = connectionTestMotd;
source.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait...");
JsonNode output;
JsonObject output;
try {
String hostname = URLEncoder.encode(ip, StandardCharsets.UTF_8);
output = WebUtils.getJson("https://checker.geysermc.org/ping?hostname=" + hostname + "&port=" + port);
@@ -179,18 +180,18 @@ public class ConnectionTestCommand extends GeyserCommand {
CONNECTION_TEST_MOTD = null;
}
if (output.get("success").asBoolean()) {
JsonNode cache = output.get("cache");
if (output.get("success").getAsBoolean()) {
JsonObject cache = output.getAsJsonObject("cache");
String when;
if (cache.get("fromCache").asBoolean()) {
when = cache.get("secondsSince").asInt() + " seconds ago";
if (cache.get("fromCache").isJsonPrimitive()) {
when = cache.get("secondsSince").getAsBoolean() + " seconds ago";
} else {
when = "now";
}
JsonNode ping = output.get("ping");
JsonNode pong = ping.get("pong");
String remoteMotd = pong.get("motd").asText();
JsonObject ping = output.getAsJsonObject("ping");
JsonObject pong = ping.getAsJsonObject("pong");
String remoteMotd = pong.get("motd").getAsString();
if (!connectionTestMotd.equals(remoteMotd)) {
source.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " +
"Did you supply the correct IP and port of your server?");
@@ -198,7 +199,7 @@ public class ConnectionTestCommand extends GeyserCommand {
return;
}
if (ping.get("tcpFirst").asBoolean()) {
if (ping.get("tcpFirst").getAsBoolean()) {
source.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information.");
sendLinks(source);
return;
@@ -210,9 +211,9 @@ public class ConnectionTestCommand extends GeyserCommand {
}
source.sendMessage("Your server is likely unreachable from outside the network!");
JsonNode message = output.get("message");
if (message != null && !message.asText().isEmpty()) {
source.sendMessage("Got the error message: " + message.asText());
JsonElement message = output.get("message");
if (message != null && !message.getAsString().isEmpty()) {
source.sendMessage("Got the error message: " + message.getAsString());
}
sendLinks(source);
} catch (Exception e) {

View File

@@ -25,11 +25,14 @@
package org.geysermc.geyser.command.defaults;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
@@ -38,16 +41,12 @@ import org.geysermc.geyser.dump.DumpInfo;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.geyser.util.WebUtils;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.incendo.cloud.parser.standard.StringArrayParser.stringArrayParser;
public class DumpCommand extends GeyserCommand {
@@ -56,7 +55,6 @@ public class DumpCommand extends GeyserCommand {
private static final Iterable<String> SUGGESTIONS = List.of("full", "offline", "logs");
private final GeyserImpl geyser;
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String DUMP_URL = "https://dump.geysermc.org/";
public DumpCommand(GeyserImpl geyser, String name, String description, String permission) {
@@ -111,20 +109,14 @@ public class DumpCommand extends GeyserCommand {
AsteriskSerializer.showSensitive = showSensitive;
Gson gson = new GsonBuilder().setPrettyPrinting().create();
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", source.locale()));
String dumpData;
try {
DumpInfo dump = new DumpInfo(geyser, addLog);
if (offlineDump) {
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
// Make arrays easier to read
prettyPrinter.indentArraysWith(new DefaultIndenter(" ", "\n"));
dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(dump);
} else {
dumpData = MAPPER.writeValueAsString(dump);
}
} catch (IOException e) {
dumpData = gson.toJson(dump);
} catch (Exception e) {
source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", source.locale()));
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.collect_error_short"), e);
return;
@@ -150,11 +142,11 @@ public class DumpCommand extends GeyserCommand {
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", source.locale()));
String response = null;
JsonNode responseNode;
JsonObject responseNode;
try {
response = WebUtils.post(DUMP_URL + "documents", dumpData);
responseNode = MAPPER.readTree(response);
} catch (IOException e) {
responseNode = JsonUtils.parseJson(response);
} catch (Throwable e) {
source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", source.locale()));
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.upload_error_short"), e);
if (e instanceof JsonParseException && response != null) {
@@ -164,11 +156,11 @@ public class DumpCommand extends GeyserCommand {
}
if (!responseNode.has("key")) {
source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response));
source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").getAsString() : response));
return;
}
uploadedDumpUrl = DUMP_URL + responseNode.get("key").asText();
uploadedDumpUrl = DUMP_URL + responseNode.get("key").getAsString();
}
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", source.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl);

View File

@@ -25,7 +25,7 @@
package org.geysermc.geyser.command.defaults;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonObject;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.MinecraftVersion;
import org.geysermc.geyser.api.util.PlatformType;
@@ -77,7 +77,7 @@ public class VersionCommand extends GeyserCommand {
GeyserImpl.NAME, GeyserImpl.VERSION, SUPPORTED_JAVA_RANGE, SUPPORTED_BEDROCK_RANGE));
// Disable update checking in dev mode and for players in Geyser Standalone
if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) {
if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.platformType() == PlatformType.STANDALONE)) {
return;
}
@@ -89,8 +89,8 @@ public class VersionCommand extends GeyserCommand {
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", source.locale()));
try {
int buildNumber = this.geyser.buildNumber();
JsonNode response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest");
int latestBuildNumber = response.get("build").asInt();
JsonObject response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest");
int latestBuildNumber = response.get("build").getAsInt();
if (latestBuildNumber == buildNumber) {
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", source.locale()));

View File

@@ -25,21 +25,18 @@
package org.geysermc.geyser.command.standalone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.Collections;
import java.util.Set;
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final
@ConfigSerializable
@SuppressWarnings("FieldMayBeFinal")
public class PermissionConfiguration {
@JsonProperty("default-permissions")
private Set<String> defaultPermissions = Collections.emptySet();
@JsonProperty("default-denied-permissions")
private Set<String> defaultDeniedPermissions = Collections.emptySet();
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import com.google.common.annotations.VisibleForTesting;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.returnsreceiver.qual.This;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.text.GeyserLocale;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions;
import org.spongepowered.configurate.objectmapping.meta.Processor;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Consumer;
public final class ConfigLoader {
private static final String HEADER = """
--------------------------------
Geyser Configuration File
A bridge between Minecraft: Bedrock Edition and Minecraft: Java Edition.
GitHub: https://github.com/GeyserMC/Geyser
Discord: https://discord.gg/geysermc
Wiki: https://geysermc.org/wiki
NOTICE: See https://geysermc.org/wiki/geyser/setup/ for the setup guide. Many video tutorials are outdated.
In most cases, especially with server hosting providers, further hosting-specific configuration is required.
--------------------------------""";
/**
* Only nullable for testing.
*/
private final @Nullable GeyserBootstrap bootstrap;
private PlatformType platformType;
private @Nullable Consumer<CommentedConfigurationNode> transformer;
private File configFile;
/**
* Only set during testing.
*/
@VisibleForTesting
CommentedConfigurationNode configurationNode;
public ConfigLoader(GeyserBootstrap bootstrap) {
this.bootstrap = bootstrap;
this.platformType = bootstrap.platformType();
configFile = new File(bootstrap.getConfigFolder().toFile(), "config.yml");
}
@VisibleForTesting
ConfigLoader(File file) {
this.bootstrap = null;
configFile = file;
}
/**
* Creates the directory as indicated by {@link GeyserBootstrap#getConfigFolder()}
*/
@This
public ConfigLoader createFolder() {
Path dataFolder = this.bootstrap.getConfigFolder();
if (!dataFolder.toFile().exists()) {
if (!dataFolder.toFile().mkdir()) {
GeyserImpl.getInstance().getLogger().warning("Failed to create config folder: " + dataFolder);
}
}
return this;
}
@This
public ConfigLoader transformer(Consumer<CommentedConfigurationNode> transformer) {
this.transformer = transformer;
return this;
}
@This
public ConfigLoader configFile(File configFile) {
this.configFile = configFile;
return this;
}
/**
* @return null if the config failed to load.
*/
@Nullable
public <T extends GeyserConfig> T load(Class<T> configClass) {
try {
return load0(configClass);
} catch (IOException ex) {
bootstrap.getGeyserLogger().error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
return null;
}
}
private <T extends GeyserConfig> T load0(Class<T> configClass) throws ConfigurateException {
var loader = createLoader(configFile);
CommentedConfigurationNode node = loader.load();
boolean originallyEmpty = !configFile.exists() || node.isNull();
ConfigurationTransformation.Versioned migrations = ConfigMigrations.TRANSFORMER.apply(configClass, bootstrap);
int currentVersion = migrations.version(node);
migrations.apply(node);
int newVersion = migrations.version(node);
T config = node.get(configClass);
// Serialize the instance to ensure strict field ordering. Additionally, if we serialized back
// to the old node, existing nodes would only have their value changed, keeping their position
// at the top of the ordered map, forcing all new nodes to the bottom (regardless of field order).
// For that reason, we must also create a new node.
CommentedConfigurationNode newRoot = CommentedConfigurationNode.root(loader.defaultOptions());
newRoot.set(config);
if (originallyEmpty || currentVersion != newVersion) {
if (!originallyEmpty && currentVersion > 4) {
// Only copy comments over if the file already existed, and we are going to replace it
// Second case: Version 4 is pre-configurate where there were commented out nodes.
// These get treated as comments on lower nodes, which produces very undesirable results.
ConfigurationCommentMover.moveComments(node, newRoot);
}
loader.save(newRoot);
}
if (transformer != null) {
// We transform AFTER saving so that these specific transformations aren't applied to file.
transformer.accept(newRoot);
config = newRoot.get(configClass);
}
if (this.bootstrap != null) { // Null for testing only.
this.bootstrap.getGeyserLogger().setDebug(config.debugMode());
} else {
this.configurationNode = newRoot;
}
return config;
}
@VisibleForTesting
CommentedConfigurationNode loadConfigurationNode(Class<? extends GeyserConfig> configClass, PlatformType platformType) throws ConfigurateException {
this.platformType = platformType;
load0(configClass);
return configurationNode.copy();
}
private YamlConfigurationLoader createLoader(File file) {
return YamlConfigurationLoader.builder()
.file(file)
.indent(2)
.nodeStyle(NodeStyle.BLOCK)
.defaultOptions(options -> InterfaceDefaultOptions.addTo(options, builder -> {
builder.addProcessor(ExcludePlatform.class, excludePlatform(platformType.platformName()))
.addProcessor(PluginSpecific.class, integrationSpecific(platformType != PlatformType.STANDALONE));
})
.shouldCopyDefaults(false) // If we use ConfigurationNode#get(type, default), do not write the default back to the node.
.header(ConfigLoader.HEADER)
.serializers(builder -> builder.register(new LowercaseEnumSerializer())))
.build();
}
private static Processor.Factory<ExcludePlatform, Object> excludePlatform(String thisPlatform) {
return (data, fieldType) -> (value, destination) -> {
for (String platform : data.platforms()) {
if (thisPlatform.equals(platform)) {
//noinspection DataFlowIssue
destination.parent().removeChild(destination.key());
break;
}
}
};
}
private static Processor.Factory<PluginSpecific, Object> integrationSpecific(boolean thisConfigPlugin) {
return (data, fieldType) -> (value, destination) -> {
if (data.forPlugin() != thisConfigPlugin) {
//noinspection DataFlowIssue
destination.parent().removeChild(destination.key());
}
};
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import org.geysermc.geyser.GeyserBootstrap;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import java.util.function.BiFunction;
import static org.spongepowered.configurate.NodePath.path;
import static org.spongepowered.configurate.transformation.TransformAction.remove;
import static org.spongepowered.configurate.transformation.TransformAction.rename;
public class ConfigMigrations {
public static final BiFunction<Class<? extends GeyserConfig>, GeyserBootstrap, ConfigurationTransformation.Versioned> TRANSFORMER = (configClass, bootstrap) ->
ConfigurationTransformation.versionedBuilder()
.versionKey("config-version")
.addVersion(5, ConfigurationTransformation.builder()
// Java section
.addAction(path("remote"), rename("java"))
.addAction(path("remote", "address"), (path, value) -> {
if ("auto".equals(value.getString())) {
// Auto-convert back to localhost
value.set("127.0.0.1");
}
return null;
})
// Motd section
.addAction(path("bedrock", "motd1"), renameAndMove("motd", "primary-motd"))
.addAction(path("bedrock", "motd2"), renameAndMove("motd", "secondary-motd"))
.addAction(path("passthrough-motd"), moveTo("motd"))
.addAction(path("passthrough-player-counts"), moveTo("motd"))
.addAction(path("ping-passthrough-interval"), moveTo("motd"))
.addAction(path("max-players"), moveTo("motd"))
.addAction(path("legacy-ping-passthrough"), configClass == GeyserRemoteConfig.class ? remove() : (path, value) -> {
// Invert value
value.set(!value.getBoolean());
return new Object[]{ "motd", "integrated-ping-passthrough" };
})
// gameplay
.addAction(path("command-suggestions"), moveTo("gameplay"))
.addAction(path("forward-player-ping"), moveTo("gameplay"))
.addAction(path("show-cooldown"), (path, value) -> {
String s = value.getString();
if (s != null) {
switch (s) {
case "true" -> value.set("title");
case "false" -> value.set("disabled");
}
}
return new Object[]{ "gameplay", "show-cooldown" };
})
.addAction(path("bedrock", "server-name"), moveTo("gameplay"))
.addAction(path("show-coordinates"), moveTo("gameplay"))
.addAction(path("disable-bedrock-scaffolding"), moveTo("gameplay"))
.addAction(path("custom-skull-render-distance"), moveTo("gameplay"))
.addAction(path("force-resource-packs"), moveTo("gameplay"))
.addAction(path("xbox-achievements-enabled"), moveTo("gameplay"))
.addAction(path("unusable-space-block"), moveTo("gameplay"))
.addAction(path("unusable-space-block"), moveTo("gameplay"))
.addAction(path("add-non-bedrock-items"), renameAndMove("gameplay", "enable-custom-content"))
.addAction(path("above-bedrock-nether-building"), renameAndMove("gameplay", "nether-roof-workaround"))
.addAction(path("xbox-achievements-enabled"), moveTo("gameplay"))
// NOTE: We're not explicitly removing the allow-custom-skulls option, it will already be removed since it
// won't be written back. If we remove it, we can't query the value of it!
.addAction(path("max-visible-custom-skulls"), (path, value) -> {
ConfigurationNode parent = value.parent();
if (parent != null && parent.isMap()) {
ConfigurationNode allowCustomSkulls = parent.childrenMap().get("allow-custom-skulls");
if (allowCustomSkulls != null && !allowCustomSkulls.getBoolean()) {
value.set(0);
}
}
return new Object[]{ "gameplay", "max-visible-custom-skulls" };
})
.addAction(path("emote-offhand-workaround"), (path, value) -> {
String previous = value.getString();
if (!Objects.equals(previous, "disabled") && bootstrap != null) {
bootstrap.getGeyserLogger().warning("The emote-offhand-workaround option has been removed from Geyser. If you still wish to have this functionality, use this Geyser extension: https://github.com/GeyserMC/EmoteOffhandExtension/");
}
if (Objects.equals(previous, "no-emotes")) {
value.set(false);
return new Object[]{ "gameplay", "show-emotes" };
}
return null;
})
// For the warning!
.addAction(path("allow-third-party-capes"), (node, value) -> {
if (bootstrap != null) {
bootstrap.getGeyserLogger().warning("Third-party ears/capes have been removed from Geyser. If you still wish to have this functionality, use this Geyser extension: https://github.com/GeyserMC/ThirdPartyCosmetics");
}
return null;
})
.addAction(path("allow-third-party-ears"), (node, value) -> {
if (bootstrap != null) {
bootstrap.getGeyserLogger().warning("Third-party ears/capes have been removed from Geyser. If you still wish to have this functionality, use this Geyser extension: https://github.com/GeyserMC/ThirdPartyCosmetics");
}
return null;
})
// Advanced section
.addAction(path("cache-images"), moveTo("advanced"))
.addAction(path("scoreboard-packet-threshold"), moveTo("advanced"))
.addAction(path("add-team-suggestions"), moveTo("advanced"))
.addAction(path("floodgate-key-file"), (path, value) -> {
// Elimate any legacy config values
if ("public-key.pem".equals(value.getString())) {
value.set("key.pem");
}
return new Object[]{ "advanced", "floodgate-key-file" };
})
// Bedrock
.addAction(path("bedrock", "broadcast-port"), moveTo("advanced", "bedrock"))
.addAction(path("bedrock", "compression-level"), moveTo("advanced", "bedrock"))
.addAction(path("bedrock", "enable-proxy-protocol"), renameAndMove("advanced", "bedrock", "use-haproxy-protocol"))
.addAction(path("bedrock", "proxy-protocol-whitelisted-ips"), renameAndMove("advanced", "bedrock", "haproxy-protocol-whitelisted-ips"))
.addAction(path("mtu"), moveTo("advanced", "bedrock"))
// Java
.addAction(path("remote", "use-proxy-protocol"), renameAndMove("advanced", "java", "use-haproxy-protocol"))
.addAction(path("disable-compression"), moveTo("advanced", "java"))
.addAction(path("use-direct-connection"), moveTo("advanced", "java"))
// Other
.addAction(path("default-locale"), (path, value) -> {
if (value.getString() == null) {
value.set("system");
}
return null;
})
.addAction(path("metrics", "uuid"), (path, value) -> {
if ("generateduuid".equals(value.getString())) {
// Manually copied config without Metrics UUID creation?
value.set(UUID.randomUUID());
}
return new Object[]{ "metrics-uuid" };
})
.addAction(path("metrics", "enabled"), (path, value) -> {
// Move to the root, not in the Metrics class.
return new Object[]{ "enable-metrics" };
})
.build())
.build();
static TransformAction renameAndMove(String... newPath) {
return ((path, value) -> Arrays.stream(newPath).toArray());
}
static TransformAction moveTo(String... newPath) {
return (path, value) -> {
Object[] arr = path.array();
if (arr.length == 0) {
throw new ConfigurateException(value, "The root node cannot be renamed!");
} else {
// create a new array with space for newPath segments + the original last segment
Object[] result = new Object[newPath.length + 1];
System.arraycopy(newPath, 0, result, 0, newPath.length);
result[newPath.length] = arr[arr.length - 1];
return result;
}
};
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationVisitor;
/**
* Moves comments from a different node and puts them on this node
*/
public final class ConfigurationCommentMover implements ConfigurationVisitor.Stateless<RuntimeException> {
private final CommentedConfigurationNode otherRoot;
private ConfigurationCommentMover(@NonNull CommentedConfigurationNode otherNode) {
this.otherRoot = otherNode;
}
@Override
public void enterNode(final ConfigurationNode node) {
if (!(node instanceof CommentedConfigurationNode destination)) {
// Should not occur because all nodes in a tree are the same type,
// and our static method below ensures this visitor is only used on CommentedConfigurationNodes
throw new IllegalStateException(node.path() + " is not a CommentedConfigurationNode");
}
// Node with the same path
CommentedConfigurationNode source = otherRoot.node(node.path());
moveSingle(source, destination);
}
private static void moveSingle(@NonNull CommentedConfigurationNode source, @NonNull CommentedConfigurationNode destination) {
// Only transfer the comment, overriding if necessary
String comment = source.comment();
if (comment != null) {
destination.comment(comment);
}
}
/**
* Moves comments from a source node and its children to a destination node and its children (of a different tree), overriding if necessary.
* Comments are only moved to the destination node and its children which exist.
* Comments are only moved to and from nodes with the exact same path.
*
* @param source the source of the comments, which must be the topmost parent of a tree
* @param destination the destination of the comments, any node in a different tree
*/
public static void moveComments(@NonNull CommentedConfigurationNode source, @NonNull CommentedConfigurationNode destination) {
if (source.parent() != null) {
throw new IllegalArgumentException("source is not the base of the tree it is within: " + source.path());
}
if (source.isNull()) {
// It has no value(s), but may still have a comment on it. Don't both traversing the whole destination tree.
moveSingle(source, destination);
} else {
destination.visit(new ConfigurationCommentMover(source));
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -25,26 +25,13 @@
package org.geysermc.geyser.configuration;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.io.IOException;
public enum EmoteOffhandWorkaroundOption {
NO_EMOTES,
EMOTES_AND_OFFHAND,
DISABLED;
public static class Deserializer extends JsonDeserializer<EmoteOffhandWorkaroundOption> {
@Override
public EmoteOffhandWorkaroundOption deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
return switch (value) {
case "no-emotes" -> NO_EMOTES;
case "emotes-and-offhand" -> EMOTES_AND_OFFHAND;
default -> DISABLED;
};
}
}
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcludePlatform {
String[] platforms();
}

View File

@@ -0,0 +1,475 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.network.BedrockListener;
import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.CooldownUtils;
import org.spongepowered.configurate.interfaces.meta.Exclude;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
import org.spongepowered.configurate.interfaces.meta.range.NumericRange;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@ConfigSerializable
public interface GeyserConfig {
@Comment("Network settings for the Bedrock listener")
BedrockConfig bedrock();
@Comment("Network settings for the Java server connection")
JavaConfig java();
@Comment("MOTD settings")
MotdConfig motd();
@Comment("Gameplay options that affect Bedrock players")
GameplayConfig gameplay();
@Comment("The default locale if we don't have the one the client requested. If set to \"system\", the system's language will be used.")
@DefaultString(GeyserLocale.SYSTEM_LOCALE)
@NonNull
String defaultLocale();
@Comment("Whether player IP addresses will be logged by the server.")
@DefaultBoolean(true)
boolean logPlayerIpAddresses();
@Comment("""
For online mode authentication type only.
Stores a list of Bedrock player usernames that should have their Java Edition account saved after login.
This saves a token that can be reused to authenticate the player later. This does not save emails or passwords,
but you should still be cautious when adding to this list and giving others access to this Geyser instance's files.
Removing a name from this list will delete its cached login information on the next Geyser startup.
The file that tokens will be saved in is in the same folder as this config, named "saved-refresh-tokens.json".""")
default List<String> savedUserLogins() {
return List.of("ThisExampleUsernameShouldBeLongEnoughToNeverBeAnXboxUsername",
"ThisOtherExampleUsernameShouldAlsoBeLongEnough");
}
@Comment("""
For online mode authentication type only.
Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account.
User is allowed to disconnect from the server during this period.""")
@DefaultNumeric(120)
int pendingAuthenticationTimeout();
@Comment("""
Whether to alert the console and operators that a new Geyser version is available that supports a Bedrock version
that this Geyser version does not support. It's recommended to keep this option enabled, as many Bedrock platforms
auto-update.""")
@DefaultBoolean(true)
boolean notifyOnNewBedrockUpdate();
@Comment("Advanced configuration options. These usually do not need modifications.")
AdvancedConfig advanced();
@Comment("""
bStats is a stat tracker that is entirely anonymous and tracks only basic information
about Geyser, such as how many people are online, how many servers are using Geyser,
what OS is being used, etc. You can learn more about bStats here: https://bstats.org/.
https://bstats.org/plugin/server-implementation/GeyserMC""")
@DefaultBoolean(true)
@ExcludePlatform(platforms = {"BungeeCord", "Spigot", "Velocity"}) // bStats platform versions used
boolean enableMetrics();
@Comment("The bstats metrics uuid. Do not touch!")
@ExcludePlatform(platforms = {"BungeeCord", "Spigot", "Velocity"}) // bStats platform versions used
default UUID metricsUuid() {
return UUID.randomUUID();
}
@Comment("If debug messages should be sent through console")
boolean debugMode();
@Comment("Do not change!")
@SuppressWarnings("unused")
default int configVersion() {
return Constants.CONFIG_VERSION;
}
@ConfigSerializable
interface BedrockConfig extends BedrockListener {
@Comment("""
The IP address that Geyser will bind on to listen for incoming Bedrock connections.
Generally, you should only change this if you want to limit what IPs can connect to your server.""")
@NonNull
@Override
@DefaultString("0.0.0.0")
@AsteriskSerializer.Asterisk
String address();
@Comment("""
The port that will Geyser will listen on for incoming Bedrock connections.
Since Minecraft: Bedrock Edition uses UDP, this port must allow UDP traffic.""")
@Override
@DefaultNumeric(19132)
@NumericRange(from = 0, to = 65535)
int port();
@Comment("""
Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock.
This option makes the Bedrock port the same as the Java port every time you start the server.""")
@DefaultBoolean
@PluginSpecific
boolean cloneRemotePort();
void address(String address);
void port(int port);
@Exclude
@Override
default int broadcastPort() {
return GeyserImpl.getInstance().config().advanced().bedrock().broadcastPort();
}
@Exclude
@Override
default String primaryMotd() {
return GeyserImpl.getInstance().config().motd().primaryMotd();
}
@Exclude
@Override
default String secondaryMotd() {
return GeyserImpl.getInstance().config().motd().secondaryMotd();
}
@Exclude
@Override
default String serverName() {
return GeyserImpl.getInstance().config().gameplay().serverName();
}
}
@ConfigSerializable
interface JavaConfig extends RemoteServer {
void address(String address);
void port(int port);
@Comment("""
What type of authentication Bedrock players will be checked against when logging into the Java server.
Can be "floodgate" (see https://wiki.geysermc.org/floodgate/), "online", or "offline".""")
@NonNull
@Override
default AuthType authType() {
return AuthType.ONLINE;
}
void authType(AuthType authType);
boolean forwardHostname();
@Override
@Exclude
default String minecraftVersion() {
return GameProtocol.getJavaMinecraftVersion();
}
@Override
@Exclude
default int protocolVersion() {
return GameProtocol.getJavaProtocolVersion();
}
@Override
@Exclude
default boolean resolveSrv() {
return false;
}
}
@ConfigSerializable
interface MotdConfig {
@Comment("""
The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true.
If either of these are empty, the respective string will default to "Geyser\"""")
@DefaultString("Geyser")
String primaryMotd();
@DefaultString("Another Geyser server.")
String secondaryMotd();
@Comment("Whether Geyser should relay the MOTD from the Java server to Bedrock players.")
@DefaultBoolean(true)
boolean passthroughMotd();
@Comment("""
Maximum amount of players that can connect.
This is only visual, and is only applied if passthrough-motd is disabled.""")
@DefaultNumeric(100)
int maxPlayers();
@Comment("Whether to relay the player count and max players from the Java server to Bedrock players.")
@DefaultBoolean(true)
boolean passthroughPlayerCounts();
@Comment("""
Whether to use server API methods to determine the Java server's MOTD and ping passthrough.
There is no need to disable this unless your MOTD or player count does not appear properly.""")
@DefaultBoolean(true)
@PluginSpecific
boolean integratedPingPassthrough();
@Comment("How often to ping the Java server to refresh MOTD and player count, in seconds.")
@DefaultNumeric(3)
int pingPassthroughInterval();
}
@ConfigSerializable
interface GameplayConfig {
@Comment("The server name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.")
@DefaultString("Geyser")
String serverName();
@Comment("""
Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat.
Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below:
https://geysermc.org/img/external/cooldown_indicator.png
This can be disabled by going into Bedrock settings under the accessibility tab and setting "Text Background Opacity" to 0
This setting can be set to "title", "actionbar" or "false\"""")
default CooldownUtils.CooldownType showCooldown() {
return CooldownUtils.CooldownType.TITLE;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@Comment("""
Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands.
Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.""")
@DefaultBoolean(true)
boolean commandSuggestions();
@Comment("Controls if coordinates are shown to players.")
@DefaultBoolean(true)
boolean showCoordinates();
@Comment("Whether Bedrock players are blocked from performing their scaffolding-style bridging.")
boolean disableBedrockScaffolding();
@Comment("""
Bedrock prevents building and displaying blocks above Y127 in the Nether.
This config option works around that by changing the Nether dimension ID to the End ID.
The main downside to this is that the entire Nether will have the same red fog rather than having different fog for each biome.""")
boolean netherRoofWorkaround();
@Comment("""
Whether to show Bedrock Edition emotes to other Bedrock Edition players.
""")
@DefaultBoolean(true)
boolean emotesEnabled();
@Comment("""
Which item to use to mark unavailable slots in a Bedrock player inventory. Examples of this are the 2x2 crafting grid while in creative,
or custom inventory menus with sizes different from the usual 3x9. A barrier block is the default item.
This config option can be set to any Bedrock item identifier. If you want to set this to a custom item, make sure that you specify the item in the following format: "geyser_custom:<mapping-name>"
""")
@DefaultString("minecraft:barrier")
String unusableSpaceBlock();
@Comment("""
Whether to add any items and blocks which normally does not exist in Bedrock Edition.
This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching.
If this is disabled, furnace minecart items will be mapped to hopper minecart items.
Geyser's block, item, and skull mappings systems will also be disabled.
This option requires a restart of Geyser in order to change its setting.""")
@DefaultBoolean(true)
boolean enableCustomContent();
@Comment("""
Force clients to load all resource packs if there are any.
If set to false, it allows the user to connect to the server even if they don't
want to download the resource packs.""")
@DefaultBoolean(true)
boolean forceResourcePacks();
@Comment("""
Whether to automatically serve a resource pack that is required for some Geyser features to all connecting Bedrock players.
If enabled, force-resource-packs will be enabled.""")
@DefaultBoolean(true)
boolean enableIntegratedPack();
@Comment("""
Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate
ping, it may also cause players to time out more easily.""")
boolean forwardPlayerPing();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@Comment("""
Allows Xbox achievements to be unlocked.
If a player types in an unknown command, they will receive a message that states cheats are disabled.
Otherwise, commands work as expected.""")
boolean xboxAchievementsEnabled();
@Comment("""
The maximum number of custom skulls to be displayed per player. Increasing this may decrease performance on weaker devices.
A value of 0 will disable all custom skulls.
Setting this to -1 will cause all custom skulls to be displayed regardless of distance or number.""")
@DefaultNumeric(128)
int maxVisibleCustomSkulls();
@Comment("The radius in blocks around the player in which custom skulls are displayed.")
@DefaultNumeric(32)
int customSkullRenderDistance();
}
@ConfigSerializable
interface AdvancedBedrockConfig {
@Comment("""
The port to broadcast to Bedrock clients with the MOTD that they should use to connect to the server.
A value of 0 will broadcast the port specified above.
DO NOT change this unless Geyser runs on a different port than the one that is used to connect.""")
@DefaultNumeric(0)
@NumericRange(from = 0, to = 65535)
int broadcastPort();
void broadcastPort(int port);
@Comment("""
How much to compress network traffic to the Bedrock client. The higher the number, the more CPU usage used, but
the smaller the bandwidth used. Does not have any effect below -1 or above 9. Set to -1 to disable.""")
@DefaultNumeric(6)
@NumericRange(from = -1, to = 9)
int compressionLevel();
@Comment("""
Whether to expect HAPROXY protocol for connecting Bedrock clients.
This is useful only when you are running a UDP reverse proxy in front of your Geyser instance.
IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!""")
@DefaultBoolean
boolean useHaproxyProtocol();
@Comment("""
A list of allowed HAPROXY protocol speaking proxy IP addresses/subnets. Only effective when "use-proxy-protocol" is enabled, and
should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.).
Keeping this list empty means there is no IP address whitelist.
IP addresses, subnets, and links to plain text files are supported.""")
default List<String> haproxyProtocolWhitelistedIps() {
return Collections.emptyList();
}
@Comment("""
The internet supports a maximum MTU of 1492 but could cause issues with packet fragmentation.
1400 is the default.""")
@DefaultNumeric(1400)
int mtu();
@Comment("""
This option disables the auth step Geyser performs for connecting Bedrock players.
It can be used to allow connections from ProxyPass and WaterdogPE. In these cases, make sure that users
cannot directly connect to this Geyser instance. See https://www.spigotmc.org/wiki/firewall-guide/ for
assistance - and use UDP instead of TCP.
Disabling Bedrock authentication for other use-cases is NOT SUPPORTED, as it allows anyone to spoof usernames, and is therefore a security risk.
All Floodgate functionality (including skin uploading and account linking) will also not work when this option is disabled.""")
@DefaultBoolean(true)
boolean validateBedrockLogin();
}
@ConfigSerializable
interface AdvancedJavaConfig {
@Comment("""
Whether to enable HAPROXY protocol when connecting to the Java server.
This is useful only when:
1) Your Java server supports HAPROXY protocol (it probably doesn't)
2) You run Velocity or BungeeCord with the option enabled in the proxy's main config.
IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!""")
boolean useHaproxyProtocol();
@Comment("""
Whether to connect directly into the Java server without creating a TCP connection.
This should only be disabled if a plugin that interfaces with packets or the network does not work correctly with Geyser.
If enabled, the remote address and port sections are ignored.
If disabled, expect performance decrease and latency increase.
""")
@DefaultBoolean(true)
@PluginSpecific
boolean useDirectConnection();
@Comment("""
Whether Geyser should attempt to disable packet compression (from the Java Server to Geyser) for Bedrock players.
This should be a benefit as there is no need to compress data when Java packets aren't being handled over the network.
This requires use-direct-connection to be true.
""")
@DefaultBoolean(true)
@PluginSpecific
boolean disableCompression();
}
@ConfigSerializable
interface AdvancedConfig {
@Comment("""
Specify how many days player skin images will be cached to disk to save downloading them from the internet.
A value of 0 is disabled. (Default: 0)""")
int cacheImages();
@Comment("""
Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle
a lot of scoreboard packets per second, this can cause serious lag.
This option allows you to specify after how many Scoreboard packets per seconds
the Scoreboard updates will be limited to four updates per second.""")
@DefaultNumeric(20)
int scoreboardPacketThreshold();
@Comment("""
Whether Geyser should send team names in command suggestions.
Disable this if you have a lot of teams used that you don't need as suggestions.""")
@DefaultBoolean(true)
boolean addTeamSuggestions();
@Comment("""
A list of remote resource pack urls to send to the Bedrock client for downloading.
The Bedrock client is very picky about how these are delivered - please see our wiki page for further info: https://geysermc.org/wiki/geyser/packs/
""")
default List<String> resourcePackUrls() {
return Collections.emptyList();
}
// Cannot be type File yet because we may want to hide it in plugin instances.
@Comment("""
Floodgate uses encryption to ensure use from authorized sources.
This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity)
You can ignore this when not using Floodgate.
If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate.""")
@DefaultString("key.pem")
String floodgateKeyFile();
@Comment("Advanced networking options for the Geyser to Java server connection")
AdvancedJavaConfig java();
@Comment("Advanced networking options for Geyser's Bedrock listener")
AdvancedBedrockConfig bedrock();
}
}

View File

@@ -1,191 +0,0 @@
/*
* Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.network.BedrockListener;
import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.network.CIDRMatcher;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.text.GeyserLocale;
import java.nio.file.Path;
import java.util.List;
public interface GeyserConfiguration {
/**
* If the config was originally 'auto' before the values changed
*/
void setAutoconfiguredRemote(boolean autoconfiguredRemote);
// Modify this when you introduce breaking changes into the config
int CURRENT_CONFIG_VERSION = 4;
IBedrockConfiguration getBedrock();
IRemoteConfiguration getRemote();
List<String> getSavedUserLogins();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isCommandSuggestions();
@JsonIgnore
boolean isPassthroughMotd();
@JsonIgnore
boolean isPassthroughPlayerCounts();
@JsonIgnore
boolean isLegacyPingPassthrough();
int getPingPassthroughInterval();
boolean isForwardPlayerPing();
int getMaxPlayers();
boolean isDebugMode();
@Deprecated
boolean isAllowThirdPartyCapes();
@Deprecated
boolean isAllowThirdPartyEars();
String getShowCooldown();
boolean isShowCoordinates();
boolean isDisableBedrockScaffolding();
EmoteOffhandWorkaroundOption getEmoteOffhandWorkaround();
String getDefaultLocale();
Path getFloodgateKeyPath();
boolean isAddNonBedrockItems();
boolean isAboveBedrockNetherBuilding();
boolean isForceResourcePacks();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isXboxAchievementsEnabled();
int getCacheImages();
boolean isAllowCustomSkulls();
int getMaxVisibleCustomSkulls();
int getCustomSkullRenderDistance();
boolean isLogPlayerIpAddresses();
boolean isNotifyOnNewBedrockUpdate();
String getUnusableSpaceBlock();
IMetricsInfo getMetrics();
int getPendingAuthenticationTimeout();
boolean isAutoconfiguredRemote();
interface IBedrockConfiguration extends BedrockListener {
void setAddress(String address);
void setPort(int port);
void setBroadcastPort(int broadcastPort);
boolean isCloneRemotePort();
int getCompressionLevel();
boolean isEnableProxyProtocol();
List<String> getProxyProtocolWhitelistedIPs();
/**
* @return Unmodifiable list of {@link CIDRMatcher}s from {@link #getProxyProtocolWhitelistedIPs()}
*/
List<CIDRMatcher> getWhitelistedIPsMatchers();
}
interface IRemoteConfiguration extends RemoteServer {
void setAddress(String address);
void setPort(int port);
boolean isUseProxyProtocol();
boolean isForwardHost();
default String minecraftVersion() {
return GameProtocol.getJavaMinecraftVersion();
}
default int protocolVersion() {
return GameProtocol.getJavaProtocolVersion();
}
void setAuthType(AuthType authType);
}
interface IMetricsInfo {
boolean isEnabled();
String getUniqueId();
}
int getScoreboardPacketThreshold();
// if u have offline mode enabled pls be safe
boolean isEnableProxyConnections();
int getMtu();
boolean isUseDirectConnection();
boolean isDisableCompression();
int getConfigVersion();
static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) {
if (geyserConfig.getConfigVersion() < CURRENT_CONFIG_VERSION) {
geyserLogger.warning(GeyserLocale.getLocaleStringLog("geyser.bootstrap.config.outdated"));
} else if (geyserConfig.getConfigVersion() > CURRENT_CONFIG_VERSION) {
geyserLogger.warning(GeyserLocale.getLocaleStringLog("geyser.bootstrap.config.too_new"));
}
}
}

View File

@@ -25,26 +25,20 @@
package org.geysermc.geyser.configuration;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@JsonIgnoreProperties(ignoreUnknown = true)
@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final
@ConfigSerializable
public class GeyserCustomSkullConfiguration {
@JsonProperty("player-usernames")
private List<String> playerUsernames;
@JsonProperty("player-uuids")
private List<String> playerUUIDs;
@JsonProperty("player-profiles")
private List<String> playerProfiles;
@JsonProperty("skin-hashes")
private List<String> skinHashes;
public List<String> getPlayerUsernames() {
@@ -62,4 +56,14 @@ public class GeyserCustomSkullConfiguration {
public List<String> getPlayerSkinHashes() {
return Objects.requireNonNullElse(skinHashes, Collections.emptyList());
}
@Override
public String toString() {
return "GeyserCustomSkullConfiguration{" +
"playerUsernames=" + playerUsernames +
", playerUUIDs=" + playerUUIDs +
", playerProfiles=" + playerProfiles +
", skinHashes=" + skinHashes +
'}';
}
}

View File

@@ -1,366 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.network.CIDRMatcher;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.WebUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final
public abstract class GeyserJacksonConfiguration implements GeyserConfiguration {
@Setter
private boolean autoconfiguredRemote = false;
private BedrockConfiguration bedrock = new BedrockConfiguration();
private RemoteConfiguration remote = new RemoteConfiguration();
@JsonProperty("saved-user-logins")
private List<String> savedUserLogins = Collections.emptyList();
@JsonProperty("floodgate-key-file")
private String floodgateKeyFile = "key.pem";
public abstract Path getFloodgateKeyPath();
@JsonProperty("command-suggestions")
private boolean commandSuggestions = true;
@JsonProperty("passthrough-motd")
private boolean isPassthroughMotd = false;
@JsonProperty("passthrough-player-counts")
private boolean isPassthroughPlayerCounts = false;
@JsonProperty("legacy-ping-passthrough")
private boolean isLegacyPingPassthrough = false;
@JsonProperty("ping-passthrough-interval")
private int pingPassthroughInterval = 3;
@JsonProperty("forward-player-ping")
private boolean forwardPlayerPing = false;
@JsonProperty("max-players")
private int maxPlayers = 100;
@JsonProperty("debug-mode")
private boolean debugMode = false;
@JsonProperty("allow-third-party-capes")
private boolean allowThirdPartyCapes = false;
@JsonProperty("show-cooldown")
private String showCooldown = "title";
@JsonProperty("show-coordinates")
private boolean showCoordinates = true;
@JsonProperty("disable-bedrock-scaffolding")
private boolean disableBedrockScaffolding = false;
@JsonDeserialize(using = EmoteOffhandWorkaroundOption.Deserializer.class)
@JsonProperty("emote-offhand-workaround")
private EmoteOffhandWorkaroundOption emoteOffhandWorkaround = EmoteOffhandWorkaroundOption.DISABLED;
@JsonProperty("allow-third-party-ears")
private boolean allowThirdPartyEars = false;
@JsonProperty("default-locale")
private String defaultLocale = null; // is null by default so system language takes priority
@JsonProperty("cache-images")
private int cacheImages = 0;
@JsonProperty("allow-custom-skulls")
private boolean allowCustomSkulls = true;
@JsonProperty("max-visible-custom-skulls")
private int maxVisibleCustomSkulls = 128;
@JsonProperty("custom-skull-render-distance")
private int customSkullRenderDistance = 32;
@JsonProperty("add-non-bedrock-items")
private boolean addNonBedrockItems = true;
@JsonProperty("above-bedrock-nether-building")
private boolean aboveBedrockNetherBuilding = false;
@JsonProperty("force-resource-packs")
private boolean forceResourcePacks = true;
@JsonProperty("xbox-achievements-enabled")
private boolean xboxAchievementsEnabled = false;
@JsonProperty("log-player-ip-addresses")
private boolean logPlayerIpAddresses = true;
@JsonProperty("notify-on-new-bedrock-update")
private boolean notifyOnNewBedrockUpdate = true;
@JsonProperty("unusable-space-block")
private String unusableSpaceBlock = "minecraft:barrier";
private MetricsInfo metrics = new MetricsInfo();
@JsonProperty("pending-authentication-timeout")
private int pendingAuthenticationTimeout = 120;
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BedrockConfiguration implements IBedrockConfiguration {
@AsteriskSerializer.Asterisk(isIp = true)
@JsonProperty("address")
@Setter
private String address = "0.0.0.0";
@Override
public @NonNull String address() {
return address;
}
@Setter
@JsonProperty("port")
private int port = 19132;
@Override
public int port() {
return port;
}
@Setter
@JsonProperty("broadcast-port")
private int broadcastPort = 0;
@Override
public int broadcastPort() {
return broadcastPort;
}
@Getter
@JsonProperty("clone-remote-port")
private boolean cloneRemotePort = false;
@JsonProperty("motd1")
private String motd1 = "GeyserMC";
@Override
public String primaryMotd() {
return motd1;
}
@JsonProperty("motd2")
private String motd2 = "Geyser";
@Override
public String secondaryMotd() {
return motd2;
}
@JsonProperty("server-name")
private String serverName = GeyserImpl.NAME;
@Override
public String serverName() {
return serverName;
}
@JsonProperty("compression-level")
private int compressionLevel = 6;
public int getCompressionLevel() {
return Math.max(-1, Math.min(compressionLevel, 9));
}
@Getter
@JsonProperty("enable-proxy-protocol")
private boolean enableProxyProtocol = false;
@Getter
@JsonProperty("proxy-protocol-whitelisted-ips")
private List<String> proxyProtocolWhitelistedIPs = Collections.emptyList();
@JsonIgnore
private List<CIDRMatcher> whitelistedIPsMatchers = null;
@Override
public List<CIDRMatcher> getWhitelistedIPsMatchers() {
// Effective Java, Third Edition; Item 83: Use lazy initialization judiciously
List<CIDRMatcher> matchers = this.whitelistedIPsMatchers;
if (matchers == null) {
synchronized (this) {
// Check if proxyProtocolWhitelistedIPs contains URLs we need to fetch and parse by line
List<String> whitelistedCIDRs = new ArrayList<>();
for (String ip: proxyProtocolWhitelistedIPs) {
if (!ip.startsWith("http")) {
whitelistedCIDRs.add(ip);
continue;
}
WebUtils.getLineStream(ip).forEach(whitelistedCIDRs::add);
}
this.whitelistedIPsMatchers = matchers = whitelistedCIDRs.stream()
.map(CIDRMatcher::new)
.collect(Collectors.toList());
}
}
return Collections.unmodifiableList(matchers);
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class RemoteConfiguration implements IRemoteConfiguration {
@Setter
@AsteriskSerializer.Asterisk(isIp = true)
@JsonProperty("address")
private String address = "auto";
@Override
public String address() {
return address;
}
@JsonDeserialize(using = PortDeserializer.class)
@Setter
@JsonProperty("port")
private int port = 25565;
@Override
public int port() {
return port;
}
@Setter
@JsonDeserialize(using = AuthTypeDeserializer.class)
@JsonProperty("auth-type")
private AuthType authType = AuthType.ONLINE;
@Override
public @NonNull AuthType authType() {
return authType;
}
@Override
public boolean resolveSrv() {
return false;
}
@Getter
@JsonProperty("use-proxy-protocol")
private boolean useProxyProtocol = false;
@Getter
@JsonProperty("forward-hostname")
private boolean forwardHost = false;
}
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class MetricsInfo implements IMetricsInfo {
private boolean enabled = true;
@JsonDeserialize(using = MetricsIdDeserializer.class)
@JsonProperty("uuid")
private String uniqueId = UUID.randomUUID().toString();
private static class MetricsIdDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String uuid = p.getValueAsString();
if ("generateduuid".equals(uuid)) {
// Compensate for configs not copied from the jar
return UUID.randomUUID().toString();
}
return uuid;
}
}
}
@JsonProperty("scoreboard-packet-threshold")
private int scoreboardPacketThreshold = 10;
@JsonProperty("enable-proxy-connections")
private boolean enableProxyConnections = false;
@JsonProperty("mtu")
private int mtu = 1400;
@JsonProperty("use-direct-connection")
private boolean useDirectConnection = true;
@JsonProperty("disable-compression")
private boolean isDisableCompression = true;
@JsonProperty("config-version")
private int configVersion = 0;
/**
* Ensure that the port deserializes in the config as a number no matter what.
*/
protected static class PortDeserializer extends JsonDeserializer<Integer> {
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
System.err.println(GeyserLocale.getLocaleStringLog("geyser.bootstrap.config.invalid_port"));
return 25565;
}
}
}
public static class AuthTypeDeserializer extends JsonDeserializer<AuthType> {
@Override
public AuthType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return AuthType.getByName(p.getValueAsString());
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import org.spongepowered.configurate.interfaces.meta.Exclude;
import org.spongepowered.configurate.interfaces.meta.Field;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public interface GeyserPluginConfig extends GeyserConfig {
@Override
IntegratedJavaConfig java();
@Override
PluginMotdConfig motd();
@ConfigSerializable
interface PluginMotdConfig extends MotdConfig {
@Comment("""
How often to ping the Java server to refresh MOTD and player count, in seconds.
Only relevant if integrated-ping-passthrough is disabled.""")
@Override
int pingPassthroughInterval();
}
@ConfigSerializable
interface IntegratedJavaConfig extends JavaConfig {
@Override
@Field
String address();
@Override
void address(String address);
@Override
@Field
int port();
@Override
void port(int port);
@Override
@Exclude
default boolean forwardHostname() {
return true; // No need to worry about suspicious behavior flagging the server.
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
import org.spongepowered.configurate.interfaces.meta.range.NumericRange;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
/**
* Used for any instance where the Java server is detached from Geyser.
*/
@ConfigSerializable
public interface GeyserRemoteConfig extends GeyserConfig {
@Override
RemoteConfig java();
@ConfigSerializable
interface RemoteConfig extends JavaConfig {
@Override
@Comment("The IP address of the Java Edition server.")
@DefaultString("127.0.0.1")
@AsteriskSerializer.Asterisk
String address();
@Override
@Comment("The port of the Java Edition server.")
@DefaultNumeric(25565)
@NumericRange(from = 0, to = 65535)
int port();
@Override
@Comment("""
Whether to forward the hostname that the Bedrock client used to connect over to the Java server.
This is designed to be used for forced hosts on proxies.""")
boolean forwardHostname();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import io.leangen.geantyref.TypeToken;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.Scalars;
import org.spongepowered.configurate.serialize.SerializationException;
import java.lang.reflect.Type;
import java.util.Locale;
import java.util.function.Predicate;
/**
* Ensures enum values are written to lowercase. {@link Scalars#ENUM} will read enum values
* in any case.
*/
final class LowercaseEnumSerializer extends ScalarSerializer<Enum<?>> {
LowercaseEnumSerializer() {
super(new TypeToken<Enum<?>>() {});
}
@Override
public Enum<?> deserialize(Type type, Object obj) throws SerializationException {
return Scalars.ENUM.deserialize(type, obj);
}
@Override
protected Object serialize(Enum<?> item, Predicate<Class<?>> typeSupported) {
return item.name().toLowerCase(Locale.ROOT);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Add to a config value to indicate this field is only for plugin versions of Geyser,
* or vice-versa.
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PluginSpecific {
boolean forPlugin() default true;
}

View File

@@ -25,6 +25,7 @@
package org.geysermc.geyser.dump;
import com.google.gson.annotations.JsonAdapter;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
@@ -38,7 +39,7 @@ public class BootstrapDumpInfo {
private final PlatformType platform;
public BootstrapDumpInfo() {
this.platform = GeyserImpl.getInstance().getPlatformType();
this.platform = GeyserImpl.getInstance().platformType();
}
@Getter
@@ -55,7 +56,7 @@ public class BootstrapDumpInfo {
@AllArgsConstructor
public static class ListenerInfo {
@AsteriskSerializer.Asterisk(isIp = true)
@JsonAdapter(value = AsteriskSerializer.class)
public String ip;
public int port;
}

View File

@@ -25,28 +25,39 @@
package org.geysermc.geyser.dump;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.SerializedName;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.floodgate.util.DeviceOs;
import org.geysermc.floodgate.util.FloodgateInfoHolder;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.util.MinecraftVersion;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.pack.ResourcePackHolder;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.geysermc.geyser.util.CpuUtils;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.WebUtils;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions;
import org.spongepowered.configurate.serialize.SerializationException;
import java.io.File;
import java.io.IOException;
@@ -57,12 +68,15 @@ import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
@Getter
public class DumpInfo {
@JsonIgnore
private static final long MEGABYTE = 1024L * 1024L;
private final DumpInfo.VersionInfo versionInfo;
@@ -71,16 +85,17 @@ public class DumpInfo {
private final Locale systemLocale;
private final String systemEncoding;
private final GitInfo gitInfo;
private final GeyserConfiguration config;
private final Floodgate floodgate;
private Object config;
private final Object2IntMap<DeviceOs> userPlatforms;
private final int connectionAttempts;
private final HashInfo hashInfo;
private final String hash;
private final RamInfo ramInfo;
private LogsInfo logsInfo;
private final BootstrapDumpInfo bootstrapInfo;
private final FlagsInfo flagsInfo;
private final List<ExtensionInfo> extensionInfo;
private final List<PackInfo> packInfo;
private final MappingInfo mappingInfo;
public DumpInfo(GeyserImpl geyser, boolean addLog) {
this.versionInfo = new VersionInfo();
@@ -92,27 +107,35 @@ public class DumpInfo {
this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY);
this.config = geyser.getConfig();
this.floodgate = new Floodgate();
String md5Hash = "unknown";
String sha256Hash = "unknown";
try {
// https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file
// https://stackoverflow.com/questions/304268/getting-a-files-md5-checksum-in-java
File file = new File(DumpInfo.class.getProtectionDomain().getCodeSource().getLocation().toURI());
ByteSource byteSource = Files.asByteSource(file);
// Jenkins uses MD5 for its hash - TODO remove
//noinspection UnstableApiUsage,deprecation
md5Hash = byteSource.hash(Hashing.md5()).toString();
//noinspection UnstableApiUsage
sha256Hash = byteSource.hash(Hashing.sha256()).toString();
} catch (Exception e) {
if (this.config.isDebugMode()) {
// Workaround for JsonAdapter not being allowed on methods
ConfigurationOptions options = InterfaceDefaultOptions.addTo(ConfigurationOptions.defaults(), builder ->
builder.addProcessor(AsteriskSerializer.Asterisk.class, String.class, AsteriskSerializer.CONFIGURATE_SERIALIZER))
.shouldCopyDefaults(false);
ConfigurationNode configNode = CommentedConfigurationNode.root(options);
configNode.set(geyser.config());
this.config = toGson(configNode);
} catch (SerializationException e) {
e.printStackTrace();
if (geyser.config().debugMode()) {
e.printStackTrace();
}
}
this.hashInfo = new HashInfo(md5Hash, sha256Hash);
String sha256Hash = "unknown";
try {
// https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file
File file = new File(DumpInfo.class.getProtectionDomain().getCodeSource().getLocation().toURI());
ByteSource byteSource = Files.asByteSource(file);
//noinspection UnstableApiUsage
sha256Hash = byteSource.hash(Hashing.sha256()).toString();
} catch (Exception e) {
if (geyser.config().debugMode()) {
e.printStackTrace();
}
}
this.hash = sha256Hash;
this.ramInfo = new RamInfo();
@@ -140,6 +163,44 @@ public class DumpInfo {
for (Extension extension : GeyserApi.api().extensionManager().extensions()) {
this.extensionInfo.add(new ExtensionInfo(extension.isEnabled(), extension.name(), extension.description().version(), extension.description().apiVersion(), extension.description().main(), extension.description().authors()));
}
this.packInfo = Registries.RESOURCE_PACKS.get().values().stream()
.map(PackInfo::new)
.toList();
this.mappingInfo = new MappingInfo(BlockRegistries.CUSTOM_BLOCKS.get().length,
BlockRegistries.CUSTOM_SKULLS.get().size(),
Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_PROTOCOL).getCustomIdMappings().size()
);
}
private JsonElement toGson(ConfigurationNode node) {
if (node.isMap()) {
JsonObject object = new JsonObject();
node.childrenMap().forEach((key, value) -> {
JsonElement json = toGson(value);
object.add(key.toString(), json);
});
return object;
} else if (node.isList()) {
JsonArray array = new JsonArray();
node.childrenList().forEach(childNode -> array.add(toGson(childNode)));
return array;
} else {
return convertRawScalar(node);
}
}
private JsonElement convertRawScalar(ConfigurationNode node) {
final @Nullable Object value = node.rawScalar();
if (value == null) {
return JsonNull.INSTANCE;
} else if (value instanceof Number n) {
return new JsonPrimitive(n);
} else if (value instanceof Boolean b) {
return new JsonPrimitive(b);
} else {
return new JsonPrimitive(value.toString());
}
}
@Getter
@@ -233,17 +294,6 @@ public class DumpInfo {
}
}
@Getter
public static class Floodgate {
private final Properties gitInfo;
private final Object config;
Floodgate() {
this.gitInfo = FloodgateInfoHolder.getGitProperties();
this.config = FloodgateInfoHolder.getConfig();
}
}
@Getter
public static class LogsInfo {
private String link;
@@ -253,16 +303,13 @@ public class DumpInfo {
Map<String, String> fields = new HashMap<>();
fields.put("content", FileUtils.readAllLines(geyser.getBootstrap().getLogsPath()).collect(Collectors.joining("\n")));
JsonNode logData = GeyserImpl.JSON_MAPPER.readTree(WebUtils.postForm("https://api.mclo.gs/1/log", fields));
JsonObject logData = new JsonParser().parse(WebUtils.postForm("https://api.mclo.gs/1/log", fields)).getAsJsonObject();
this.link = logData.get("url").textValue();
this.link = logData.get("url").getAsString();
} catch (IOException ignored) { }
}
}
public record HashInfo(String md5Hash, String sha256Hash) {
}
public record RamInfo(long free, long total, long max) {
public RamInfo() {
this(Runtime.getRuntime().freeMemory() / MEGABYTE,
@@ -283,7 +330,17 @@ public class DumpInfo {
public record ExtensionInfo(boolean enabled, String name, String version, String apiVersion, String main, List<String> authors) {
}
public record GitInfo(String buildNumber, @JsonProperty("git.commit.id.abbrev") String commitHashAbbrev, @JsonProperty("git.commit.id") String commitHash,
@JsonProperty("git.branch") String branchName, @JsonProperty("git.remote.origin.url") String originUrl) {
public record GitInfo(String buildNumber, @SerializedName("git.commit.id.abbrev") String commitHashAbbrev, @SerializedName("git.commit.id") String commitHash,
@SerializedName("git.branch") String branchName, @SerializedName("git.remote.origin.url") String originUrl) {
}
public record PackInfo(String name, String type) {
public PackInfo(ResourcePackHolder holder) {
this(holder.pack().manifest().header().name(), holder.codec().getClass().getSimpleName());
}
}
public record MappingInfo(int customBlocks, int customSkulls, int customItems) {
}
}

View File

@@ -72,7 +72,7 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
if (translator.acceptedType() != metadata.getType()) {
GeyserImpl.getInstance().getLogger().warning("Metadata ID " + metadata.getId() + " was received with type " + metadata.getType() + " but we expected " + translator.acceptedType() + " for " + entity.getDefinition().entityType());
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
GeyserImpl.getInstance().getLogger().debug(metadata.toString());
}
return;

View File

@@ -220,7 +220,7 @@ public class Entity implements GeyserEntity {
flagsDirty = false;
if (session.getGeyser().getConfig().isDebugMode() && PRINT_ENTITY_SPAWN_DEBUG) {
if (session.getGeyser().config().debugMode() && PRINT_ENTITY_SPAWN_DEBUG) {
EntityType type = definition.entityType();
String name = type != null ? type.name() : getClass().getSimpleName();
session.getGeyser().getLogger().debug("Spawned entity " + name + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")");

View File

@@ -34,6 +34,7 @@ import org.geysermc.geyser.api.pack.exception.ResourcePackException;
import org.geysermc.geyser.api.pack.option.ResourcePackOption;
import org.geysermc.geyser.pack.GeyserResourcePack;
import org.geysermc.geyser.pack.ResourcePackHolder;
import org.geysermc.geyser.util.GeyserIntegratedPackUtil;
import java.util.Collection;
import java.util.List;
@@ -42,11 +43,12 @@ import java.util.Objects;
import java.util.UUID;
@Getter
public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent {
public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent implements GeyserIntegratedPackUtil {
private final Map<UUID, ResourcePackHolder> packs;
public GeyserDefineResourcePacksEventImpl(Map<UUID, ResourcePackHolder> packMap) {
this.packs = packMap;
registerGeyserPack(this);
}
@Override
@@ -61,6 +63,8 @@ public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePack
throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION);
}
preProcessPack(pack);
UUID uuid = resourcePack.uuid();
if (packs.containsKey(uuid)) {
throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE);
@@ -120,4 +124,14 @@ public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePack
holder.optionHolder().validateAndAdd(holder.pack(), options);
}
@Override
public void unregisterIntegratedPack() {
unregister(INTEGRATED_PACK_UUID);
}
@Override
public boolean integratedPackRegistered() {
return packs.containsKey(INTEGRATED_PACK_UUID);
}
}

View File

@@ -45,6 +45,7 @@ import org.geysermc.geyser.pack.ResourcePackHolder;
import org.geysermc.geyser.pack.option.OptionHolder;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.GeyserIntegratedPackUtil;
import java.util.AbstractMap;
import java.util.ArrayList;
@@ -55,7 +56,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent {
public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent implements GeyserIntegratedPackUtil {
/**
* The packs for this Session. A {@link ResourcePackHolder} may contain resource pack options registered
@@ -103,6 +104,8 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION);
}
preProcessPack(pack);
UUID uuid = resourcePack.uuid();
if (packs.containsKey(uuid)) {
throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE);
@@ -178,6 +181,16 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
holder.validateAndAdd(pack, options);
}
@Override
public void unregisterIntegratedPack() {
unregister(INTEGRATED_PACK_UUID);
}
@Override
public boolean integratedPackRegistered() {
return packs.containsKey(INTEGRATED_PACK_UUID);
}
// Methods used internally for e.g. ordered packs, or resource pack entries
public List<ResourcePackStackPacket.Entry> orderedPacks() {
@@ -201,7 +214,14 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
public List<ResourcePacksInfoPacket.Entry> infoPacketEntries() {
List<ResourcePacksInfoPacket.Entry> entries = new ArrayList<>();
boolean anyCdn = packs.values().stream().anyMatch(holder -> holder.codec() instanceof UrlPackCodec);
boolean warned = false;
for (ResourcePackHolder holder : packs.values()) {
if (!warned && anyCdn && !(holder.codec() instanceof UrlPackCodec)) {
GeyserImpl.getInstance().getLogger().warning("Mixing pack codecs will result in all UrlPackCodec delivered packs to fall back to non-cdn delivery!");
warned = true;
}
GeyserResourcePack pack = holder.pack();
ResourcePackManifest.Header header = pack.manifest().header();
entries.add(new ResourcePacksInfoPacket.Entry(

View File

@@ -460,7 +460,7 @@ public final class ClickPlan {
} else if (action.click.actionType == ContainerActionType.MOVE_TO_HOTBAR_SLOT) {
stateIdIncrements = 1;
} else {
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().config().debugMode()) {
session.getGeyser().getLogger().debug("Not sure how to handle state ID hack in crafting table: " + plan);
}
stateIdIncrements = 1;

View File

@@ -103,7 +103,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
super(geyser, session);
ZlibCompression compression = new ZlibCompression(Zlib.RAW);
compression.setLevel(this.geyser.getConfig().getBedrock().getCompressionLevel());
compression.setLevel(this.geyser.config().advanced().bedrock().compressionLevel());
this.compressionStrategy = new SimpleCompressionStrategy(compression);
}
@@ -236,14 +236,17 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
// Can happen if an error occurs in the resource pack event; that'll disconnect the player
return PacketSignal.HANDLED;
}
session.integratedPackActive(resourcePackLoadEvent.isIntegratedPackActive());
ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket();
resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries());
resourcePacksInfo.setVibrantVisualsForceDisabled(!session.isAllowVibrantVisuals());
resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks());
resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().config().gameplay().forceResourcePacks() ||
resourcePackLoadEvent.isIntegratedPackActive());
resourcePacksInfo.setWorldTemplateId(UUID.randomUUID());
resourcePacksInfo.setWorldTemplateVersion("*");
session.sendUpstreamPacket(resourcePacksInfo);
GeyserLocale.loadGeyserLocale(session.locale());
@@ -264,7 +267,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
switch (packet.getStatus()) {
case COMPLETED -> {
finishedResourcePackSending = true;
if (geyser.getConfig().getRemote().authType() != AuthType.ONLINE) {
if (geyser.config().java().authType() != AuthType.ONLINE) {
session.authenticate(session.getAuthData().name());
} else if (!couldLoginUserByName(session.getAuthData().name())) {
// We must spawn the white world
@@ -296,6 +299,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
session.sendUpstreamPacket(stackPacket);
}
case REFUSED -> session.disconnect("disconnectionScreen.resourcePack");
default -> {
GeyserImpl.getInstance().getLogger().debug("received unknown status packet: " + packet);
session.disconnect("disconnectionScreen.resourcePack");
@@ -315,7 +319,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
}
private boolean couldLoginUserByName(String bedrockUsername) {
if (geyser.getConfig().getSavedUserLogins().contains(bedrockUsername)) {
if (geyser.config().savedUserLogins().contains(bedrockUsername)) {
String authChain = geyser.authChainFor(bedrockUsername);
if (authChain != null) {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().name()));
@@ -425,7 +429,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
// Also flushes packets
// Avoids bursting slower / delayed clients
session.sendUpstreamPacketImmediately(data);
GeyserImpl.getInstance().getScheduledThread().schedule(this::processNextChunk, PACKET_SEND_DELAY, TimeUnit.MILLISECONDS);
session.scheduleInEventLoop(this::processNextChunk, PACKET_SEND_DELAY, TimeUnit.MILLISECONDS);
} else {
session.sendUpstreamPacket(data);
}

View File

@@ -50,7 +50,7 @@ public abstract class GeyserInjector {
* @param bootstrap the bootstrap of the Geyser instance.
*/
public void initializeLocalChannel(GeyserBootstrap bootstrap) {
if (!bootstrap.getGeyserConfig().isUseDirectConnection()) {
if (!bootstrap.config().advanced().java().useDirectConnection()) {
bootstrap.getGeyserLogger().debug("Disabling direct injection!");
return;
}

View File

@@ -46,7 +46,7 @@ import org.cloudburstmc.protocol.bedrock.BedrockPong;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.connection.ConnectionRequestEvent;
import org.geysermc.geyser.command.defaults.ConnectionTestCommand;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.event.type.GeyserBedrockPingEventImpl;
import org.geysermc.geyser.network.CIDRMatcher;
import org.geysermc.geyser.network.GameProtocol;
@@ -60,10 +60,13 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.WebUtils;
import org.geysermc.mcprotocollib.network.helper.TransportHelper;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -131,7 +134,7 @@ public final class GeyserServer {
this.listenCount = 1;
}
if (this.geyser.getConfig().getBedrock().isEnableProxyProtocol()) {
if (this.geyser.config().advanced().bedrock().useHaproxyProtocol()) {
this.proxiedAddresses = ExpiringMap.builder()
.expiration(30 + 1, TimeUnit.MINUTES)
.expirationPolicy(ExpirationPolicy.ACCESSED).build();
@@ -139,7 +142,7 @@ public final class GeyserServer {
this.proxiedAddresses = null;
}
this.broadcastPort = geyser.getConfig().getBedrock().broadcastPort();
this.broadcastPort = geyser.config().advanced().bedrock().broadcastPort();
}
public CompletableFuture<Void> bind(InetSocketAddress address) {
@@ -161,12 +164,12 @@ public final class GeyserServer {
.addAfter(RakServerOfflineHandler.NAME, RakPingHandler.NAME, new RakPingHandler(this));
// Add proxy handler
boolean isProxyProtocol = this.geyser.getConfig().getBedrock().isEnableProxyProtocol();
boolean isProxyProtocol = this.geyser.config().advanced().bedrock().useHaproxyProtocol();
if (isProxyProtocol) {
channel.pipeline().addFirst("proxy-protocol-decoder", new ProxyServerHandler());
}
boolean isWhitelistedProxyProtocol = isProxyProtocol && !this.geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs().isEmpty();
boolean isWhitelistedProxyProtocol = isProxyProtocol && !this.geyser.config().advanced().bedrock().haproxyProtocolWhitelistedIps().isEmpty();
if (Boolean.parseBoolean(System.getProperty("Geyser.RakRateLimitingDisabled", "false")) || isWhitelistedProxyProtocol) {
// We would already block any non-whitelisted IP addresses in onConnectionRequest so we can remove the rate limiter
channel.pipeline().remove(RakServerRateLimiter.NAME);
@@ -200,7 +203,7 @@ public final class GeyserServer {
@SuppressWarnings("Convert2MethodRef")
private ServerBootstrap createBootstrap() {
if (this.geyser.getConfig().isDebugMode()) {
if (this.geyser.config().debugMode()) {
this.geyser.getLogger().debug("Transport type: " + TRANSPORT.method().name());
if (TRANSPORT.datagramChannelClass() == NioDatagramChannel.class) {
if (System.getProperties().contains("disableNativeEventLoop")) {
@@ -216,7 +219,7 @@ public final class GeyserServer {
GeyserServerInitializer serverInitializer = new GeyserServerInitializer(this.geyser);
playerGroup = serverInitializer.getEventLoopGroup();
this.geyser.getLogger().debug("Setting MTU to " + this.geyser.getConfig().getMtu());
this.geyser.getLogger().debug("Setting MTU to " + this.geyser.config().advanced().bedrock().mtu());
int rakPacketLimit = positivePropOrDefault("Geyser.RakPacketLimit", DEFAULT_PACKET_LIMIT);
this.geyser.getLogger().debug("Setting RakNet packet limit to " + rakPacketLimit);
@@ -231,7 +234,7 @@ public final class GeyserServer {
.channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannelClass()))
.group(group, childGroup)
.option(RakChannelOption.RAK_HANDLE_PING, true)
.option(RakChannelOption.RAK_MAX_MTU, this.geyser.getConfig().getMtu())
.option(RakChannelOption.RAK_MAX_MTU, this.geyser.config().advanced().bedrock().mtu())
.option(RakChannelOption.RAK_PACKET_LIMIT, rakPacketLimit)
.option(RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, rakGlobalPacketLimit)
.option(RakChannelOption.RAK_SEND_COOKIE, rakSendCookie)
@@ -239,10 +242,10 @@ public final class GeyserServer {
}
public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) {
List<String> allowedProxyIPs = geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs();
if (geyser.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) {
List<String> allowedProxyIPs = geyser.config().advanced().bedrock().haproxyProtocolWhitelistedIps();
if (geyser.config().advanced().bedrock().useHaproxyProtocol() && !allowedProxyIPs.isEmpty()) {
boolean isWhitelistedIP = false;
for (CIDRMatcher matcher : geyser.getConfig().getBedrock().getWhitelistedIPsMatchers()) {
for (CIDRMatcher matcher : getWhitelistedIPsMatchers()) {
if (matcher.matches(inetSocketAddress.getAddress())) {
isWhitelistedIP = true;
break;
@@ -256,8 +259,8 @@ public final class GeyserServer {
}
String ip;
if (geyser.getConfig().isLogPlayerIpAddresses()) {
if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) {
if (geyser.config().logPlayerIpAddresses()) {
if (geyser.config().advanced().bedrock().useHaproxyProtocol()) {
ip = this.proxiedAddresses.getOrDefault(inetSocketAddress, inetSocketAddress).toString();
} else {
ip = inetSocketAddress.toString();
@@ -283,10 +286,10 @@ public final class GeyserServer {
}
public BedrockPong onQuery(Channel channel, InetSocketAddress inetSocketAddress) {
if (geyser.getConfig().isDebugMode() && PRINT_DEBUG_PINGS) {
if (geyser.config().debugMode() && PRINT_DEBUG_PINGS) {
String ip;
if (geyser.getConfig().isLogPlayerIpAddresses()) {
if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) {
if (geyser.config().logPlayerIpAddresses()) {
if (geyser.config().advanced().bedrock().useHaproxyProtocol()) {
ip = this.proxiedAddresses.getOrDefault(inetSocketAddress, inetSocketAddress).toString();
} else {
ip = inetSocketAddress.toString();
@@ -297,10 +300,10 @@ public final class GeyserServer {
geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.network.pinged", ip));
}
GeyserConfiguration config = geyser.getConfig();
GeyserConfig config = geyser.config();
GeyserPingInfo pingInfo = null;
if (config.isPassthroughMotd() || config.isPassthroughPlayerCounts()) {
if (config.motd().passthroughMotd() || config.motd().passthroughPlayerCounts()) {
IGeyserPingPassthrough pingPassthrough = geyser.getBootstrap().getGeyserPingPassthrough();
if (pingPassthrough != null) {
pingInfo = pingPassthrough.getPingInformation(inetSocketAddress);
@@ -317,25 +320,25 @@ public final class GeyserServer {
.ipv6Port(this.broadcastPort)
.serverId(channel.config().getOption(RakChannelOption.RAK_GUID));
if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
if (config.motd().passthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
String mainMotd = (motd.length > 0) ? motd[0] : config.getBedrock().primaryMotd(); // First line of the motd.
String subMotd = (motd.length > 1) ? motd[1] : config.getBedrock().secondaryMotd(); // Second line of the motd if present, otherwise default.
String mainMotd = (motd.length > 0) ? motd[0] : config.motd().primaryMotd(); // First line of the motd.
String subMotd = (motd.length > 1) ? motd[1] : config.motd().secondaryMotd(); // Second line of the motd if present, otherwise default.
pong.motd(mainMotd.trim());
pong.subMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit.
} else {
pong.motd(config.getBedrock().primaryMotd());
pong.subMotd(config.getBedrock().secondaryMotd());
pong.motd(config.motd().primaryMotd());
pong.subMotd(config.motd().secondaryMotd());
}
// Placed here to prevent overriding values set in the ping event.
if (config.isPassthroughPlayerCounts() && pingInfo != null) {
if (config.motd().passthroughPlayerCounts() && pingInfo != null) {
pong.playerCount(pingInfo.getPlayers().getOnline());
pong.maximumPlayerCount(pingInfo.getPlayers().getMax());
} else {
pong.playerCount(geyser.getSessionManager().getSessions().size());
pong.maximumPlayerCount(config.getMaxPlayers());
pong.maximumPlayerCount(config.motd().maxPlayers());
}
this.geyser.eventBus().fire(new GeyserBedrockPingEventImpl(pong, inetSocketAddress));
@@ -386,6 +389,35 @@ public final class GeyserServer {
return pong;
}
private List<CIDRMatcher> whitelistedIPsMatchers = null;
/**
* @return Unmodifiable list of {@link CIDRMatcher}s from {@link GeyserConfig.AdvancedBedrockConfig#haproxyProtocolWhitelistedIps()}
*/
public List<CIDRMatcher> getWhitelistedIPsMatchers() {
// Effective Java, Third Edition; Item 83: Use lazy initialization judiciously
List<CIDRMatcher> matchers = this.whitelistedIPsMatchers;
if (matchers == null) {
synchronized (this) {
// Check if proxyProtocolWhitelistedIPs contains URLs we need to fetch and parse by line
List<String> whitelistedCIDRs = new ArrayList<>();
for (String ip: geyser.config().advanced().bedrock().haproxyProtocolWhitelistedIps()) {
if (!ip.startsWith("http")) {
whitelistedCIDRs.add(ip);
continue;
}
WebUtils.getLineStream(ip).forEach(whitelistedCIDRs::add);
}
this.whitelistedIPsMatchers = matchers = whitelistedCIDRs.stream()
.map(CIDRMatcher::new)
.toList();
}
}
return Collections.unmodifiableList(matchers);
}
/**
* @return the throwable from the given supplier, or the throwable caught while calling the supplier.
*/

View File

@@ -25,21 +25,23 @@
package org.geysermc.geyser.pack;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.pack.ResourcePackManifest;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.UUID;
public record GeyserResourcePackManifest(
@JsonProperty("format_version") int formatVersion,
@SerializedName("format_version") int formatVersion,
Header header,
Collection<Module> modules,
Collection<Dependency> dependencies,
@@ -60,13 +62,13 @@ public record GeyserResourcePackManifest(
this.settings = ensureNonNull(settings);
}
public record Header(UUID uuid, Version version, String name, String description, @JsonProperty("min_engine_version") Version minimumSupportedMinecraftVersion) implements ResourcePackManifest.Header { }
public record Header(UUID uuid, Version version, String name, String description, @SerializedName("min_engine_version") Version minimumSupportedMinecraftVersion) implements ResourcePackManifest.Header { }
public record Module(UUID uuid, Version version, String type, String description) implements ResourcePackManifest.Module { }
public record Dependency(UUID uuid, Version version) implements ResourcePackManifest.Dependency { }
public record Subpack(@JsonProperty("folder_name") String folderName, String name, @JsonProperty("memory_tier") Float memoryTier) implements ResourcePackManifest.Subpack { }
public record Subpack(@SerializedName("folder_name") String folderName, String name, @SerializedName("memory_tier") Float memoryTier) implements ResourcePackManifest.Subpack { }
public record Setting(String type, String text) implements ResourcePackManifest.Setting { }
@@ -75,7 +77,7 @@ public record GeyserResourcePackManifest(
return Collections.unmodifiableCollection(collection);
}
@JsonDeserialize(using = Version.VersionDeserializer.class)
@JsonAdapter(value = Version.VersionDeserializer.class)
public record Version(int major, int minor, int patch) implements ResourcePackManifest.Version {
@Override
@@ -83,11 +85,11 @@ public record GeyserResourcePackManifest(
return major + "." + minor + "." + patch;
}
public static class VersionDeserializer extends JsonDeserializer<Version> {
public static class VersionDeserializer implements JsonDeserializer<Version> {
@Override
public Version deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
int[] version = ctxt.readValue(p, int[].class);
return new Version(version[0], version[1], version[2]);
public Version deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray array = json.getAsJsonArray();
return new Version(array.get(0).getAsInt(), array.get(1).getAsInt(), array.get(2).getAsInt());
}
}
}

View File

@@ -78,7 +78,7 @@ public class SkullResourcePackManager {
Path packPath = cachePath.resolve("player_skulls.mcpack");
File packFile = packPath.toFile();
if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().config().gameplay().enableCustomContent()) {
packFile.delete(); // No need to keep resource pack
return null;
}
@@ -161,7 +161,7 @@ public class SkullResourcePackManager {
}
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().debug("Unable to clean up skull skin cache.");
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
* Copyright (c) 2019-2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -25,8 +25,7 @@
package org.geysermc.geyser.ping;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.gson.JsonSyntaxException;
import io.netty.handler.codec.haproxy.HAProxyCommand;
import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
import io.netty.util.NetUtil;
@@ -34,18 +33,29 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.util.VarInts;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.util.JsonUtils;
import java.io.*;
import java.net.*;
import java.util.concurrent.TimeUnit;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable {
public class GeyserLegacyPingPassthrough extends Thread implements IGeyserPingPassthrough, Runnable {
private static final byte[] HAPROXY_BINARY_PREFIX = new byte[]{13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10};
private final GeyserImpl geyser;
private final long interval;
public GeyserLegacyPingPassthrough(GeyserImpl geyser) {
public GeyserLegacyPingPassthrough(GeyserImpl geyser, int interval) {
this.geyser = geyser;
this.interval = interval * 1000L;
}
private GeyserPingInfo pingInfo;
@@ -56,12 +66,14 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn
* @return GeyserPingPassthrough, or null if not initialized
*/
public static @Nullable IGeyserPingPassthrough init(GeyserImpl geyser) {
if (geyser.getConfig().isPassthroughMotd() || geyser.getConfig().isPassthroughPlayerCounts()) {
GeyserLegacyPingPassthrough pingPassthrough = new GeyserLegacyPingPassthrough(geyser);
if (geyser.config().motd().passthroughMotd() || geyser.config().motd().passthroughPlayerCounts()) {
// Ensure delay is not zero
int interval = (geyser.getConfig().getPingPassthroughInterval() == 0) ? 1 : geyser.getConfig().getPingPassthroughInterval();
int interval = (geyser.config().motd().pingPassthroughInterval() == 0) ? 1 : geyser.config().motd().pingPassthroughInterval();
geyser.getLogger().debug("Scheduling ping passthrough at an interval of " + interval + " second(s).");
geyser.getScheduledThread().scheduleAtFixedRate(pingPassthrough, 1, interval, TimeUnit.SECONDS);
GeyserLegacyPingPassthrough pingPassthrough = new GeyserLegacyPingPassthrough(geyser, interval);
pingPassthrough.setName("Geyser LegacyPingPassthrough Thread");
pingPassthrough.setDaemon(true);
pingPassthrough.start();
return pingPassthrough;
}
return null;
@@ -74,76 +86,84 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn
@Override
public void run() {
try (Socket socket = new Socket()) {
String address = geyser.getConfig().getRemote().address();
int port = geyser.getConfig().getRemote().port();
InetSocketAddress endpoint = new InetSocketAddress(address, port);
socket.connect(endpoint, 5000);
while (!geyser.isShuttingDown() && !geyser.isReloading()) {
try (Socket socket = new Socket()) {
String address = geyser.config().java().address();
int port = geyser.config().java().port();
InetSocketAddress endpoint = new InetSocketAddress(address, port);
socket.connect(endpoint, 5000);
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
try (DataOutputStream handshake = new DataOutputStream(byteArrayStream)) {
handshake.write(0x0);
VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion());
VarInts.writeUnsignedInt(handshake, address.length());
handshake.writeBytes(address);
handshake.writeShort(port);
VarInts.writeUnsignedInt(handshake, 1);
}
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
try (DataOutputStream handshake = new DataOutputStream(byteArrayStream)) {
handshake.write(0x0);
VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion());
VarInts.writeUnsignedInt(handshake, address.length());
handshake.writeBytes(address);
handshake.writeShort(port);
VarInts.writeUnsignedInt(handshake, 1);
}
byte[] buffer;
byte[] buffer;
try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())) {
if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
// HAProxy support
// Based on https://github.com/netty/netty/blob/d8ad931488f6b942dabe28ecd6c399b4438da0a8/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageEncoder.java#L78
dataOutputStream.write(HAPROXY_BINARY_PREFIX);
dataOutputStream.writeByte((0x02 << 4) | HAProxyCommand.PROXY.byteValue());
dataOutputStream.writeByte(socket.getLocalAddress() instanceof Inet4Address ?
try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())) {
if (geyser.config().advanced().java().useHaproxyProtocol()) {
// HAProxy support
// Based on https://github.com/netty/netty/blob/d8ad931488f6b942dabe28ecd6c399b4438da0a8/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageEncoder.java#L78
dataOutputStream.write(HAPROXY_BINARY_PREFIX);
dataOutputStream.writeByte((0x02 << 4) | HAProxyCommand.PROXY.byteValue());
dataOutputStream.writeByte(socket.getLocalAddress() instanceof Inet4Address ?
HAProxyProxiedProtocol.TCP4.byteValue() : HAProxyProxiedProtocol.TCP6.byteValue());
byte[] srcAddrBytes = NetUtil.createByteArrayFromIpAddressString(
byte[] srcAddrBytes = NetUtil.createByteArrayFromIpAddressString(
((InetSocketAddress) socket.getLocalSocketAddress()).getAddress().getHostAddress());
byte[] dstAddrBytes = NetUtil.createByteArrayFromIpAddressString(
byte[] dstAddrBytes = NetUtil.createByteArrayFromIpAddressString(
endpoint.getAddress().getHostAddress());
dataOutputStream.writeShort(srcAddrBytes.length + dstAddrBytes.length + 4);
dataOutputStream.write(srcAddrBytes);
dataOutputStream.write(dstAddrBytes);
dataOutputStream.writeShort(((InetSocketAddress) socket.getLocalSocketAddress()).getPort());
dataOutputStream.writeShort(port);
}
dataOutputStream.writeShort(srcAddrBytes.length + dstAddrBytes.length + 4);
dataOutputStream.write(srcAddrBytes);
dataOutputStream.write(dstAddrBytes);
dataOutputStream.writeShort(((InetSocketAddress) socket.getLocalSocketAddress()).getPort());
dataOutputStream.writeShort(port);
}
VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
dataOutputStream.write(byteArrayStream.toByteArray());
dataOutputStream.writeByte(0x01);
dataOutputStream.writeByte(0x00);
try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) {
VarInts.readUnsignedInt(dataInputStream);
VarInts.readUnsignedInt(dataInputStream);
int length = VarInts.readUnsignedInt(dataInputStream);
buffer = new byte[length];
dataInputStream.readFully(buffer);
dataOutputStream.writeByte(0x09);
VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
dataOutputStream.write(byteArrayStream.toByteArray());
dataOutputStream.writeByte(0x01);
dataOutputStream.writeLong(System.currentTimeMillis());
dataOutputStream.writeByte(0x00);
VarInts.readUnsignedInt(dataInputStream);
try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) {
VarInts.readUnsignedInt(dataInputStream);
VarInts.readUnsignedInt(dataInputStream);
int length = VarInts.readUnsignedInt(dataInputStream);
buffer = new byte[length];
dataInputStream.readFully(buffer);
dataOutputStream.writeByte(0x09);
dataOutputStream.writeByte(0x01);
dataOutputStream.writeLong(System.currentTimeMillis());
VarInts.readUnsignedInt(dataInputStream);
}
}
this.pingInfo = JsonUtils.fromJson(buffer, GeyserPingInfo.class);
} catch (SocketTimeoutException | ConnectException ex) {
this.pingInfo = null;
this.geyser.getLogger().debug("Connection timeout for ping passthrough.");
} catch (JsonSyntaxException ex) {
this.geyser.getLogger().error("Failed to parse json when pinging server!", ex);
} catch (EOFException e) {
this.pingInfo = null;
this.geyser.getLogger().warning("Failed to ping the remote Java server! Is it online and configured in Geyser's config?");
} catch (UnknownHostException ex) {
// Don't reset pingInfo, as we want to keep the last known value
this.geyser.getLogger().warning("Unable to resolve remote host! Is the remote server down or invalid?");
} catch (IOException e) {
this.geyser.getLogger().error("IO error while trying to use legacy ping passthrough", e);
}
this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(buffer, GeyserPingInfo.class);
} catch (SocketTimeoutException | ConnectException ex) {
this.pingInfo = null;
this.geyser.getLogger().debug("Connection timeout for ping passthrough.");
} catch (JsonParseException | JsonMappingException ex) {
this.geyser.getLogger().error("Failed to parse json when pinging server!", ex);
} catch (EOFException e) {
this.pingInfo = null;
this.geyser.getLogger().warning("Failed to ping the remote Java server! Is it online and configured in Geyser's config?");
} catch (UnknownHostException ex) {
// Don't reset pingInfo, as we want to keep the last known value
this.geyser.getLogger().warning("Unable to resolve remote host! Is the remote server down or invalid?");
} catch (IOException e) {
this.geyser.getLogger().error("IO error while trying to use legacy ping passthrough", e);
try {
Thread.sleep(interval);
} catch (InterruptedException ex) {
// no-op
}
}
}
}

View File

@@ -25,21 +25,25 @@
package org.geysermc.geyser.ping;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
import lombok.Data;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.lang.reflect.Type;
/**
* The structure of this class and its nested classes are specifically
* designed for the format received by {@link GeyserLegacyPingPassthrough}.
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GeyserPingInfo {
@Nullable
@JsonAdapter(DescriptionDeserializer.class)
private String description;
private Players players;
@@ -58,13 +62,7 @@ public class GeyserPingInfo {
this.players = new Players(maxPlayers, onlinePlayers);
}
@JsonSetter("description")
void setDescription(JsonNode description) {
this.description = description.toString();
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Players {
private int max;
@@ -79,4 +77,14 @@ public class GeyserPingInfo {
this.online = online;
}
}
/**
* So GSON does not complain how we are treating Description - it will be converted to a proper Component later.
*/
private static final class DescriptionDeserializer implements JsonDeserializer<String> {
@Override
public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return json.toString();
}
}
}

View File

@@ -73,7 +73,7 @@ public class PacketTranslatorRegistry<T> extends AbstractMappedRegistry<Class<?
}
return true;
} else {
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
if (!IGNORED_PACKETS.contains(clazz)) {
GeyserImpl.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet));
}

View File

@@ -25,14 +25,16 @@
package org.geysermc.geyser.registry.loader;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.util.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Map;
public class BiomeIdentifierRegistryLoader implements RegistryLoader<String, Object2IntMap<String>> {
@@ -43,12 +45,12 @@ public class BiomeIdentifierRegistryLoader implements RegistryLoader<String, Obj
// crashes the client. Therefore, we need to have a list of all valid Bedrock biome IDs with which we can use from.
// The server sends the corresponding Java network IDs, so we don't need to worry about that now.
// Reference variable for Jackson to read off of
TypeReference<Map<String, BiomeEntry>> biomeEntriesType = new TypeReference<>() { };
// Reference variable for Gson to read off of
Type biomeEntriesType = new TypeToken<Map<String, BiomeEntry>>() { }.getType();
Map<String, BiomeEntry> biomeEntries;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("mappings/biomes.json")) {
biomeEntries = GeyserImpl.JSON_MAPPER.readValue(stream, biomeEntriesType);
biomeEntries = JsonUtils.fromJson(stream, biomeEntriesType);
} catch (IOException e) {
throw new AssertionError("Unable to load Bedrock runtime biomes", e);
}
@@ -66,7 +68,7 @@ public class BiomeIdentifierRegistryLoader implements RegistryLoader<String, Obj
/**
* The Bedrock network ID for this biome.
*/
@JsonProperty("bedrock_id")
@SerializedName("bedrock_id")
private int bedrockId;
}
}

View File

@@ -25,12 +25,12 @@
package org.geysermc.geyser.registry.loader;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.geysermc.geyser.GeyserImpl;
import java.io.InputStream;
import java.util.Map;
import java.util.WeakHashMap;
import java.io.InputStreamReader;
/**
* An abstract registry loader for loading effects from a resource path.
@@ -38,21 +38,14 @@ import java.util.WeakHashMap;
* @param <T> the value
*/
public abstract class EffectRegistryLoader<T> implements RegistryLoader<String, T> {
private static final Map<String, JsonNode> loadedFiles = new WeakHashMap<>();
public void loadFile(String input) {
if (!loadedFiles.containsKey(input)) {
JsonNode effects;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input)) {
effects = GeyserImpl.JSON_MAPPER.readTree(stream);
} catch (Exception e) {
throw new AssertionError("Unable to load registrations for " + input, e);
}
loadedFiles.put(input, effects);
public JsonObject loadFile(String input) {
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input);
InputStreamReader reader = new InputStreamReader(stream)) {
//noinspection deprecation
return new JsonParser().parse(reader).getAsJsonObject();
} catch (Exception e) {
throw new AssertionError("Unable to load registrations for " + input, e);
}
}
public JsonNode get(String input) {
return loadedFiles.get(input);
}
}
}

View File

@@ -25,15 +25,15 @@
package org.geysermc.geyser.registry.loader;
import com.fasterxml.jackson.databind.JsonNode;
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.LevelEventType;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.registry.type.ParticleMapping;
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
@@ -44,16 +44,13 @@ public class ParticleTypesRegistryLoader extends EffectRegistryLoader<Map<Partic
@Override
public Map<ParticleType, ParticleMapping> load(String input) {
this.loadFile(input);
Iterator<Map.Entry<String, JsonNode>> particlesIterator = this.get(input).fields();
JsonObject particlesJson = this.loadFile(input);
Map<ParticleType, ParticleMapping> particles = new Object2ObjectOpenHashMap<>();
try {
while (particlesIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = particlesIterator.next();
for (Map.Entry<String, JsonElement> entry : particlesJson.entrySet()) {
String key = entry.getKey().toUpperCase(Locale.ROOT);
JsonNode bedrockId = entry.getValue().get("bedrockId");
JsonNode eventType = entry.getValue().get("eventType");
JsonElement bedrockId = entry.getValue().getAsJsonObject().get("bedrockId");
JsonElement eventType = entry.getValue().getAsJsonObject().get("eventType");
if (eventType == null && bedrockId == null) {
GeyserImpl.getInstance().getLogger().debug("Skipping particle mapping " + key + " because no Bedrock equivalent exists.");
continue;
@@ -63,16 +60,16 @@ public class ParticleTypesRegistryLoader extends EffectRegistryLoader<Map<Partic
if (eventType != null) {
try {
// Check if we have a particle type mapping
type = org.cloudburstmc.protocol.bedrock.data.ParticleType.valueOf(eventType.asText().toUpperCase(Locale.ROOT));
type = org.cloudburstmc.protocol.bedrock.data.ParticleType.valueOf(eventType.getAsString().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ex) {
// No particle type; try level event
type = LevelEvent.valueOf(eventType.asText().toUpperCase(Locale.ROOT));
type = LevelEvent.valueOf(eventType.getAsString().toUpperCase(Locale.ROOT));
}
}
particles.put(ParticleType.valueOf(key), new ParticleMapping(
type,
bedrockId == null ? null : bedrockId.asText())
bedrockId == null ? null : bedrockId.getAsString())
);
}
} catch (Exception e) {

View File

@@ -124,20 +124,19 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks);
GeyserImpl.getInstance().eventBus().fire(event);
GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap);
for (Path path : event.resourcePacks()) {
try {
GeyserResourcePack pack = readPack(path).build();
packMap.put(pack.uuid(), ResourcePackHolder.of(pack));
defineEvent.register(readPack(path).build());
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", path));
e.printStackTrace();
}
}
// Load all remote resource packs from the config before firing the new event
// TODO configurate
//packMap.putAll(loadRemotePacks());
GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap);
loadRemotePacks(defineEvent);
GeyserImpl.getInstance().eventBus().fire(defineEvent);
// After loading the new resource packs: let's clean up the old url packs
@@ -225,7 +224,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
}
}
private Map<UUID, ResourcePackHolder> loadRemotePacks() {
private void loadRemotePacks(GeyserDefineResourcePacksEventImpl event) {
GeyserImpl instance = GeyserImpl.getInstance();
// Unable to make this a static variable, as the test would fail
final Path cachedDirectory = instance.getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs");
@@ -235,19 +234,14 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
Files.createDirectories(cachedDirectory);
} catch (IOException e) {
instance.getLogger().error("Could not create remote pack cache directory", e);
return new Object2ObjectOpenHashMap<>();
return;
}
}
//List<String> remotePackUrls = instance.getConfig().getResourcePackUrls();
List<String> remotePackUrls = List.of();
Map<UUID, ResourcePackHolder> packMap = new Object2ObjectOpenHashMap<>();
List<String> remotePackUrls = instance.config().advanced().resourcePackUrls();
for (String url : remotePackUrls) {
try {
GeyserUrlPackCodec codec = new GeyserUrlPackCodec(url);
GeyserResourcePack pack = codec.create();
packMap.put(pack.uuid(), ResourcePackHolder.of(pack));
event.register(new GeyserUrlPackCodec(url).create());
} catch (Throwable e) {
instance.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url));
instance.getLogger().error(e.getMessage());
@@ -256,8 +250,6 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
}
}
}
return packMap;
}
/**

View File

@@ -25,8 +25,8 @@
package org.geysermc.geyser.registry.loader;
import com.fasterxml.jackson.databind.JsonNode;
import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEvent;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.cloudburstmc.protocol.bedrock.data.LevelEventType;
import org.geysermc.geyser.GeyserImpl;
@@ -34,8 +34,8 @@ import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.translator.level.event.PlaySoundEventTranslator;
import org.geysermc.geyser.translator.level.event.SoundEventEventTranslator;
import org.geysermc.geyser.translator.level.event.SoundLevelEventTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEvent;
import java.util.Iterator;
import java.util.Map;
/**
@@ -47,37 +47,36 @@ public class SoundEventsRegistryLoader extends EffectRegistryLoader<Map<LevelEve
public Map<LevelEvent, LevelEventTranslator> load(String input) {
this.loadFile(input);
Iterator<Map.Entry<String, JsonNode>> effectsIterator = this.get(input).fields();
JsonObject effectsJson = this.loadFile(input);
Map<LevelEvent, LevelEventTranslator> soundEffects = new Object2ObjectOpenHashMap<>();
while (effectsIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = effectsIterator.next();
JsonNode node = entry.getValue();
for (Map.Entry<String, JsonElement> entry : effectsJson.entrySet()) {
JsonObject node = entry.getValue().getAsJsonObject();
try {
String type = node.get("type").asText();
String type = node.get("type").getAsString();
LevelEvent javaEffect = null;
LevelEventTranslator transformer = null;
switch (type) {
case "soundLevel" -> {
javaEffect = org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType.valueOf(entry.getKey());
LevelEventType levelEventType = org.cloudburstmc.protocol.bedrock.data.LevelEvent.valueOf(node.get("name").asText());
int data = node.has("data") ? node.get("data").intValue() : 0;
LevelEventType levelEventType = org.cloudburstmc.protocol.bedrock.data.LevelEvent.valueOf(node.get("name").getAsString());
int data = node.has("data") ? node.get("data").getAsInt() : 0;
transformer = new SoundLevelEventTranslator(levelEventType, data);
}
case "soundEvent" -> {
javaEffect = org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType.valueOf(entry.getKey());
org.cloudburstmc.protocol.bedrock.data.SoundEvent soundEvent = org.cloudburstmc.protocol.bedrock.data.SoundEvent.valueOf(node.get("name").asText());
String identifier = node.has("identifier") ? node.get("identifier").asText() : "";
int extraData = node.has("extraData") ? node.get("extraData").intValue() : -1;
org.cloudburstmc.protocol.bedrock.data.SoundEvent soundEvent = org.cloudburstmc.protocol.bedrock.data.SoundEvent.valueOf(node.get("name").getAsString());
String identifier = node.has("identifier") ? node.get("identifier").getAsString() : "";
int extraData = node.has("extraData") ? node.get("extraData").getAsInt() : -1;
transformer = new SoundEventEventTranslator(soundEvent, identifier, extraData);
}
case "playSound" -> {
javaEffect = org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType.valueOf(entry.getKey());
String name = node.get("name").asText();
float volume = node.has("volume") ? node.get("volume").floatValue() : 1.0f;
boolean pitchSub = node.has("pitch_sub") && node.get("pitch_sub").booleanValue();
float pitchMul = node.has("pitch_mul") ? node.get("pitch_mul").floatValue() : 1.0f;
float pitchAdd = node.has("pitch_add") ? node.get("pitch_add").floatValue() : 0.0f;
boolean relative = !node.has("relative") || node.get("relative").booleanValue();
String name = node.get("name").getAsString();
float volume = node.has("volume") ? node.get("volume").getAsFloat() : 1.0f;
boolean pitchSub = node.has("pitch_sub") && node.get("pitch_sub").getAsBoolean();
float pitchMul = node.has("pitch_mul") ? node.get("pitch_mul").getAsFloat() : 1.0f;
float pitchAdd = node.has("pitch_add") ? node.get("pitch_add").getAsFloat() : 0.0f;
boolean relative = !node.has("relative") || node.get("relative").getAsBoolean();
transformer = new PlaySoundEventTranslator(name, volume, pitchSub, pitchMul, pitchAdd, relative);
}
}

View File

@@ -25,15 +25,16 @@
package org.geysermc.geyser.registry.loader;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.registry.type.SoundMapping;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
@@ -42,30 +43,30 @@ import java.util.Map;
public class SoundRegistryLoader implements RegistryLoader<String, Map<String, SoundMapping>> {
@Override
public Map<String, SoundMapping> load(String input) {
JsonNode soundsTree;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input)) {
soundsTree = GeyserImpl.JSON_MAPPER.readTree(stream);
JsonObject soundsJson;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input);
InputStreamReader isr = new InputStreamReader(stream)) {
//noinspection deprecation
soundsJson = new JsonParser().parse(isr).getAsJsonObject();
} catch (IOException e) {
throw new AssertionError("Unable to load sound mappings", e);
}
Map<String, SoundMapping> soundMappings = new HashMap<>();
Iterator<Map.Entry<String, JsonNode>> soundsIterator = soundsTree.fields();
while (soundsIterator.hasNext()) {
Map.Entry<String, JsonNode> next = soundsIterator.next();
JsonNode brMap = next.getValue();
String javaSound = next.getKey();
for (Map.Entry<String, JsonElement> entry : soundsJson.entrySet()) {
JsonObject brMap = entry.getValue().getAsJsonObject();
String javaSound = entry.getKey();
soundMappings.put(javaSound, new SoundMapping(
javaSound,
brMap.has("bedrock_mapping") && brMap.get("bedrock_mapping").isTextual() ? brMap.get("bedrock_mapping").asText() : null,
brMap.has("playsound_mapping") && brMap.get("playsound_mapping").isTextual() ? brMap.get("playsound_mapping").asText() : null,
brMap.has("extra_data") && brMap.get("extra_data").isInt() ? brMap.get("extra_data").asInt() : -1,
brMap.has("identifier") && brMap.get("identifier").isTextual() ? brMap.get("identifier").asText() : null,
brMap.has("level_event") && brMap.get("level_event").isBoolean() && brMap.get("level_event").asBoolean(),
brMap.has("pitch_adjust") && brMap.get("pitch_adjust").isNumber() ? brMap.get("pitch_adjust").floatValue() : 1.0f
brMap.has("bedrock_mapping") ? brMap.get("bedrock_mapping").getAsString() : null,
brMap.has("playsound_mapping") ? brMap.get("playsound_mapping").getAsString() : null,
brMap.has("extra_data") ? brMap.get("extra_data").getAsInt() : -1,
brMap.has("identifier") ? brMap.get("identifier").getAsString() : null,
brMap.has("level_event") && brMap.get("level_event").getAsBoolean(),
brMap.has("pitch_adjust") ? brMap.get("pitch_adjust").getAsFloat() : 1.0f
)
);
}
return soundMappings;
}
}
}

View File

@@ -25,7 +25,8 @@
package org.geysermc.geyser.registry.mappings;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -35,6 +36,7 @@ import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping;
import org.geysermc.geyser.registry.mappings.versions.MappingsReader;
import org.geysermc.geyser.registry.mappings.versions.MappingsReader_v1;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -95,10 +97,10 @@ public class MappingsConfigReader {
}
}
public @Nullable JsonNode getMappingsRoot(Path file) {
JsonNode mappingsRoot;
try {
mappingsRoot = GeyserImpl.JSON_MAPPER.readTree(file.toFile());
public @Nullable JsonObject getMappingsRoot(Path file) {
JsonObject mappingsRoot;
try (FileReader reader = new FileReader(file.toFile())) {
mappingsRoot = (JsonObject) new JsonParser().parse(reader);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to read custom mapping file: " + file, e);
return null;
@@ -112,8 +114,8 @@ public class MappingsConfigReader {
return mappingsRoot;
}
public int getFormatVersion(JsonNode mappingsRoot, Path file) {
int formatVersion = mappingsRoot.get("format_version").asInt();
public int getFormatVersion(JsonObject mappingsRoot, Path file) {
int formatVersion = mappingsRoot.get("format_version").getAsInt();
if (!this.mappingReaders.containsKey(formatVersion)) {
GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " has an unknown format version: " + formatVersion);
return -1;
@@ -122,7 +124,7 @@ public class MappingsConfigReader {
}
public void readItemMappingsFromJson(Path file, BiConsumer<String, CustomItemData> consumer) {
JsonNode mappingsRoot = getMappingsRoot(file);
JsonObject mappingsRoot = getMappingsRoot(file);
if (mappingsRoot == null) {
return;
@@ -138,7 +140,7 @@ public class MappingsConfigReader {
}
public void readBlockMappingsFromJson(Path file, BiConsumer<String, CustomBlockMapping> consumer) {
JsonNode mappingsRoot = getMappingsRoot(file);
JsonObject mappingsRoot = getMappingsRoot(file);
if (mappingsRoot == null) {
return;

View File

@@ -25,7 +25,7 @@
package org.geysermc.geyser.registry.mappings.versions;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonObject;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomRenderOffsets;
@@ -36,14 +36,14 @@ import java.nio.file.Path;
import java.util.function.BiConsumer;
public abstract class MappingsReader {
public abstract void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer);
public abstract void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer);
public abstract void readItemMappings(Path file, JsonObject mappingsRoot, BiConsumer<String, CustomItemData> consumer);
public abstract void readBlockMappings(Path file, JsonObject mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer);
public abstract CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException;
public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException;
public abstract CustomItemData readItemMappingEntry(JsonObject node) throws InvalidCustomMappingsFileException;
public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonObject node) throws InvalidCustomMappingsFileException;
protected @Nullable CustomRenderOffsets fromJsonNode(JsonNode node) {
if (node == null || !node.isObject()) {
protected @Nullable CustomRenderOffsets fromJsonObject(JsonObject node) {
if (node == null) {
return null;
}
@@ -53,9 +53,8 @@ public abstract class MappingsReader {
);
}
protected CustomRenderOffsets.@Nullable Hand getHandOffsets(JsonNode node, String hand) {
JsonNode tmpNode = node.get(hand);
if (tmpNode == null || !tmpNode.isObject()) {
protected CustomRenderOffsets.@Nullable Hand getHandOffsets(JsonObject node, String hand) {
if (!(node.get(hand) instanceof JsonObject tmpNode)) {
return null;
}
@@ -65,9 +64,8 @@ public abstract class MappingsReader {
);
}
protected CustomRenderOffsets.@Nullable Offset getPerspectiveOffsets(JsonNode node, String perspective) {
JsonNode tmpNode = node.get(perspective);
if (tmpNode == null || !tmpNode.isObject()) {
protected CustomRenderOffsets.@Nullable Offset getPerspectiveOffsets(JsonObject node, String perspective) {
if (!(node.get(perspective) instanceof JsonObject tmpNode)) {
return null;
}
@@ -78,9 +76,8 @@ public abstract class MappingsReader {
);
}
protected CustomRenderOffsets.@Nullable OffsetXYZ getOffsetXYZ(JsonNode node, String offsetType) {
JsonNode tmpNode = node.get(offsetType);
if (tmpNode == null || !tmpNode.isObject()) {
protected CustomRenderOffsets.@Nullable OffsetXYZ getOffsetXYZ(JsonObject node, String offsetType) {
if (!(node.get(offsetType) instanceof JsonObject tmpNode)) {
return null;
}
@@ -89,9 +86,9 @@ public abstract class MappingsReader {
}
return new CustomRenderOffsets.OffsetXYZ(
tmpNode.get("x").floatValue(),
tmpNode.get("y").floatValue(),
tmpNode.get("z").floatValue()
tmpNode.get("x").getAsFloat(),
tmpNode.get("y").getAsFloat(),
tmpNode.get("z").getAsFloat()
);
}
}

View File

@@ -25,8 +25,10 @@
package org.geysermc.geyser.registry.mappings.versions;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -34,9 +36,14 @@ import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.*;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.component.GeometryComponent;
import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions.BlockFilterType;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face;
import org.geysermc.geyser.api.block.custom.component.TransformationComponent;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.api.util.CreativeCategory;
@@ -57,7 +64,13 @@ import org.geysermc.geyser.util.MathUtils;
import org.geysermc.geyser.util.MinecraftKey;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -68,7 +81,7 @@ import java.util.stream.Collectors;
*/
public class MappingsReader_v1 extends MappingsReader {
@Override
public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
public void readItemMappings(Path file, JsonObject mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
this.readItemMappingsV1(file, mappingsRoot, consumer);
}
@@ -76,24 +89,24 @@ public class MappingsReader_v1 extends MappingsReader {
* Read item block from a JSON node
*
* @param file The path to the file
* @param mappingsRoot The {@link JsonNode} containing the mappings
* @param mappingsRoot The {@link JsonObject} containing the mappings
* @param consumer The consumer to accept the mappings
* @see #readBlockMappingsV1(Path, JsonNode, BiConsumer)
* @see #readBlockMappingsV1(Path, JsonObject, BiConsumer)
*/
@Override
public void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer) {
public void readBlockMappings(Path file, JsonObject mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer) {
this.readBlockMappingsV1(file, mappingsRoot, consumer);
}
public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
JsonNode itemsNode = mappingsRoot.get("items");
public void readItemMappingsV1(Path file, JsonObject mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
JsonObject itemsNode = mappingsRoot.getAsJsonObject("items");
if (itemsNode != null && itemsNode.isObject()) {
itemsNode.fields().forEachRemaining(entry -> {
if (entry.getValue().isArray()) {
entry.getValue().forEach(data -> {
if (itemsNode != null) {
itemsNode.entrySet().forEach(entry -> {
if (entry.getValue() instanceof JsonArray array) {
array.forEach(data -> {
try {
CustomItemData customItemData = this.readItemMappingEntry(data);
CustomItemData customItemData = this.readItemMappingEntry((JsonObject) data);
consumer.accept(entry.getKey(), customItemData);
} catch (InvalidCustomMappingsFileException e) {
GeyserImpl.getInstance().getLogger().error("Error in registering items for custom mapping file: " + file.toString(), e);
@@ -108,19 +121,17 @@ public class MappingsReader_v1 extends MappingsReader {
* Read block mappings from a JSON node
*
* @param file The path to the file
* @param mappingsRoot The {@link JsonNode} containing the mappings
* @param mappingsRoot The {@link JsonObject} containing the mappings
* @param consumer The consumer to accept the mappings
* @see #readBlockMappings(Path, JsonNode, BiConsumer)
* @see #readBlockMappings(Path, JsonObject, BiConsumer)
*/
public void readBlockMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer) {
JsonNode blocksNode = mappingsRoot.get("blocks");
if (blocksNode != null && blocksNode.isObject()) {
blocksNode.fields().forEachRemaining(entry -> {
if (entry.getValue().isObject()) {
public void readBlockMappingsV1(Path file, JsonObject mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer) {
if (mappingsRoot.get("blocks") instanceof JsonObject blocksNode) {
blocksNode.entrySet().forEach(entry -> {
if (entry.getValue() instanceof JsonObject jsonObject) {
try {
String identifier = MinecraftKey.key(entry.getKey()).asString();
CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, entry.getValue());
CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, jsonObject);
consumer.accept(identifier, customBlockMapping);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Error in registering blocks for custom mapping file: " + file.toString());
@@ -131,85 +142,85 @@ public class MappingsReader_v1 extends MappingsReader {
}
}
private CustomItemOptions readItemCustomItemOptions(JsonNode node) {
private CustomItemOptions readItemCustomItemOptions(JsonObject node) {
CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder();
JsonNode customModelData = node.get("custom_model_data");
if (customModelData != null && customModelData.isInt()) {
customItemOptions.customModelData(customModelData.asInt());
JsonElement customModelData = node.get("custom_model_data");
if (customModelData != null && customModelData.isJsonPrimitive()) {
customItemOptions.customModelData(customModelData.getAsInt());
}
JsonNode damagePredicate = node.get("damage_predicate");
if (damagePredicate != null && damagePredicate.isInt()) {
customItemOptions.damagePredicate(damagePredicate.asInt());
JsonElement damagePredicate = node.get("damage_predicate");
if (damagePredicate != null && damagePredicate.isJsonPrimitive()) {
customItemOptions.damagePredicate(damagePredicate.getAsInt());
}
JsonNode unbreakable = node.get("unbreakable");
if (unbreakable != null && unbreakable.isBoolean()) {
customItemOptions.unbreakable(unbreakable.asBoolean());
JsonElement unbreakable = node.get("unbreakable");
if (unbreakable != null && unbreakable.isJsonPrimitive()) {
customItemOptions.unbreakable(unbreakable.getAsBoolean());
}
JsonNode defaultItem = node.get("default");
if (defaultItem != null && defaultItem.isBoolean()) {
customItemOptions.defaultItem(defaultItem.asBoolean());
JsonElement defaultItem = node.get("default");
if (defaultItem != null && defaultItem.isJsonPrimitive()) {
customItemOptions.defaultItem(defaultItem.getAsBoolean());
}
return customItemOptions.build();
}
@Override
public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null || !node.isObject()) {
public CustomItemData readItemMappingEntry(JsonObject node) throws InvalidCustomMappingsFileException {
if (node == null) {
throw new InvalidCustomMappingsFileException("Invalid item mappings entry");
}
JsonNode name = node.get("name");
if (name == null || !name.isTextual() || name.asText().isEmpty()) {
JsonElement name = node.get("name");
if (name == null || !name.isJsonPrimitive() || name.getAsString().isEmpty()) {
throw new InvalidCustomMappingsFileException("An item entry has no name");
}
CustomItemData.Builder customItemData = CustomItemData.builder()
.name(name.asText())
.name(name.getAsString())
.customItemOptions(this.readItemCustomItemOptions(node));
//The next entries are optional
if (node.has("display_name")) {
customItemData.displayName(node.get("display_name").asText());
customItemData.displayName(node.get("display_name").getAsString());
}
if (node.has("icon")) {
customItemData.icon(node.get("icon").asText());
customItemData.icon(node.get("icon").getAsString());
}
if (node.has("creative_category")) {
customItemData.creativeCategory(node.get("creative_category").asInt());
customItemData.creativeCategory(node.get("creative_category").getAsInt());
}
if (node.has("creative_group")) {
customItemData.creativeGroup(node.get("creative_group").asText());
customItemData.creativeGroup(node.get("creative_group").getAsString());
}
if (node.has("allow_offhand")) {
customItemData.allowOffhand(node.get("allow_offhand").asBoolean());
customItemData.allowOffhand(node.get("allow_offhand").getAsBoolean());
}
if (node.has("display_handheld")) {
customItemData.displayHandheld(node.get("display_handheld").asBoolean());
customItemData.displayHandheld(node.get("display_handheld").getAsBoolean());
}
if (node.has("texture_size")) {
customItemData.textureSize(node.get("texture_size").asInt());
customItemData.textureSize(node.get("texture_size").getAsInt());
}
if (node.has("render_offsets")) {
JsonNode tmpNode = node.get("render_offsets");
JsonObject tmpNode = node.getAsJsonObject("render_offsets");
customItemData.renderOffsets(fromJsonNode(tmpNode));
customItemData.renderOffsets(fromJsonObject(tmpNode));
}
if (node.get("tags") instanceof ArrayNode tags) {
if (node.get("tags") instanceof JsonArray tags) {
Set<String> tagsSet = new ObjectOpenHashSet<>();
tags.forEach(tag -> tagsSet.add(tag.asText()));
tags.forEach(tag -> tagsSet.add(tag.getAsString()));
customItemData.tags(tagsSet);
}
@@ -220,26 +231,26 @@ public class MappingsReader_v1 extends MappingsReader {
* Read a block mapping entry from a JSON node and Java identifier
*
* @param identifier The Java identifier of the block
* @param node The {@link JsonNode} containing the block mapping entry
* @param node The {@link JsonObject} containing the block mapping entry
* @return The {@link CustomBlockMapping} record to be read by {@link org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator}
* @throws InvalidCustomMappingsFileException If the JSON node is invalid
*/
@Override
public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null || !node.isObject()) {
public CustomBlockMapping readBlockMappingEntry(String identifier, JsonObject node) throws InvalidCustomMappingsFileException {
if (node == null) {
throw new InvalidCustomMappingsFileException("Invalid block mappings entry:" + node);
}
String name = node.get("name").asText();
String name = node.get("name").getAsString();
if (name == null || name.isEmpty()) {
throw new InvalidCustomMappingsFileException("A block entry has no name");
}
boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").asBoolean();
boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").getAsBoolean();
CreativeCategory creativeCategory = CreativeCategory.NONE;
if (node.has("creative_category")) {
String categoryName = node.get("creative_category").asText();
String categoryName = node.get("creative_category").getAsString();
try {
creativeCategory = CreativeCategory.valueOf(categoryName.toUpperCase());
} catch (IllegalArgumentException e) {
@@ -249,11 +260,11 @@ public class MappingsReader_v1 extends MappingsReader {
String creativeGroup = "";
if (node.has("creative_group")) {
creativeGroup = node.get("creative_group").asText();
creativeGroup = node.get("creative_group").getAsString();
}
// If this is true, we will only register the states the user has specified rather than all the possible block states
boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean();
boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").getAsBoolean();
// Create the data for the overall block
CustomBlockData.Builder customBlockDataBuilder = new GeyserCustomBlockData.Builder()
@@ -273,12 +284,9 @@ public class MappingsReader_v1 extends MappingsReader {
Map<String, CustomBlockComponentsMapping> componentsMap = new LinkedHashMap<>();
JsonNode stateOverrides = node.get("state_overrides");
if (stateOverrides != null && stateOverrides.isObject()) {
if (node.get("state_overrides") instanceof JsonObject stateOverrides) {
// Load components for specific Java block states
Iterator<Map.Entry<String, JsonNode>> fields = stateOverrides.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> overrideEntry = fields.next();
for (Map.Entry<String, JsonElement> overrideEntry : stateOverrides.entrySet()) {
String state = identifier + "[" + overrideEntry.getKey() + "]";
if (!BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.get().containsKey(state)) {
throw new InvalidCustomMappingsFileException("Unknown Java block state: " + state + " for state_overrides.");
@@ -358,12 +366,12 @@ public class MappingsReader_v1 extends MappingsReader {
/**
* Creates a {@link CustomBlockComponents} object for the passed state override or base block node, Java block state identifier, and custom block name
*
* @param node the state override or base block {@link JsonNode}
* @param element the state override or base block {@link JsonObject}
* @param stateKey the Java block state identifier
* @param name the name of the custom block
* @return the {@link CustomBlockComponents} object
*/
private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode node, String stateKey, String name) {
private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonElement element, String stateKey, String name) {
// This is needed to find the correct selection box for the given block
int id = BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.getOrDefault(stateKey, -1);
BoxComponent boxComponent = createBoxComponent(id);
@@ -372,7 +380,7 @@ public class MappingsReader_v1 extends MappingsReader {
.collisionBox(boxComponent)
.selectionBox(boxComponent);
if (node == null) {
if (!(element instanceof JsonObject node)) {
// No other components were defined
return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent);
}
@@ -394,28 +402,28 @@ public class MappingsReader_v1 extends MappingsReader {
// We set this to max value by default so that we may dictate the correct destroy time ourselves
float destructibleByMining = Float.MAX_VALUE;
if (node.has("destructible_by_mining")) {
destructibleByMining = node.get("destructible_by_mining").floatValue();
destructibleByMining = node.get("destructible_by_mining").getAsFloat();
}
builder.destructibleByMining(destructibleByMining);
if (node.has("geometry")) {
if (node.get("geometry").isTextual()) {
if (node.get("geometry").isJsonPrimitive()) {
builder.geometry(new GeyserGeometryComponent.Builder()
.identifier(node.get("geometry").asText())
.identifier(node.get("geometry").getAsString())
.build());
} else {
JsonNode geometry = node.get("geometry");
JsonObject geometry = node.getAsJsonObject("geometry");
GeometryComponent.Builder geometryBuilder = new GeyserGeometryComponent.Builder();
if (geometry.has("identifier")) {
geometryBuilder.identifier(geometry.get("identifier").asText());
geometryBuilder.identifier(geometry.get("identifier").getAsString());
}
if (geometry.has("bone_visibility")) {
JsonNode boneVisibility = geometry.get("bone_visibility");
if (boneVisibility.isObject()) {
if (geometry.get("bone_visibility") instanceof JsonObject boneVisibility) {
Map<String, String> boneVisibilityMap = new Object2ObjectOpenHashMap<>();
boneVisibility.fields().forEachRemaining(entry -> {
boneVisibility.entrySet().forEach(entry -> {
String key = entry.getKey();
String value = entry.getValue().isBoolean() ? (entry.getValue().asBoolean() ? "1" : "0") : entry.getValue().asText();
String value = entry.getValue() instanceof JsonPrimitive primitive && primitive.isBoolean()
? (entry.getValue().getAsBoolean() ? "1" : "0") : entry.getValue().getAsString();
boneVisibilityMap.put(key, value);
});
geometryBuilder.boneVisibility(boneVisibilityMap);
@@ -427,30 +435,30 @@ public class MappingsReader_v1 extends MappingsReader {
String displayName = name;
if (node.has("display_name")) {
displayName = node.get("display_name").asText();
displayName = node.get("display_name").getAsString();
}
builder.displayName(displayName);
if (node.has("friction")) {
builder.friction(node.get("friction").floatValue());
builder.friction(node.get("friction").getAsFloat());
}
if (node.has("light_emission")) {
builder.lightEmission(node.get("light_emission").asInt());
builder.lightEmission(node.get("light_emission").getAsInt());
}
if (node.has("light_dampening")) {
builder.lightDampening(node.get("light_dampening").asInt());
builder.lightDampening(node.get("light_dampening").getAsInt());
}
boolean placeAir = true;
if (node.has("place_air")) {
placeAir = node.get("place_air").asBoolean();
placeAir = node.get("place_air").getAsBoolean();
}
builder.placeAir(placeAir);
if (node.has("transformation")) {
JsonNode transformation = node.get("transformation");
JsonObject transformation = node.getAsJsonObject("transformation");
int rotationX = 0;
int rotationY = 0;
@@ -463,22 +471,22 @@ public class MappingsReader_v1 extends MappingsReader {
float transformZ = 0;
if (transformation.has("rotation")) {
JsonNode rotation = transformation.get("rotation");
rotationX = rotation.get(0).asInt();
rotationY = rotation.get(1).asInt();
rotationZ = rotation.get(2).asInt();
JsonArray rotation = transformation.getAsJsonArray("rotation");
rotationX = rotation.get(0).getAsInt();
rotationY = rotation.get(1).getAsInt();
rotationZ = rotation.get(2).getAsInt();
}
if (transformation.has("scale")) {
JsonNode scale = transformation.get("scale");
scaleX = scale.get(0).floatValue();
scaleY = scale.get(1).floatValue();
scaleZ = scale.get(2).floatValue();
JsonArray scale = transformation.getAsJsonArray("scale");
scaleX = scale.get(0).getAsFloat();
scaleY = scale.get(1).getAsFloat();
scaleZ = scale.get(2).getAsFloat();
}
if (transformation.has("translation")) {
JsonNode translation = transformation.get("translation");
transformX = translation.get(0).floatValue();
transformY = translation.get(1).floatValue();
transformZ = translation.get(2).floatValue();
JsonArray translation = transformation.getAsJsonArray("translation");
transformX = translation.get(0).getAsFloat();
transformY = translation.get(1).getAsFloat();
transformZ = translation.get(2).getAsFloat();
}
builder.transformation(new TransformationComponent(rotationX, rotationY, rotationZ, scaleX, scaleY, scaleZ, transformX, transformY, transformZ));
}
@@ -490,12 +498,10 @@ public class MappingsReader_v1 extends MappingsReader {
}
if (node.has("material_instances")) {
JsonNode materialInstances = node.get("material_instances");
if (materialInstances.isObject()) {
materialInstances.fields().forEachRemaining(entry -> {
if (node.get("material_instances") instanceof JsonObject materialInstances) {
materialInstances.entrySet().forEach(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
if (value.isObject()) {
if (entry.getValue() instanceof JsonObject value) {
MaterialInstance materialInstance = createMaterialInstanceComponent(value);
builder.materialInstance(key, materialInstance);
}
@@ -503,16 +509,10 @@ public class MappingsReader_v1 extends MappingsReader {
}
}
if (node.has("placement_filter")) {
JsonNode placementFilter = node.get("placement_filter");
if (placementFilter.isObject()) {
if (placementFilter.has("conditions")) {
JsonNode conditions = placementFilter.get("conditions");
if (conditions.isArray()) {
List<PlacementConditions> filter = createPlacementFilterComponent(conditions);
builder.placementFilter(filter);
}
}
if (node.get("placement_filter") instanceof JsonObject placementFilter) {
if (placementFilter.get("conditions") instanceof JsonArray conditions) {
List<PlacementConditions> filter = createPlacementFilterComponent(conditions);
builder.placementFilter(filter);
}
}
@@ -521,9 +521,9 @@ public class MappingsReader_v1 extends MappingsReader {
// Ideally we could programmatically extract the tags here https://wiki.bedrock.dev/blocks/block-tags.html
// This would let us automatically apply the correct vanilla tags to blocks
// However, its worth noting that vanilla tools do not currently honor these tags anyway
if (node.get("tags") instanceof ArrayNode tags) {
if (node.get("tags") instanceof JsonArray tags) {
Set<String> tagsSet = new ObjectOpenHashSet<>();
tags.forEach(tag -> tagsSet.add(tag.asText()));
tags.forEach(tag -> tagsSet.add(tag.getAsString()));
builder.tags(tagsSet);
}
@@ -613,21 +613,21 @@ public class MappingsReader_v1 extends MappingsReader {
/**
* Creates a {@link BoxComponent} from a JSON Node
*
* @param node the JSON node
* @param element the JSON node
* @return the {@link BoxComponent}
*/
private @Nullable BoxComponent createBoxComponent(JsonNode node) {
if (node != null && node.isObject()) {
private @Nullable BoxComponent createBoxComponent(JsonElement element) {
if (element instanceof JsonObject node) {
if (node.has("origin") && node.has("size")) {
JsonNode origin = node.get("origin");
float originX = origin.get(0).floatValue();
float originY = origin.get(1).floatValue();
float originZ = origin.get(2).floatValue();
JsonArray origin = node.getAsJsonArray("origin");
float originX = origin.get(0).getAsFloat();
float originY = origin.get(1).getAsFloat();
float originZ = origin.get(2).getAsFloat();
JsonNode size = node.get("size");
float sizeX = size.get(0).floatValue();
float sizeY = size.get(1).floatValue();
float sizeZ = size.get(2).floatValue();
JsonArray size = node.getAsJsonArray("size");
float sizeX = size.get(0).getAsFloat();
float sizeY = size.get(1).getAsFloat();
float sizeZ = size.get(2).getAsFloat();
return new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ);
}
@@ -642,26 +642,26 @@ public class MappingsReader_v1 extends MappingsReader {
* @param node the material instance node
* @return the {@link MaterialInstance}
*/
private MaterialInstance createMaterialInstanceComponent(JsonNode node) {
private MaterialInstance createMaterialInstanceComponent(JsonObject node) {
// Set default values, and use what the user provides if they have provided something
String texture = null;
if (node.has("texture")) {
texture = node.get("texture").asText();
texture = node.get("texture").getAsString();
}
String renderMethod = "opaque";
if (node.has("render_method")) {
renderMethod = node.get("render_method").asText();
renderMethod = node.get("render_method").getAsString();
}
boolean faceDimming = true;
if (node.has("face_dimming")) {
faceDimming = node.get("face_dimming").asBoolean();
faceDimming = node.get("face_dimming").getAsBoolean();
}
boolean ambientOcclusion = true;
if (node.has("ambient_occlusion")) {
ambientOcclusion = node.get("ambient_occlusion").asBoolean();
ambientOcclusion = node.get("ambient_occlusion").getAsBoolean();
}
return new GeyserMaterialInstance.Builder()
@@ -678,32 +678,33 @@ public class MappingsReader_v1 extends MappingsReader {
* @param node the conditions node
* @return the list of {@link PlacementConditions}
*/
private List<PlacementConditions> createPlacementFilterComponent(JsonNode node) {
private List<PlacementConditions> createPlacementFilterComponent(JsonArray node) {
List<PlacementConditions> conditions = new ArrayList<>();
// The structure of the placement filter component is the most complex of the current components
// Each condition effectively separated into two arrays: one of allowed faces, and one of blocks/block Molang queries
node.forEach(condition -> {
node.forEach(json -> {
if (!(json instanceof JsonObject condition)) {
return;
}
Set<Face> faces = EnumSet.noneOf(Face.class);
if (condition.has("allowed_faces")) {
JsonNode allowedFaces = condition.get("allowed_faces");
if (allowedFaces.isArray()) {
allowedFaces.forEach(face -> faces.add(Face.valueOf(face.asText().toUpperCase())));
if (condition.get("allowed_faces") instanceof JsonArray allowedFaces) {
allowedFaces.forEach(face -> faces.add(Face.valueOf(face.getAsString().toUpperCase())));
}
}
LinkedHashMap<String, BlockFilterType> blockFilters = new LinkedHashMap<>();
if (condition.has("block_filter")) {
JsonNode blockFilter = condition.get("block_filter");
if (blockFilter.isArray()) {
if (condition.get("block_filter") instanceof JsonArray blockFilter) {
blockFilter.forEach(filter -> {
if (filter.isObject()) {
if (filter.has("tags")) {
JsonNode tags = filter.get("tags");
blockFilters.put(tags.asText(), BlockFilterType.TAG);
if (filter instanceof JsonObject jsonObject) {
if (jsonObject.has("tags")) {
JsonElement tags = jsonObject.get("tags");
blockFilters.put(tags.getAsString(), BlockFilterType.TAG);
}
} else if (filter.isTextual()) {
blockFilters.put(filter.asText(), BlockFilterType.BLOCK);
} else if (filter instanceof JsonPrimitive primitive && primitive.isString()) {
blockFilters.put(filter.getAsString(), BlockFilterType.BLOCK);
}
});
}

View File

@@ -25,11 +25,12 @@
package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
@@ -65,6 +66,7 @@ import org.geysermc.geyser.registry.populator.conversion.Conversion827_819;
import org.geysermc.geyser.registry.populator.conversion.Conversion844_827;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.GeyserBedrockBlock;
import org.geysermc.geyser.util.JsonUtils;
import java.io.DataInputStream;
import java.io.InputStream;
@@ -441,21 +443,21 @@ public final class BlockRegistryPopulator {
BLOCKS_NBT = blocksNbt;
JAVA_BLOCKS_SIZE = blocksNbt.size();
JsonNode blockInteractionsJson;
JsonObject blockInteractionsJson;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("mappings/interactions.json")) {
blockInteractionsJson = GeyserImpl.JSON_MAPPER.readTree(stream);
blockInteractionsJson = JsonUtils.fromJson(stream);
} catch (Exception e) {
throw new AssertionError("Unable to load Java block interaction mappings", e);
}
BlockRegistries.INTERACTIVE.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("always_consumes")));
BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("requires_may_build")));
BlockRegistries.INTERACTIVE.set(toBlockStateSet(blockInteractionsJson.getAsJsonArray("always_consumes")));
BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet(blockInteractionsJson.getAsJsonArray("requires_may_build")));
}
private static BitSet toBlockStateSet(ArrayNode node) {
private static BitSet toBlockStateSet(JsonArray node) {
BitSet blockStateSet = new BitSet(node.size());
for (JsonNode javaIdentifier : node) {
blockStateSet.set(BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.get().getInt(javaIdentifier.textValue()));
for (JsonElement javaIdentifier : node) {
blockStateSet.set(BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.get().getInt(javaIdentifier.getAsString()));
}
return blockStateSet;
}

View File

@@ -25,7 +25,9 @@
package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
@@ -40,6 +42,7 @@ import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.GeyserBedrockBlock;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.geyser.registry.type.GeyserMappingItem;
import java.io.ByteArrayInputStream;
@@ -62,20 +65,21 @@ public class CreativeItemRegistryPopulator {
static List<CreativeItemGroup> readCreativeItemGroups(ItemRegistryPopulator.PaletteVersion palette, List<CreativeItemData> creativeItemData) {
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
JsonNode creativeItemEntries;
JsonArray creativeItemEntries;
try (InputStream stream = bootstrap.getResourceOrThrow(String.format("bedrock/creative_items.%s.json", palette.version()))) {
creativeItemEntries = GeyserImpl.JSON_MAPPER.readTree(stream).get("groups");
creativeItemEntries = JsonUtils.fromJson(stream).getAsJsonArray("groups");
} catch (Exception e) {
throw new AssertionError("Unable to load creative item groups", e);
}
List<CreativeItemGroup> creativeItemGroups = new ArrayList<>();
for (JsonNode creativeItemEntry : creativeItemEntries) {
CreativeItemCategory category = CreativeItemCategory.valueOf(creativeItemEntry.get("category").asText().toUpperCase(Locale.ROOT));
String name = creativeItemEntry.get("name").asText();
for (JsonElement creativeItemEntry : creativeItemEntries) {
JsonObject creativeItemEntryObject = creativeItemEntry.getAsJsonObject();
CreativeItemCategory category = CreativeItemCategory.valueOf(creativeItemEntryObject.get("category").getAsString().toUpperCase(Locale.ROOT));
String name = creativeItemEntryObject.get("name").getAsString();
JsonNode icon = creativeItemEntry.get("icon");
String identifier = icon.get("id").asText();
JsonElement icon = creativeItemEntryObject.get("icon");
String identifier = icon.getAsJsonObject().get("id").getAsString();
ItemData itemData;
if (identifier.equals("minecraft:air")) {
@@ -98,32 +102,33 @@ public class CreativeItemRegistryPopulator {
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
// Load creative items
JsonNode creativeItemEntries;
JsonArray creativeItemEntries;
try (InputStream stream = bootstrap.getResourceOrThrow(String.format("bedrock/creative_items.%s.json", palette.version()))) {
creativeItemEntries = GeyserImpl.JSON_MAPPER.readTree(stream).get("items");
creativeItemEntries = JsonUtils.fromJson(stream).getAsJsonArray("items");
} catch (Exception e) {
throw new AssertionError("Unable to load creative items", e);
}
BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.protocolVersion());
for (JsonNode itemNode : creativeItemEntries) {
ItemData.Builder itemBuilder = createItemData(itemNode, items, blockMappings, definitions);
for (JsonElement itemNode : creativeItemEntries) {
ItemData.Builder itemBuilder = createItemData((JsonObject) itemNode, items, blockMappings, definitions);
if (itemBuilder == null) {
continue;
}
int groupId = itemNode.get("groupId") != null ? itemNode.get("groupId").asInt() : 0;
var groupIdElement = itemNode.getAsJsonObject().get("groupId");
int groupId = groupIdElement != null ? groupIdElement.getAsInt() : 0;
itemConsumer.accept(itemBuilder, groupId);
}
}
private static ItemData.@Nullable Builder createItemData(JsonNode itemNode, Map<String, GeyserMappingItem> items, BlockMappings blockMappings, Map<String, ItemDefinition> definitions) {
private static ItemData.@Nullable Builder createItemData(JsonObject itemNode, Map<String, GeyserMappingItem> items, BlockMappings blockMappings, Map<String, ItemDefinition> definitions) {
int count = 1;
int damage = 0;
NbtMap tag = null;
String identifier = itemNode.get("id").textValue();
String identifier = itemNode.get("id").getAsString();
for (BiPredicate<String, Integer> predicate : JAVA_ONLY_ITEM_FILTER) {
if (predicate.test(identifier, damage)) {
return null;
@@ -150,20 +155,20 @@ public class CreativeItemRegistryPopulator {
// }
}
JsonNode damageNode = itemNode.get("damage");
JsonElement damageNode = itemNode.get("damage");
if (damageNode != null) {
damage = damageNode.asInt();
damage = damageNode.getAsInt();
}
JsonNode countNode = itemNode.get("count");
JsonElement countNode = itemNode.get("count");
if (countNode != null) {
count = countNode.asInt();
count = countNode.getAsInt();
}
GeyserBedrockBlock blockDefinition = null;
JsonNode blockStateNode;
JsonElement blockStateNode;
if ((blockStateNode = itemNode.get("block_state_b64")) != null) {
byte[] bytes = Base64.getDecoder().decode(blockStateNode.asText());
byte[] bytes = Base64.getDecoder().decode(blockStateNode.getAsString());
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
try {
NbtMap stateTag = (NbtMap) NbtUtils.createReaderLE(bais).readTag();
@@ -182,9 +187,9 @@ public class CreativeItemRegistryPopulator {
}
}
JsonNode nbtNode = itemNode.get("nbt_b64");
JsonElement nbtNode = itemNode.get("nbt_b64");
if (nbtNode != null) {
byte[] bytes = Base64.getDecoder().decode(nbtNode.asText());
byte[] bytes = Base64.getDecoder().decode(nbtNode.getAsString());
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
try {
tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag();

View File

@@ -104,7 +104,7 @@ public class CustomBlockRegistryPopulator {
* @param stage the stage to populate
*/
public static void populate(Stage stage) {
if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
if (!GeyserImpl.getInstance().config().gameplay().enableCustomContent()) {
return;
}

View File

@@ -59,7 +59,7 @@ public class CustomSkullRegistryPopulator {
SkullResourcePackManager.SKULL_SKINS.clear(); // Remove skins after reloading
BlockRegistries.CUSTOM_SKULLS.set(Object2ObjectMaps.emptyMap());
if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
if (!GeyserImpl.getInstance().config().gameplay().enableCustomContent()) {
return;
}

View File

@@ -25,9 +25,9 @@
package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gson.reflect.TypeToken;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -82,8 +82,10 @@ import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.registry.type.NonVanillaItemRegistration;
import org.geysermc.geyser.registry.type.PaletteItem;
import org.geysermc.geyser.util.JsonUtils;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -201,17 +203,17 @@ public class ItemRegistryPopulator {
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
TypeReference<Map<String, GeyserMappingItem>> mappingItemsType = new TypeReference<>() { };
Type mappingItemsType = new TypeToken<Map<String, GeyserMappingItem>>() { }.getType();
Map<String, GeyserMappingItem> items;
try (InputStream stream = bootstrap.getResourceOrThrow("mappings/items.json")) {
// Load item mappings from Java Edition to Bedrock Edition
items = GeyserImpl.JSON_MAPPER.readValue(stream, mappingItemsType);
items = JsonUtils.fromJson(stream, mappingItemsType);
} catch (Exception e) {
throw new AssertionError("Unable to load Java runtime item IDs", e);
}
boolean customItemsAllowed = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems();
boolean customItemsAllowed = GeyserImpl.getInstance().config().gameplay().enableCustomContent();
// List values here is important compared to HashSet - we need to preserve the order of what's given to us
// (as of 1.19.2 Java) to replicate some edge cases in Java predicate behavior where it checks from the bottom
@@ -228,11 +230,11 @@ public class ItemRegistryPopulator {
/* Load item palette */
for (PaletteVersion palette : paletteVersions) {
TypeReference<List<PaletteItem>> paletteEntriesType = new TypeReference<>() {};
Type paletteEntriesType = new TypeToken<List<PaletteItem>>() { }.getType();
List<PaletteItem> itemEntries;
try (InputStream stream = bootstrap.getResourceOrThrow(String.format("bedrock/runtime_item_states.%s.json", palette.version()))) {
itemEntries = GeyserImpl.JSON_MAPPER.readValue(stream, paletteEntriesType);
itemEntries = JsonUtils.fromJson(stream, paletteEntriesType);
} catch (Exception e) {
throw new AssertionError("Unable to load Bedrock runtime item IDs", e);
}

View File

@@ -25,7 +25,7 @@
package org.geysermc.geyser.registry.type;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -43,13 +43,13 @@ import lombok.With;
@NoArgsConstructor
@AllArgsConstructor
public class GeyserMappingItem {
@JsonProperty("bedrock_identifier") String bedrockIdentifier;
@JsonProperty("bedrock_data") int bedrockData;
@SerializedName("bedrock_identifier") String bedrockIdentifier;
@SerializedName("bedrock_data") int bedrockData;
Integer firstBlockRuntimeId;
Integer lastBlockRuntimeId;
@JsonProperty("tool_type") String toolType;
@JsonProperty("armor_type") String armorType;
@JsonProperty("protection_value") int protectionValue;
@JsonProperty("is_edible") boolean edible = false;
@JsonProperty("is_entity_placer") boolean entityPlacer = false;
@SerializedName("tool_type") String toolType;
@SerializedName("armor_type") String armorType;
@SerializedName("protection_value") int protectionValue;
@SerializedName("is_edible") boolean edible = false;
@SerializedName("is_entity_placer") boolean entityPlacer = false;
}

View File

@@ -80,7 +80,9 @@ import static org.geysermc.geyser.scoreboard.UpdateType.REMOVE;
*/
public final class Scoreboard {
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(
System.getProperty("Geyser.AddTeamSuggestions", String.valueOf(GeyserImpl.getInstance().config().advanced().addTeamSuggestions()))
);
private final GeyserSession session;
private final GeyserLogger logger;

View File

@@ -28,7 +28,7 @@ package org.geysermc.geyser.scoreboard;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.text.GeyserLocale;
@@ -46,9 +46,9 @@ public final class ScoreboardUpdater extends Thread {
private static final boolean DEBUG_ENABLED;
static {
GeyserConfiguration config = GeyserImpl.getInstance().getConfig();
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
DEBUG_ENABLED = config.isDebugMode();
GeyserConfig config = GeyserImpl.getInstance().config();
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.advanced().scoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
DEBUG_ENABLED = config.debugMode();
}
private final GeyserImpl geyser = GeyserImpl.getInstance();

View File

@@ -25,7 +25,6 @@
package org.geysermc.geyser.session;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.netty.channel.Channel;
import io.netty.channel.EventLoop;
@@ -125,8 +124,7 @@ import org.geysermc.geyser.api.skin.SkinData;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.GeyserEntityData;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
@@ -256,8 +254,6 @@ import java.util.concurrent.atomic.AtomicInteger;
@Getter
public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private static final Gson GSON = new Gson();
private final GeyserImpl geyser;
private final UpstreamSession upstream;
private DownstreamSession downstream;
@@ -715,7 +711,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* A cache of IDs from ClientboundKeepAlivePackets that have been sent to the Bedrock client, but haven't been returned to the server.
* Only used if {@link GeyserConfiguration#isForwardPlayerPing()} is enabled.
* Only used if {@link GeyserConfig.GameplayConfig#forwardPlayerPing()} is enabled.
*/
private final Queue<Long> keepAliveCache = new ConcurrentLinkedQueue<>();
@@ -753,6 +749,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Accessors(fluent = true)
private boolean hasAcceptedCodeOfConduct = false;
@Accessors(fluent = true)
@Setter
private boolean integratedPackActive = false;
private final Set<InputLocksFlag> inputLocksSet = EnumSet.noneOf(InputLocksFlag.class);
private boolean inputLockDirty;
@@ -799,12 +799,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.spawned = false;
this.loggedIn = false;
if (geyser.getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.NO_EMOTES) {
this.emotes = new HashSet<>();
geyser.getSessionManager().getSessions().values().forEach(player -> this.emotes.addAll(player.getEmotes()));
} else {
this.emotes = null;
}
this.emotes = new HashSet<>();
geyser.getSessionManager().getSessions().values().forEach(player -> this.emotes.addAll(player.getEmotes()));
this.remoteServer = geyser.defaultRemoteServer();
}
@@ -870,7 +866,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
upstream.sendPacket(playStatusPacket);
SetCommandsEnabledPacket setCommandsEnabledPacket = new SetCommandsEnabledPacket();
setCommandsEnabledPacket.setCommandsEnabled(!geyser.getConfig().isXboxAchievementsEnabled());
setCommandsEnabledPacket.setCommandsEnabled(!geyser.config().gameplay().xboxAchievementsEnabled());
upstream.sendPacket(setCommandsEnabledPacket);
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
@@ -929,7 +925,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
MinecraftProfile mcProfile;
MinecraftToken mcToken;
try {
JsonObject parsedAuthChain = GSON.fromJson(authChain, JsonObject.class);
JsonObject parsedAuthChain = GeyserImpl.GSON.fromJson(authChain, JsonObject.class);
if (parsedAuthChain.has("mcProfile")) { // Old Minecraft v4 auth chain
parsedAuthChain = MinecraftAuth4To5Migrator.migrateJavaSave(parsedAuthChain, GeyserImpl.OAUTH_CONFIG);
}
@@ -946,7 +942,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
new GameProfile(mcProfile.getId(), mcProfile.getName()),
mcToken.getToken()
);
geyser.saveAuthChain(bedrockUsername(), GSON.toJson(JavaAuthManager.toJson(authManager)));
geyser.saveAuthChain(bedrockUsername(), GeyserImpl.GSON.toJson(JavaAuthManager.toJson(authManager)));
return Boolean.TRUE;
}).whenComplete((successful, ex) -> {
if (this.closed) {
@@ -1039,7 +1035,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
// Save our auth chain for later use
geyser.saveAuthChain(bedrockUsername(), GSON.toJson(JavaAuthManager.toJson(result)));
geyser.saveAuthChain(bedrockUsername(), GeyserImpl.GSON.toJson(JavaAuthManager.toJson(result)));
return true;
}).getNow(false);
}
@@ -1091,10 +1087,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// Disable automatic creation of a new TcpClientSession when transferring - we don't use that functionality.
this.downstream.getSession().setFlag(MinecraftConstants.FOLLOW_TRANSFERS, false);
if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
if (geyser.config().advanced().java().useHaproxyProtocol()) {
downstream.setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
}
if (geyser.getConfig().isForwardPlayerPing()) {
if (geyser.config().gameplay().forwardPlayerPing()) {
// Let Geyser handle sending the keep alive
downstream.setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false);
}
@@ -1140,7 +1136,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
} else {
// Downstream's disconnect will fire an event that prints a log message
// Otherwise, we print a message here
String address = geyser.getConfig().isLogPlayerIpAddresses() ? upstream.getAddress().getAddress().toString() : "<IP address withheld>";
String address = geyser.config().logPlayerIpAddresses() ? upstream.getAddress().getAddress().toString() : "<IP address withheld>";
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, MessageTranslator.convertMessage(reason)));
}
@@ -1740,7 +1736,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setLevelGameType(GameType.SURVIVAL);
startGamePacket.setDifficulty(1);
startGamePacket.setDefaultSpawn(Vector3i.ZERO);
startGamePacket.setAchievementsDisabled(!geyser.getConfig().isXboxAchievementsEnabled());
startGamePacket.setAchievementsDisabled(!geyser.config().gameplay().xboxAchievementsEnabled());
startGamePacket.setCurrentTick(-1);
startGamePacket.setEduEditionOffers(0);
startGamePacket.setEduFeaturesEnabled(false);
@@ -1750,7 +1746,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setBroadcastingToLan(true);
startGamePacket.setPlatformBroadcastMode(GamePublishSetting.PUBLIC);
startGamePacket.setXblBroadcastMode(GamePublishSetting.PUBLIC);
startGamePacket.setCommandsEnabled(!geyser.getConfig().isXboxAchievementsEnabled());
startGamePacket.setCommandsEnabled(!geyser.config().gameplay().xboxAchievementsEnabled());
startGamePacket.setTexturePacksRequired(false);
startGamePacket.setBonusChestEnabled(false);
startGamePacket.setStartingWithMap(false);
@@ -1768,7 +1764,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setEducationProductionId("");
startGamePacket.setForceExperimentalGameplay(OptionalBoolean.empty());
String serverName = geyser.getConfig().getBedrock().serverName();
String serverName = geyser.config().gameplay().serverName();
startGamePacket.setLevelId(serverName);
startGamePacket.setLevelName(serverName);
@@ -1914,7 +1910,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
public void sendDownstreamPacket(Packet packet, ProtocolState intendedState) {
// protocol can be null when we're not yet logged in (online auth)
if (protocol == null) {
if (geyser.getConfig().isDebugMode()) {
if (geyser.config().debugMode()) {
geyser.getLogger().debug("Tried to send downstream packet with no downstream session!");
Thread.dumpStack();
}
@@ -1940,7 +1936,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
if (channel == null) {
// Channel is only null before the connection has initialized
geyser.getLogger().warning("Tried to send a packet to the Java server too early!");
if (geyser.getConfig().isDebugMode()) {
if (geyser.config().debugMode()) {
Thread.dumpStack();
}
return;
@@ -2431,7 +2427,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private void softEnumPacket(String name, SoftEnumUpdateType type, String enums) {
// There is no need to send command enums if command suggestions are disabled
if (!this.geyser.getConfig().isCommandSuggestions()) {
if (!this.geyser.config().gameplay().commandSuggestions()) {
return;
}
UpdateSoftEnumPacket packet = new UpdateSoftEnumPacket();

View File

@@ -110,7 +110,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
}
String address;
if (geyser.getConfig().getRemote().isForwardHost()) {
if (geyser.config().java().forwardHostname()) {
address = session.joinAddress();
} else {
address = intentionPacket.getHostname();
@@ -172,7 +172,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
customDisconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.authentication_type_mismatch", locale);
// Explain that they may be looking for Floodgate.
geyser.getLogger().warning(GeyserLocale.getLocaleStringLog(
geyser.getPlatformType() == PlatformType.STANDALONE ?
geyser.platformType() == PlatformType.STANDALONE ?
"geyser.network.remote.floodgate_explanation_standalone"
: "geyser.network.remote.floodgate_explanation_plugin",
Constants.FLOODGATE_DOWNLOAD_LOCATION
@@ -180,7 +180,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
} else {
// Likely that Floodgate is not configured correctly.
customDisconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.floodgate_login_error", locale);
if (geyser.getPlatformType() == PlatformType.STANDALONE) {
if (geyser.platformType() == PlatformType.STANDALONE) {
geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.remote.floodgate_login_error_standalone"));
}
}
@@ -203,7 +203,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
} else {
GeyserImpl.getInstance().getLogger().error("An exception occurred: ", cause);
}
if (geyser.getConfig().isDebugMode()) {
if (geyser.config().debugMode()) {
cause.printStackTrace();
}
}
@@ -233,7 +233,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
(event.getPacketClass() != null ? "(" + event.getPacketClass().getSimpleName() + ") " : "") +
event.getCause().getMessage())
);
if (geyser.getConfig().isDebugMode())
if (geyser.config().debugMode())
event.getCause().printStackTrace();
event.setSuppress(true);
}

View File

@@ -55,7 +55,7 @@ public final class SessionDisconnectListener {
String serverDisconnectMessage = MessageTranslator.convertMessage(disconnectReason, session.locale());
if (testForOutdatedServer(disconnectReason)) {
String locale = session.locale();
PlatformType platform = session.getGeyser().getPlatformType();
PlatformType platform = session.getGeyser().platformType();
String outdatedType = (platform == PlatformType.BUNGEECORD || platform == PlatformType.VELOCITY || platform == PlatformType.VIAPROXY) ?
"geyser.network.remote.outdated.proxy" : "geyser.network.remote.outdated.server";
event.disconnectReason(GeyserLocale.getPlayerLocaleString(outdatedType, locale, GameProtocol.getJavaVersions().get(0)) + '\n'

View File

@@ -25,93 +25,97 @@
package org.geysermc.geyser.session.auth;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.floodgate.util.DeviceOs;
import org.geysermc.floodgate.util.InputMode;
import org.geysermc.floodgate.util.UiProfile;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public final class BedrockClientData {
@JsonProperty(value = "GameVersion")
@SerializedName(value = "GameVersion")
private String gameVersion;
@JsonProperty(value = "ServerAddress")
@SerializedName(value = "ServerAddress")
private String serverAddress;
@JsonProperty(value = "ThirdPartyName")
@SerializedName(value = "ThirdPartyName")
private String username;
@JsonProperty(value = "LanguageCode")
@SerializedName(value = "LanguageCode")
private String languageCode;
@JsonProperty(value = "SkinId")
@SerializedName(value = "SkinId")
private String skinId;
@JsonProperty(value = "SkinData")
@SerializedName(value = "SkinData")
private String skinData;
@JsonProperty(value = "SkinImageHeight")
@SerializedName(value = "SkinImageHeight")
private int skinImageHeight;
@JsonProperty(value = "SkinImageWidth")
@SerializedName(value = "SkinImageWidth")
private int skinImageWidth;
@JsonProperty(value = "CapeId")
@SerializedName(value = "CapeId")
private String capeId;
@JsonProperty(value = "CapeData")
@SerializedName(value = "CapeData")
@JsonAdapter(value = StringToByteDeserializer.class)
private byte[] capeData;
@JsonProperty(value = "CapeImageHeight")
@SerializedName(value = "CapeImageHeight")
private int capeImageHeight;
@JsonProperty(value = "CapeImageWidth")
@SerializedName(value = "CapeImageWidth")
private int capeImageWidth;
@JsonProperty(value = "CapeOnClassicSkin")
@SerializedName(value = "CapeOnClassicSkin")
private boolean capeOnClassicSkin;
@JsonProperty(value = "SkinResourcePatch")
@SerializedName(value = "SkinResourcePatch")
private String geometryName;
@JsonProperty(value = "SkinGeometryData")
@SerializedName(value = "SkinGeometryData")
private String geometryData;
@JsonProperty(value = "PersonaSkin")
@SerializedName(value = "PersonaSkin")
private boolean personaSkin;
@JsonProperty(value = "PremiumSkin")
@SerializedName(value = "PremiumSkin")
private boolean premiumSkin;
@JsonProperty(value = "DeviceId")
@SerializedName(value = "DeviceId")
private String deviceId;
@JsonProperty(value = "DeviceModel")
@SerializedName(value = "DeviceModel")
private String deviceModel;
@JsonProperty(value = "DeviceOS")
@SerializedName(value = "DeviceOS")
private DeviceOs deviceOs;
@JsonProperty(value = "UIProfile")
@SerializedName(value = "UIProfile")
private UiProfile uiProfile;
@JsonProperty(value = "GuiScale")
@SerializedName(value = "GuiScale")
private int guiScale;
@JsonProperty(value = "CurrentInputMode")
@SerializedName(value = "CurrentInputMode")
private InputMode currentInputMode;
@JsonProperty(value = "DefaultInputMode")
@SerializedName(value = "DefaultInputMode")
private InputMode defaultInputMode;
@JsonProperty("PlatformOnlineId")
@SerializedName("PlatformOnlineId")
private String platformOnlineId;
@JsonProperty(value = "PlatformOfflineId")
@SerializedName(value = "PlatformOfflineId")
private String platformOfflineId;
@JsonProperty(value = "SelfSignedId")
@SerializedName(value = "SelfSignedId")
private UUID selfSignedId;
@JsonProperty(value = "ClientRandomId")
@SerializedName(value = "ClientRandomId")
private long clientRandomId;
@JsonProperty(value = "ArmSize")
@SerializedName(value = "ArmSize")
private String armSize;
@JsonProperty(value = "SkinAnimationData")
@SerializedName(value = "SkinAnimationData")
private String skinAnimationData;
@JsonProperty(value = "SkinColor")
@SerializedName(value = "SkinColor")
private String skinColor;
@JsonProperty(value = "ThirdPartyNameOnly")
@SerializedName(value = "ThirdPartyNameOnly")
private boolean thirdPartyNameOnly;
@JsonProperty(value = "PlayFabId")
@SerializedName(value = "PlayFabId")
private String playFabId;
@JsonIgnore
@Setter
private String originalString = null;
private transient String originalString = null;
public DeviceOs getDeviceOs() {
return deviceOs != null ? deviceOs : DeviceOs.UNKNOWN;
@@ -128,4 +132,11 @@ public final class BedrockClientData {
public UiProfile getUiProfile() {
return uiProfile != null ? uiProfile : UiProfile.CLASSIC;
}
private static final class StringToByteDeserializer implements JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return json.getAsString().getBytes(StandardCharsets.UTF_8);
}
}
}

View File

@@ -68,7 +68,7 @@ public final class BundleCache {
if (itemStack.is(session, ItemTag.BUNDLES)) {
if (itemStack.getBundleData() != null) {
session.getGeyser().getLogger().warning("Stack has bundle data already! It should not!");
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().getLogger().isDebug()) {
session.getGeyser().getLogger().debug("Player: " + session.javaUsername());
session.getGeyser().getLogger().debug("Stack: " + itemStack);
}

View File

@@ -27,7 +27,7 @@ package org.geysermc.geyser.session.cache;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.CooldownUtils;
@@ -54,15 +54,16 @@ public class PreferencesCache {
private boolean prefersCustomSkulls;
/**
* Which CooldownType the client prefers. Initially set to {@link CooldownUtils#getDefaultShowCooldown()}.
* Which CooldownType the client prefers. Initially set to the config default.
*/
@Setter
private CooldownUtils.CooldownType cooldownPreference = CooldownUtils.getDefaultShowCooldown();
private CooldownUtils.CooldownType cooldownPreference;
public PreferencesCache(GeyserSession session) {
this.session = session;
prefersCustomSkulls = session.getGeyser().getConfig().isAllowCustomSkulls();
prefersCustomSkulls = session.getGeyser().config().gameplay().maxVisibleCustomSkulls() != 0;
cooldownPreference = session.getGeyser().config().gameplay().showCooldown();
}
/**
@@ -71,10 +72,10 @@ public class PreferencesCache {
* If {@link #prefersShowCoordinates} is true, coordinates will be shown, unless either of the following conditions apply: <br>
* <br>
* {@link GeyserSession#isReducedDebugInfo()} is enabled
* {@link GeyserConfiguration#isShowCoordinates()} is disabled
* {@link GeyserConfig.GameplayConfig#showCoordinates()} is disabled
*/
public void updateShowCoordinates() {
allowShowCoordinates = !session.isReducedDebugInfo() && session.getGeyser().getConfig().isShowCoordinates();
allowShowCoordinates = !session.isReducedDebugInfo() && session.getGeyser().config().gameplay().showCoordinates();
session.sendGameRule("showcoordinates", allowShowCoordinates && prefersShowCoordinates);
}
@@ -82,6 +83,6 @@ public class PreferencesCache {
* @return true if the session prefers custom skulls, and the config allows them.
*/
public boolean showCustomSkulls() {
return prefersCustomSkulls && session.getGeyser().getConfig().isAllowCustomSkulls();
return prefersCustomSkulls && session.getGeyser().config().gameplay().maxVisibleCustomSkulls() != 0;
}
}

View File

@@ -67,11 +67,11 @@ public class SkullCache {
public SkullCache(GeyserSession session) {
this.session = session;
this.maxVisibleSkulls = session.getGeyser().getConfig().getMaxVisibleCustomSkulls();
this.maxVisibleSkulls = session.getGeyser().config().gameplay().maxVisibleCustomSkulls();
this.cullingEnabled = this.maxVisibleSkulls != -1;
// Normal skulls are not rendered beyond 64 blocks
int distance = Math.min(session.getGeyser().getConfig().getCustomSkullRenderDistance(), 64);
int distance = Math.min(session.getGeyser().config().gameplay().customSkullRenderDistance(), 64);
this.skullRenderDistanceSquared = distance * distance;
}
@@ -99,7 +99,7 @@ public class SkullCache {
}
} catch (IOException e) {
session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
}

View File

@@ -25,11 +25,9 @@
package org.geysermc.geyser.skin;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import lombok.Getter;
import org.geysermc.floodgate.pluginmessage.PluginMessageChannels;
import org.geysermc.floodgate.util.WebsocketEventType;
@@ -37,6 +35,7 @@ import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.geyser.util.PluginMessageUtils;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
@@ -52,7 +51,6 @@ import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public final class FloodgateSkinUploader {
private final ObjectMapper JACKSON = new ObjectMapper();
private final List<String> skinQueue = new ArrayList<>();
private final GeyserLogger logger;
@@ -79,15 +77,14 @@ public final class FloodgateSkinUploader {
@Override
public void onMessage(String message) {
// The reason why I don't like Jackson
try {
JsonNode node = JACKSON.readTree(message);
JsonObject node = JsonUtils.parseJson(message);
if (node.has("error")) {
logger.error("Got an error: " + node.get("error").asText());
logger.error("Got an error: " + node.get("error").getAsString());
return;
}
int typeId = node.get("event_id").asInt();
int typeId = node.get("event_id").getAsInt();
WebsocketEventType type = WebsocketEventType.fromId(typeId);
if (type == null) {
logger.warning(String.format(
@@ -98,11 +95,11 @@ public final class FloodgateSkinUploader {
switch (type) {
case SUBSCRIBER_CREATED:
id = node.get("id").asInt();
verifyCode = node.get("verify_code").asText();
id = node.get("id").getAsInt();
verifyCode = node.get("verify_code").getAsString();
break;
case SUBSCRIBER_COUNT:
subscribersCount = node.get("subscribers_count").asInt();
subscribersCount = node.get("subscribers_count").getAsInt();
break;
case SKIN_UPLOADED:
// if Geyser is the only subscriber we have send it to the server manually
@@ -111,19 +108,19 @@ public final class FloodgateSkinUploader {
break;
}
String xuid = node.get("xuid").asText();
String xuid = node.get("xuid").getAsString();
GeyserSession session = geyser.connectionByXuid(xuid);
if (session != null) {
if (!node.get("success").asBoolean()) {
if (!node.get("success").getAsBoolean()) {
logger.info("Failed to upload skin for " + session.bedrockUsername());
return;
}
JsonNode data = node.get("data");
JsonObject data = node.getAsJsonObject("data");
String value = data.get("value").asText();
String signature = data.get("signature").asText();
String value = data.get("value").getAsString();
String signature = data.get("signature").getAsString();
byte[] bytes = (value + '\0' + signature)
.getBytes(StandardCharsets.UTF_8);
@@ -131,8 +128,8 @@ public final class FloodgateSkinUploader {
}
break;
case LOG_MESSAGE:
String logMessage = node.get("message").asText();
switch (node.get("priority").asInt()) {
String logMessage = node.get("message").getAsString();
switch (node.get("priority").getAsInt()) {
case -1 -> logger.debug("Got a message from skin uploader: " + logMessage);
case 0 -> logger.info("Got a message from skin uploader: " + logMessage);
case 1 -> logger.error("Got a message from skin uploader: " + logMessage);
@@ -150,20 +147,19 @@ public final class FloodgateSkinUploader {
@Override
public void onClose(int code, String reason, boolean remote) {
if (reason != null && !reason.isEmpty()) {
// The reason why I don't like Jackson
try {
JsonNode node = JACKSON.readTree(reason);
JsonObject node = JsonUtils.parseJson(reason);
// info means that the uploader itself did nothing wrong
if (node.has("info")) {
String info = node.get("info").asText();
String info = node.get("info").getAsString();
logger.debug("Got disconnected from the skin uploader: " + info);
}
// error means that the uploader did something wrong
if (node.has("error")) {
String error = node.get("error").asText();
String error = node.get("error").getAsString();
logger.info("Got disconnected from the skin uploader: " + error);
}
} catch (JsonProcessingException ignored) {
} catch (JsonSyntaxException ignored) {
// ignore invalid json
} catch (Exception e) {
logger.error("Error while handling onClose", e);
@@ -198,24 +194,17 @@ public final class FloodgateSkinUploader {
return;
}
ObjectNode node = JACKSON.createObjectNode();
JsonObject node = new JsonObject();
if (chainData != null) {
ArrayNode chainDataNode = JACKSON.createArrayNode();
JsonArray chainDataNode = new JsonArray();
chainData.forEach(chainDataNode::add);
node.set("chain_data", chainDataNode);
node.add("chain_data", chainDataNode);
} else {
node.put("token", token);
node.addProperty("token", token);
}
node.put("client_data", clientData);
node.addProperty("client_data", clientData);
// The reason why I don't like Jackson
String jsonString;
try {
jsonString = JACKSON.writeValueAsString(node);
} catch (Exception e) {
logger.error("Failed to upload skin", e);
return;
}
String jsonString = node.toString();
if (client.isOpen()) {
client.send(jsonString);

View File

@@ -25,7 +25,10 @@
package org.geysermc.geyser.skin;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -46,6 +49,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.auth.BedrockClientData;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
@@ -303,7 +307,7 @@ public class SkinManager {
public static void handleBedrockSkin(AvatarEntity playerEntity, BedrockClientData clientData) {
GeyserImpl geyser = GeyserImpl.getInstance();
if (geyser.getConfig().isDebugMode()) {
if (geyser.config().debugMode()) {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.skin.bedrock.register", playerEntity.getUsername(), playerEntity.getUuid()));
}
@@ -317,7 +321,7 @@ public class SkinManager {
if (skinBytes.length <= (128 * 128 * 4) && !clientData.isPersonaSkin()) {
SkinProvider.storeBedrockSkin(playerEntity.getUuid(), clientData.getSkinId(), skinBytes);
SkinProvider.storeBedrockGeometry(playerEntity.getUuid(), geometryNameBytes, geometryBytes);
} else if (geyser.getConfig().isDebugMode()) {
} else if (geyser.config().debugMode()) {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.skin.bedrock.fail", playerEntity.getUsername()));
geyser.getLogger().debug("The size of '" + playerEntity.getUsername() + "' skin is: " + clientData.getSkinImageWidth() + "x" + clientData.getSkinImageHeight());
}
@@ -359,7 +363,7 @@ public class SkinManager {
return loadFromJson(skinDataValue);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for tag " + tag);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
return null;
@@ -387,7 +391,7 @@ public class SkinManager {
} else {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty);
}
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
exception.printStackTrace();
}
}
@@ -395,29 +399,25 @@ public class SkinManager {
}
public static @Nullable GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject;
JsonObject skinObject;
try {
skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
skinObject = JsonUtils.parseJson(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
GeyserImpl.getInstance().getLogger().debug("Invalid base64 encoded skin entry: " + encodedJson);
return null;
}
JsonNode textures = skinObject.get("textures");
if (textures == null) {
if (!(skinObject.get("textures") instanceof JsonObject textures)) {
return null;
}
JsonNode skinTexture = textures.get("SKIN");
if (skinTexture == null) {
if (!(textures.get("SKIN") instanceof JsonObject skinTexture)) {
return null;
}
String skinUrl;
JsonNode skinUrlNode = skinTexture.get("url");
if (skinUrlNode != null && skinUrlNode.isTextual()) {
skinUrl = skinUrlNode.asText().replace("http://", "https://");
if (skinTexture.get("url") instanceof JsonPrimitive skinUrlNode && skinUrlNode.isString()) {
skinUrl = skinUrlNode.getAsString().replace("http://", "https://");
} else {
return null;
}
@@ -434,11 +434,9 @@ public class SkinManager {
boolean isAlex = skinTexture.has("metadata");
String capeUrl = null;
JsonNode capeTexture = textures.get("CAPE");
if (capeTexture != null) {
JsonNode capeUrlNode = capeTexture.get("url");
if (capeUrlNode != null && capeUrlNode.isTextual()) {
capeUrl = capeUrlNode.asText().replace("http://", "https://");
if (textures.get("CAPE") instanceof JsonObject capeTexture) {
if (capeTexture.get("url") instanceof JsonPrimitive capeUrlNode && capeUrlNode.isString()) {
capeUrl = capeUrlNode.getAsString().replace("http://", "https://");
}
}

View File

@@ -25,9 +25,11 @@
package org.geysermc.geyser.skin;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.bytes.ByteArrays;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -56,7 +58,11 @@ import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class SkinProvider {
@@ -124,11 +130,6 @@ public class SkinProvider {
WEARING_CUSTOM_SKULL = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkull\"}}", wearingCustomSkull);
String wearingCustomSkullSlim = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json"), StandardCharsets.UTF_8);
WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkullSlim\"}}", wearingCustomSkullSlim);
GeyserImpl geyser = GeyserImpl.getInstance();
if (geyser.getConfig().isAllowThirdPartyEars() || geyser.getConfig().isAllowThirdPartyCapes()) {
geyser.getLogger().warning("Third-party ears/capes have been removed from Geyser, if you still wish to have this functionality please use the extension: https://github.com/GeyserMC/ThirdPartyCosmetics");
}
}
public static ExecutorService getExecutorService() {
@@ -147,7 +148,7 @@ public class SkinProvider {
public static void registerCacheImageTask(GeyserImpl geyser) {
// Schedule Daily Image Expiry if we are caching them
if (geyser.getConfig().getCacheImages() > 0) {
if (geyser.config().advanced().cacheImages() > 0) {
geyser.getScheduledThread().scheduleAtFixedRate(() -> {
File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile();
if (!cacheFolder.exists()) {
@@ -155,7 +156,7 @@ public class SkinProvider {
}
int count = 0;
final long expireTime = ((long) GeyserImpl.getInstance().getConfig().getCacheImages()) * ((long)1000 * 60 * 60 * 24);
final long expireTime = ((long) GeyserImpl.getInstance().config().advanced().cacheImages()) * ((long)1000 * 60 * 60 * 24);
for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) {
if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) {
//noinspection ResultOfMethodCallIgnored
@@ -186,7 +187,7 @@ public class SkinProvider {
Cape cape = null;
SkinGeometry geometry = SkinGeometry.WIDE;
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
if (GeyserImpl.getInstance().config().java().authType() != AuthType.ONLINE) {
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
@@ -425,7 +426,7 @@ public class SkinProvider {
GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl);
// Write to cache if we are allowed
if (GeyserImpl.getInstance().getConfig().getCacheImages() > 0) {
if (GeyserImpl.getInstance().config().advanced().cacheImages() > 0) {
imageFile.getParentFile().mkdirs();
try {
ImageIO.write(image, "png", imageFile);
@@ -503,15 +504,15 @@ public class SkinProvider {
public static CompletableFuture<@Nullable String> requestUsernameFromUUID(UUID uuid) {
return CompletableFuture.supplyAsync(() -> {
try {
JsonNode node = WebUtils.getJson("https://api.minecraftservices.com/minecraft/profile/lookup/" + shorthandUUID(uuid));
JsonNode name = node.get("name");
JsonObject node = WebUtils.getJson("https://api.minecraftservices.com/minecraft/profile/lookup/" + shorthandUUID(uuid));
JsonElement name = node.get("name");
if (name == null) {
GeyserImpl.getInstance().getLogger().debug("No username found in Mojang response for " + uuid);
return null;
}
return name.asText();
return name.getAsString();
} catch (Exception e) {
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
return null;
@@ -528,15 +529,15 @@ public class SkinProvider {
public static CompletableFuture<@Nullable UUID> requestUUIDFromUsername(String username) {
return CompletableFuture.supplyAsync(() -> {
try {
JsonNode node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username);
JsonNode id = node.get("id");
JsonObject node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username);
JsonElement id = node.get("id");
if (id == null) {
GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + username);
return null;
}
return expandUUID(id.asText());
return expandUUID(id.getAsString());
} catch (Exception e) {
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
return null;
@@ -553,16 +554,16 @@ public class SkinProvider {
public static CompletableFuture<@Nullable String> requestTexturesFromUUID(UUID uuid) {
return CompletableFuture.supplyAsync(() -> {
try {
JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + shorthandUUID(uuid));
JsonNode properties = node.get("properties");
JsonObject node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + shorthandUUID(uuid));
JsonArray properties = node.getAsJsonArray("properties");
if (properties == null) {
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid);
return null;
}
return node.get("properties").get(0).get("value").asText();
return properties.get(0).getAsJsonObject().get("value").getAsString();
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().debug("Unable to request textures for " + uuid);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
return null;

View File

@@ -25,84 +25,67 @@
package org.geysermc.geyser.text;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.spongepowered.configurate.objectmapping.meta.Processor;
import org.spongepowered.configurate.serialize.SerializationException;
import java.io.IOException;
import java.io.Serial;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Optional;
public class AsteriskSerializer extends StdSerializer<Object> implements ContextualSerializer {
@Serial
private static final long serialVersionUID = 1L;
import java.lang.reflect.Type;
import java.net.InetAddress;
public class AsteriskSerializer implements JsonSerializer<String> {
public static final String[] NON_SENSITIVE_ADDRESSES = {"", "0.0.0.0", "localhost", "127.0.0.1", "auto", "unknown"};
public static boolean showSensitive = false;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = AsteriskSerializer.class)
public @interface Asterisk {
String value() default "***";
/**
* If true, this value will be shown if {@link #showSensitive} is true, or if the IP is determined to not be a public IP
*
* @return true if this should be analyzed and treated as an IP
*/
boolean isIp() default false;
}
String asterisk;
boolean isIp;
@SuppressWarnings("unused") // Used by Jackson for Geyser dumps
public AsteriskSerializer() {
super(Object.class);
}
public AsteriskSerializer(String asterisk, boolean isIp) {
super(Object.class);
this.asterisk = asterisk;
this.isIp = isIp;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty property) {
Optional<Asterisk> anno = Optional.ofNullable(property)
.map(prop -> prop.getAnnotation(Asterisk.class));
return new AsteriskSerializer(anno.map(Asterisk::value).orElse(null), anno.map(Asterisk::isIp).orElse(false));
}
@Override
public void serialize(Object obj, JsonGenerator gen, SerializerProvider prov) throws IOException {
if (isIp && (showSensitive || !isSensitiveIp((String) obj))) {
gen.writeObject(obj);
return;
public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
if (showSensitive || !isSensitiveIp(src)) {
return new JsonPrimitive(src);
}
gen.writeString(asterisk);
return new JsonPrimitive("***");
}
private boolean isSensitiveIp(String ip) {
private static boolean isSensitiveIp(String ip) {
for (String address : NON_SENSITIVE_ADDRESSES) {
if (address.equalsIgnoreCase(ip)) {
return false;
}
}
try {
InetAddress address = InetAddress.getByName(ip);
if (address.isSiteLocalAddress() || address.isLoopbackAddress()) {
return false;
}
} catch (Exception e) {
// Ignore
}
return true;
}
public static Processor.Factory<Asterisk, String> CONFIGURATE_SERIALIZER = (data, fieldType) -> (value, destination) -> {
if (showSensitive || !isSensitiveIp(value)) {
return;
}
try {
destination.set("***");
} catch (SerializationException e) {
throw new RuntimeException("Unable to censor IP address", e); // Error over silently printing an IP address.
}
};
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Asterisk {
}
}

View File

@@ -41,6 +41,7 @@ import java.util.Map;
import java.util.Properties;
public class GeyserLocale {
public static final String SYSTEM_LOCALE = "system";
/**
* If we determine the default locale that the user wishes to use, use that locale
@@ -80,8 +81,8 @@ public class GeyserLocale {
* Finalize the default locale, now that we know what the default locale should be.
*/
public static void finalizeDefaultLocale(GeyserImpl geyser) {
String newDefaultLocale = geyser.getConfig().getDefaultLocale();
if (newDefaultLocale == null) {
String newDefaultLocale = geyser.config().defaultLocale();
if (SYSTEM_LOCALE.equals(newDefaultLocale)) {
// We want to use the system locale which is already loaded
return;
}

View File

@@ -25,11 +25,13 @@
package org.geysermc.geyser.text;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.util.AssetUtils;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.JsonUtils;
import org.geysermc.geyser.util.WebUtils;
import java.io.FileNotFoundException;
@@ -39,7 +41,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
@@ -194,14 +195,12 @@ public class MinecraftLocale {
// Read the localefile
try (InputStream localeStream = Files.newInputStream(localeFile, StandardOpenOption.READ)) {
// Parse the file as json
JsonNode localeObj = GeyserImpl.JSON_MAPPER.readTree(localeStream);
JsonObject localeObj = JsonUtils.fromJson(localeStream);
// Parse all the locale fields
Iterator<Map.Entry<String, JsonNode>> localeIterator = localeObj.fields();
Map<String, String> langMap = new HashMap<>();
while (localeIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = localeIterator.next();
langMap.put(entry.getKey(), entry.getValue().asText());
for (Map.Entry<String, JsonElement> entry : localeObj.entrySet()) {
langMap.put(entry.getKey(), entry.getValue().getAsString());
}
return langMap;
} catch (FileNotFoundException e){

View File

@@ -306,7 +306,7 @@ public abstract class InventoryTranslator<Type extends Inventory> {
case PLACE: {
TransferItemStackRequestAction transferAction = (TransferItemStackRequestAction) action;
if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) {
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().config().debugMode()) {
session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.bedrockUsername());
dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination());
}
@@ -477,7 +477,7 @@ public abstract class InventoryTranslator<Type extends Inventory> {
ItemStackRequestSlotData destination = swapAction.getDestination();
if (!(checkNetId(session, inventory, source) && checkNetId(session, inventory, destination))) {
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().config().debugMode()) {
session.getGeyser().getLogger().error("DEBUG: About to reject SWAP request made by " + session.bedrockUsername());
dumpStackRequestDetails(session, inventory, source, destination);
}
@@ -984,7 +984,7 @@ public abstract class InventoryTranslator<Type extends Inventory> {
* as bad (false).
*/
protected static ItemStackResponse rejectRequest(ItemStackRequest request, boolean throwError) {
if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (throwError && GeyserImpl.getInstance().config().debugMode()) {
new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace();
}
return new ItemStackResponse(ItemStackResponseStatus.ERROR, request.getRequestId(), Collections.emptyList());

View File

@@ -107,7 +107,7 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
return skull.getBlockDefinition();
} catch (InterruptedException | ExecutionException e) {
session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + javaNbt);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
e.printStackTrace();
}
}
@@ -118,7 +118,7 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
resolvedFuture.whenComplete((resolved, throwable) -> {
if (throwable != null ) {
session.getGeyser().getLogger().debug("Failed resolving profile of player head at: " + blockPosition + " " + javaNbt);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
if (GeyserImpl.getInstance().config().debugMode()) {
throwable.printStackTrace();
}
return;

View File

@@ -26,8 +26,6 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import org.cloudburstmc.protocol.bedrock.packet.CommandRequestPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;

View File

@@ -26,7 +26,6 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@@ -36,10 +35,6 @@ public class BedrockEmoteListTranslator extends PacketTranslator<EmoteListPacket
@Override
public void translate(GeyserSession session, EmoteListPacket packet) {
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) {
return;
}
session.refreshEmotes(packet.getPieceIds());
}
}

View File

@@ -173,7 +173,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
final Vector3i packetBlockPosition = packet.getBlockPosition();
Vector3i blockPos = BlockUtils.getBlockPosition(packetBlockPosition, Direction.getUntrusted(packet, InventoryTransactionPacket::getBlockFace));
if (session.getGeyser().getConfig().isDisableBedrockScaffolding()) {
if (session.getGeyser().config().gameplay().disableBedrockScaffolding()) {
float yaw = session.getPlayerEntity().getYaw();
boolean isGodBridging = switch (packet.getBlockFace()) {
case 2 -> yaw <= -135f || yaw > 135f;

View File

@@ -48,7 +48,7 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator<Netwo
public void translate(GeyserSession session, NetworkStackLatencyPacket packet) {
// negative timestamps are used as hack to fix the url image loading bug
if (packet.getTimestamp() >= 0) {
if (session.getGeyser().getConfig().isForwardPlayerPing()) {
if (session.getGeyser().config().gameplay().forwardPlayerPing()) {
// use our cached value because
// a) bedrock can be inaccurate with the value returned
// b) playstation replies with a different magnitude than other platforms

View File

@@ -46,7 +46,7 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat
if (session.remoteServer().authType() == AuthType.ONLINE) {
if (!session.isLoggedIn()) {
if (session.getGeyser().getConfig().getSavedUserLogins().contains(session.bedrockUsername())) {
if (session.getGeyser().config().savedUserLogins().contains(session.bedrockUsername())) {
if (session.getGeyser().authChainFor(session.bedrockUsername()) == null) {
LoginEncryptionUtils.buildAndShowConsentWindow(session);
} else {

View File

@@ -26,8 +26,8 @@
package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import org.cloudburstmc.protocol.bedrock.packet.EmotePacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.bedrock.ClientEmoteEvent;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
@@ -39,17 +39,12 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> {
@Override
public void translate(GeyserSession session, EmotePacket packet) {
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) {
// Activate the workaround - we should trigger the offhand now
session.requestOffhandSwap();
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) {
return;
}
}
// For the future: could have a method that exposes which players will see the emote
ClientEmoteEvent event = new ClientEmoteEvent(session, packet.getEmoteId());
if (!GeyserImpl.getInstance().config().gameplay().emotesEnabled()) {
event.setCancelled(true);
}
session.getGeyser().eventBus().fire(event);
if (event.isCancelled()) {
return;

View File

@@ -123,7 +123,7 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
@Override
public void translate(GeyserSession session, ClientboundCommandsPacket packet) {
// Don't send command suggestions if they are disabled
if (!session.getGeyser().getConfig().isCommandSuggestions()) {
if (!session.getGeyser().config().gameplay().commandSuggestions()) {
session.getGeyser().getLogger().debug("Not sending translated command suggestions as they are disabled.");
// Send a mostly empty packet so Bedrock doesn't override /help with its own, built-in help command.

View File

@@ -39,7 +39,7 @@ public class JavaKeepAliveTranslator extends PacketTranslator<ClientboundKeepAli
@Override
public void translate(GeyserSession session, ClientboundKeepAlivePacket packet) {
if (!session.getGeyser().getConfig().isForwardPlayerPing()) {
if (!session.getGeyser().config().gameplay().forwardPlayerPing()) {
return;
}
// We use this once the client replies (see BedrockNetworkStackLatencyTranslator)

View File

@@ -45,7 +45,7 @@ public class JavaSetEntityDataTranslator extends PacketTranslator<ClientboundSet
EntityDefinition<?> definition = entity.getDefinition();
for (EntityMetadata<?, ?> metadata : packet.getMetadata()) {
if (metadata.getId() >= definition.translators().size()) {
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().config().debugMode()) {
// Minecraft client just ignores these
session.getGeyser().getLogger().warning("Metadata ID " + metadata.getId() + " is out of bounds of known entity metadata size " + definition.translators().size() + " for entity type " + entity.getDefinition().entityType());
session.getGeyser().getLogger().debug(metadata.toString());

View File

@@ -110,7 +110,7 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
ChunkUtils.updateChunkPosition(session, position.toInt());
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().config().debugMode()) {
session.getGeyser().getLogger().debug("Spawned player at " + packet.getPosition());
}
return;

View File

@@ -97,7 +97,7 @@ public class JavaBlockEventTranslator extends PacketTranslator<ClientboundBlockE
// Retracting sticky pistons is an exception, since the event is not called on Spigot from 1.13.2 - 1.17.1
// See https://github.com/PaperMC/Paper/blob/6fa1983e9ce177a4a412d5b950fd978620174777/patches/server/0304-Fire-BlockPistonRetractEvent-for-all-empty-pistons.patch
boolean isSticky = isSticky(pistonBlock);
if (session.getGeyser().getPlatformType() == PlatformType.SPIGOT && !isSticky) {
if (session.getGeyser().platformType() == PlatformType.SPIGOT && !isSticky) {
return;
}

View File

@@ -438,7 +438,7 @@ public class MessageTranslator {
textPacket.setMessage(MessageTranslator.convertMessage(withDecoration.build(), session.locale()));
} else {
session.getGeyser().getLogger().debug("Likely illegal chat type detection found.");
if (session.getGeyser().getConfig().isDebugMode()) {
if (session.getGeyser().config().debugMode()) {
Thread.dumpStack();
}
textPacket.setMessage(MessageTranslator.convertMessage(message, session.locale()));

View File

@@ -25,18 +25,28 @@
package org.geysermc.geyser.util;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.text.GeyserLocale;
import java.io.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile;
@@ -83,7 +93,7 @@ public final class AssetUtils {
return CompletableFuture.supplyAsync(() -> {
try {
// Get the version manifest from Mojang
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(
VersionManifest versionManifest = GeyserImpl.GSON.fromJson(
WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
// Get the url for the latest version of the games manifest
@@ -101,26 +111,24 @@ public final class AssetUtils {
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
VersionInfo versionInfo = GeyserImpl.GSON.fromJson(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
GeyserImpl.getInstance().getLogger().debug(versionInfo.getDownloads()); // Was previously a Jackson call for writeValueToString
CLIENT_JAR_INFO = versionInfo.getDownloads().get("client");
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(CLIENT_JAR_INFO));
GeyserImpl.getInstance().getLogger().debug(CLIENT_JAR_INFO); // Was previously a Jackson call for writeValueToString
// Get the assets list
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
JsonObject assets = ((JsonObject) new JsonParser().parse(WebUtils.getBody(versionInfo.getAssetIndex().getUrl()))).getAsJsonObject("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
for (Map.Entry<String, JsonElement> entry : assets.entrySet()) {
if (!entry.getKey().startsWith("minecraft/lang/")) {
// No need to cache non-language assets as we don't use them
continue;
}
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
Asset asset = GeyserImpl.GSON.fromJson(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
@@ -221,106 +229,108 @@ public final class AssetUtils {
/* Classes that map to JSON files served by Mojang */
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionManifest {
@JsonProperty("latest")
@SerializedName("latest")
private LatestVersion latestVersion;
@JsonProperty("versions")
@SerializedName("versions")
private List<Version> versions;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class LatestVersion {
@JsonProperty("release")
@SerializedName("release")
private String release;
@JsonProperty("snapshot")
@SerializedName("snapshot")
private String snapshot;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Version {
@JsonProperty("id")
@SerializedName("id")
private String id;
@JsonProperty("type")
@SerializedName("type")
private String type;
@JsonProperty("url")
@SerializedName("url")
private String url;
@JsonProperty("time")
@SerializedName("time")
private String time;
@JsonProperty("releaseTime")
@SerializedName("releaseTime")
private String releaseTime;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionInfo {
@JsonProperty("id")
@SerializedName("id")
private String id;
@JsonProperty("type")
@SerializedName("type")
private String type;
@JsonProperty("time")
@SerializedName("time")
private String time;
@JsonProperty("releaseTime")
@SerializedName("releaseTime")
private String releaseTime;
@JsonProperty("assetIndex")
@SerializedName("assetIndex")
private AssetIndex assetIndex;
@JsonProperty("downloads")
@SerializedName("downloads")
private Map<String, VersionDownload> downloads;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionDownload {
@JsonProperty("sha1")
@SerializedName("sha1")
private String sha1;
@JsonProperty("size")
@SerializedName("size")
private int size;
@JsonProperty("url")
@SerializedName("url")
private String url;
@Override
public String toString() {
return "VersionDownload{" +
"sha1='" + sha1 + '\'' +
", size=" + size +
", url='" + url + '\'' +
'}';
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class AssetIndex {
@JsonProperty("id")
@SerializedName("id")
private String id;
@JsonProperty("sha1")
@SerializedName("sha1")
private String sha1;
@JsonProperty("size")
@SerializedName("size")
private int size;
@JsonProperty("totalSize")
@SerializedName("totalSize")
private int totalSize;
@JsonProperty("url")
@SerializedName("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public static class Asset {
@JsonProperty("hash")
@SerializedName("hash")
private String hash;
@JsonProperty("size")
@SerializedName("size")
private int size;
}

View File

@@ -38,23 +38,13 @@ import java.util.concurrent.TimeUnit;
* Much of the work here is from the wonderful folks from <a href="https://github.com/ViaVersion/ViaRewind">ViaRewind</a>
*/
public class CooldownUtils {
private static CooldownType DEFAULT_SHOW_COOLDOWN;
public static void setDefaultShowCooldown(String showCooldown) {
DEFAULT_SHOW_COOLDOWN = CooldownType.getByName(showCooldown);
}
public static CooldownType getDefaultShowCooldown() {
return DEFAULT_SHOW_COOLDOWN;
}
/**
* Starts sending the fake cooldown to the Bedrock client. If the cooldown is not disabled, the sent type is the cooldownPreference in {@link PreferencesCache}
*
* @param session GeyserSession
*/
public static void sendCooldown(GeyserSession session) {
if (DEFAULT_SHOW_COOLDOWN == CooldownType.DISABLED) return;
if (session.getGeyser().config().gameplay().showCooldown() == CooldownType.DISABLED) return;
CooldownType sessionPreference = session.getPreferencesCache().getCooldownPreference();
if (sessionPreference == CooldownType.DISABLED) return;
@@ -170,10 +160,6 @@ public class CooldownUtils {
* @return The converted CooldownType
*/
public static CooldownType getByName(String name) {
if (name.equalsIgnoreCase("true")) { // Backwards config compatibility
return CooldownType.TITLE;
}
for (CooldownType type : VALUES) {
if (type.name().equalsIgnoreCase(name)) {
return type;

View File

@@ -25,15 +25,17 @@
package org.geysermc.geyser.util;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.io.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -44,28 +46,18 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FileUtils {
/**
* Load the given YAML file into the given class
*
* @param src File to load
* @param valueType Class to load file into
* @param <T> the type
* @return The data as the given class
* @throws IOException if the config could not be loaded
*/
public final class FileUtils {
public static <T> T loadConfig(File src, Class<T> valueType) throws IOException {
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory())
// Allow inference of single values as arrays
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
return objectMapper.readValue(src, valueType);
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.file(src)
.build();
ConfigurationNode node = loader.load();
return node.get(valueType);
}
public static <T> T loadJson(InputStream src, Class<T> valueType) throws IOException {
public static <T> T loadJson(InputStream src, Class<T> valueType) {
// Read specifically with UTF-8 to allow any non-UTF-encoded JSON to read
return GeyserImpl.JSON_MAPPER.readValue(new InputStreamReader(src, StandardCharsets.UTF_8), valueType);
return GeyserImpl.GSON.fromJson(new InputStreamReader(src, StandardCharsets.UTF_8), valueType);
}
/**
@@ -254,4 +246,7 @@ public class FileUtils {
throw new RuntimeException(e);
}
}
private FileUtils() {
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.pack.PackCodec;
import org.geysermc.geyser.api.pack.PathPackCodec;
import org.geysermc.geyser.api.pack.ResourcePack;
import org.geysermc.geyser.api.pack.ResourcePackManifest;
import org.geysermc.geyser.api.pack.UrlPackCodec;
import org.geysermc.geyser.api.pack.exception.ResourcePackException;
import org.geysermc.geyser.api.pack.option.PriorityOption;
import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl;
import org.geysermc.geyser.pack.GeyserResourcePack;
import org.geysermc.geyser.pack.ResourcePackHolder;
import org.geysermc.geyser.registry.loader.ResourcePackLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public interface GeyserIntegratedPackUtil {
Path CACHE = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
Path PACK_PATH = CACHE.resolve("GeyserIntegratedPack.mcpack");
UUID OPTIONAL_PACK_UUID = UUID.fromString("e5f5c938-a701-11eb-b2a3-047d7bb283ba");
UUID INTEGRATED_PACK_UUID = UUID.fromString("2254393d-8430-45b0-838a-bd397828c765");
AtomicReference<ResourcePackManifest.Version> INTEGRATED_PACK_VERSION = new AtomicReference<>();
AtomicBoolean PACK_ENABLED = new AtomicBoolean(GeyserImpl.getInstance().config().gameplay().enableIntegratedPack());
default void registerGeyserPack(GeyserDefineResourcePacksEventImpl event) {
if (!GeyserImpl.getInstance().config().gameplay().enableIntegratedPack()) {
return;
}
try {
Files.createDirectories(CACHE);
Files.copy(GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("GeyserIntegratedPack.mcpack"),
PACK_PATH, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Could not copy over Geyser integrated resource pack!", e);
PACK_ENABLED.set(false);
return;
}
GeyserResourcePack integrated = ResourcePackLoader.readPack(PACK_PATH).build();
INTEGRATED_PACK_VERSION.set(integrated.manifest().header().version());
try {
event.getPacks().put(INTEGRATED_PACK_UUID, ResourcePackHolder.of(integrated));
event.registerOptions(INTEGRATED_PACK_UUID, PriorityOption.LOW);
PACK_ENABLED.set(true);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Could not register GeyserIntegratedPack!", e);
PACK_ENABLED.set(false);
}
}
/**
* Pre-processes a resource pack about to be registered
* @param pack the pack to check
* @throws ResourcePackException if duplicate was discovered
*/
default void preProcessPack(GeyserResourcePack pack) {
if (!PACK_ENABLED.get()) {
return;
}
if (Objects.equals(pack.uuid(), INTEGRATED_PACK_UUID)) {
handleDuplicateIntegratedPack(pack);
return;
}
if (Objects.equals(pack.uuid(), OPTIONAL_PACK_UUID)) {
handleOptionalPack(pack);
}
}
default void handleDuplicateIntegratedPack(ResourcePack duplicate) {
ResourcePackManifest.Version version = duplicate.manifest().header().version();
if (duplicate.codec() instanceof UrlPackCodec) {
if (Objects.equals(version, INTEGRATED_PACK_VERSION.get())) {
GeyserImpl.getInstance().getLogger().debug("Found GeyserIntegratedPack sent via UrlPackCodec (version: %s)!".formatted(version));
} else {
GeyserImpl.getInstance().getLogger().warning("Found GeyserIntegratedPack sent via UrlPackCodec, but the version differs! " +
"(found: %s, expected: %s). Skipping our own, but things may not work as expected!".formatted(duplicate, INTEGRATED_PACK_VERSION.get()));
}
unregisterIntegratedPack();
PACK_ENABLED.set(true);
return;
}
throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE);
}
default void handleOptionalPack(ResourcePack pack) {
// Gracefully handle optional pack presence to avoid issues
if (pack.codec() instanceof UrlPackCodec) {
GeyserImpl.getInstance().getLogger().warning("Detected GeyserOptionalPack sent via the UrlPackCodec! Please migrate to sending the " +
"GeyserIntegratedPack instead - it will be required in the future for advanced features to work correctly!");
} else {
GeyserImpl.getInstance().getLogger().warning("Detected GeyserOptionalPack! " +
"It should be removed " + warnMessageLocation(pack.codec()) + ", as Geyser now includes an improved version of this resource pack by default!"
);
}
GeyserImpl.getInstance().getLogger().warning("Disabling the integrated pack...");
unregisterIntegratedPack();
PACK_ENABLED.set(false);
}
default String warnMessageLocation(PackCodec codec) {
if (codec instanceof PathPackCodec pathPackCodec) {
try {
// try to create nicer /packs/xyz.mcpack path if possible
return "(found in: %s)".formatted(GeyserImpl.getInstance().getBootstrap().getConfigFolder().relativize(pathPackCodec.path()));
} catch (Exception e) {
return "(found in: %s)".formatted(pathPackCodec.path());
}
}
return "(registered with codec: %s)".formatted(codec);
}
void unregisterIntegratedPack();
boolean integratedPackRegistered();
default boolean isIntegratedPackActive() {
return GeyserImpl.getInstance().config().gameplay().enableIntegratedPack() && integratedPackRegistered();
}
}

View File

@@ -268,7 +268,7 @@ public class InventoryUtils {
}
public static boolean canStack(GeyserItemStack item1, GeyserItemStack item2) {
if (GeyserImpl.getInstance().getConfig().isDebugMode())
if (GeyserImpl.getInstance().config().debugMode())
canStackDebug(item1, item2);
if (item1.isEmpty() || item2.isEmpty())
return false;
@@ -320,7 +320,7 @@ public class InventoryUtils {
private static ItemDefinition getUnusableSpaceBlockDefinition(int protocolVersion) {
ItemMappings mappings = Registries.ITEMS.forVersion(protocolVersion);
String unusableSpaceBlock = GeyserImpl.getInstance().getConfig().getUnusableSpaceBlock();
String unusableSpaceBlock = GeyserImpl.getInstance().config().gameplay().unusableSpaceBlock();
ItemDefinition itemDefinition = mappings.getDefinition(unusableSpaceBlock);
if (itemDefinition == null) {

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import marcono1234.gson.recordadapter.RecordTypeAdapterFactory;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.pack.GeyserResourcePackManifest;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
public final class JsonUtils {
public static <T> T fromJson(byte[] bytes, Class<T> type) {
return GeyserImpl.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), type);
}
public static JsonObject fromJson(InputStream stream) {
return (JsonObject) new JsonParser().parse(new InputStreamReader(stream));
}
public static JsonObject parseJson(String s) {
return (JsonObject) new JsonParser().parse(s);
}
public static <T> T fromJson(InputStream stream, Type type) {
return GeyserImpl.GSON.fromJson(new InputStreamReader(stream), type);
}
public static Gson createGson() {
GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
try {
new Gson().fromJson("{\"version\":[1,0,0],\"uuid\":\"eebb4ea8-a701-11eb-95ba-047d7bb283ba\"}", GeyserResourcePackManifest.Dependency.class);
} catch (Throwable e) {
// 1.16.5 and 1.17.1 (at minimum) have an outdated Gson version that doesn't support records.
// Remove this workaround when all platforms support Gson 2.10+
// (Explicitly allow missing component values - the dependencies module for resource packs, for example, can be missing)
builder.registerTypeAdapterFactory(RecordTypeAdapterFactory.builder().allowMissingComponentValues().create())
// Since this is a record, the above will take precedence unless we explicitly declare it.
.registerTypeAdapter(GeyserResourcePackManifest.Version.class, new GeyserResourcePackManifest.Version.VersionDeserializer());
}
return builder.create();
}
private JsonUtils() {
}
}

View File

@@ -25,9 +25,6 @@
package org.geysermc.geyser.util;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.raphimc.minecraftauth.msa.model.MsaDeviceCode;
import org.cloudburstmc.protocol.bedrock.data.auth.AuthPayload;
import org.cloudburstmc.protocol.bedrock.data.auth.CertificateChainPayload;
@@ -55,8 +52,6 @@ import java.security.PublicKey;
import java.util.function.BiConsumer;
public class LoginEncryptionUtils {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
private static boolean HAS_SENT_ENCRYPTION_MESSAGE = false;
public static void encryptPlayerConnection(GeyserSession session, LoginPacket loginPacket) {
@@ -71,7 +66,7 @@ public class LoginEncryptionUtils {
geyser.getLogger().debug(String.format("Is player data signed? %s", result.signed()));
if (!result.signed() && !session.getGeyser().getConfig().isEnableProxyConnections()) {
if (!result.signed() && session.getGeyser().config().advanced().bedrock().validateBedrockLogin()) {
session.disconnect(GeyserLocale.getLocaleStringLog("geyser.network.remote.invalid_xbox_account"));
return;
}
@@ -97,8 +92,7 @@ public class LoginEncryptionUtils {
throw new IllegalStateException("Client data isn't signed by the given chain data");
}
JsonNode clientDataJson = JSON_MAPPER.readTree(clientDataPayload);
BedrockClientData data = JSON_MAPPER.convertValue(clientDataJson, BedrockClientData.class);
BedrockClientData data = JsonUtils.fromJson(clientDataPayload, BedrockClientData.class);
data.setOriginalString(jwt);
session.setClientData(data);
@@ -106,7 +100,7 @@ public class LoginEncryptionUtils {
startEncryptionHandshake(session, identityPublicKey);
} catch (Throwable e) {
// An error can be thrown on older Java 8 versions about an invalid key
if (geyser.getConfig().isDebugMode()) {
if (geyser.config().debugMode()) {
e.printStackTrace();
}
@@ -225,7 +219,7 @@ public class LoginEncryptionUtils {
.append("\n%xbox.signin.enterCode\n")
.append(ChatColor.GREEN)
.append(msCode.getUserCode());
int timeout = session.getGeyser().getConfig().getPendingAuthenticationTimeout();
int timeout = session.getGeyser().config().pendingAuthenticationTimeout();
if (timeout != 0) {
message.append("\n\n")
.append(ChatColor.RESET)

View File

@@ -1,447 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import javax.net.ssl.HttpsURLConnection;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;
/**
* bStats collects some data for plugin authors.
* <p>
* Check out <a href="https://bStats.org/">bStats</a> to learn more about bStats!
*/
public class Metrics {
// The version of this bStats class
public static final int B_STATS_VERSION = 1;
// The url to which the data is sent
private static final String URL = "https://bStats.org/submitData/server-implementation";
// Should failed requests be logged?
private static boolean logFailedRequests = false;
// The logger for the failed requests
private static Logger logger = Logger.getLogger("bStats");
// The name of the server software
private final String name;
// The uuid of the server
private final String serverUUID;
// A list with all custom charts
private final List<CustomChart> charts = new ArrayList<>();
private final static ObjectMapper mapper = new ObjectMapper();
private final GeyserImpl geyser;
/**
* Class constructor.
*
* @param geyser The Geyser instance
* @param name The name of the server software.
* @param serverUUID The uuid of the server.
* @param logFailedRequests Whether failed requests should be logged or not.
* @param logger The logger for the failed requests.
*/
public Metrics(GeyserImpl geyser, String name, String serverUUID, boolean logFailedRequests, Logger logger) {
this.geyser = geyser;
this.name = name;
this.serverUUID = serverUUID;
Metrics.logFailedRequests = logFailedRequests;
Metrics.logger = logger;
// Start submitting the data
startSubmitting();
}
/**
* Adds a custom chart.
*
* @param chart The chart to add.
*/
public void addCustomChart(CustomChart chart) {
if (chart == null) {
throw new IllegalArgumentException("Chart cannot be null!");
}
charts.add(chart);
}
/**
* Starts the Scheduler which submits our data every 30 minutes.
*/
private void startSubmitting() {
geyser.getScheduledThread().scheduleAtFixedRate(this::submitData, 1, 30, TimeUnit.MINUTES);
// Submit the data every 30 minutes, first time after 1 minutes to give other plugins enough time to start
// WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted!
// WARNING: Just don't do it!
}
/**
* Gets the plugin specific data.
*
* @return The plugin specific data.
*/
private ObjectNode getPluginData() {
ObjectNode data = mapper.createObjectNode();
data.put("pluginName", name); // Append the name of the server software
data.put("pluginVersion", GeyserImpl.VERSION); // Append the name of the server software
ArrayNode customCharts = mapper.createArrayNode();
for (CustomChart customChart : charts) {
// Add the data of the custom charts
JsonNode chart = customChart.getRequestJsonNode();
if (chart == null) { // If the chart is null, we skip it
continue;
}
customCharts.add(chart);
}
data.set("customCharts", customCharts);
return data;
}
/**
* Gets the server specific data.
*
* @return The server specific data.
*/
private ObjectNode getServerData() {
// 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();
ObjectNode data = mapper.createObjectNode();
data.put("serverUUID", serverUUID);
data.put("osName", osName);
data.put("osArch", osArch);
data.put("osVersion", osVersion);
data.put("coreCount", coreCount);
return data;
}
/**
* Collects the data and sends it afterwards.
*/
private void submitData() {
final ObjectNode data = getServerData();
ArrayNode pluginData = mapper.createArrayNode();
pluginData.add(getPluginData());
data.putPOJO("plugins", pluginData);
new Thread(() -> {
try {
// We are still in the Thread of the timer, so nothing get blocked :)
sendData(data);
} catch (Exception e) {
// Something went wrong! :(
if (logFailedRequests) {
logger.log(Level.WARNING, "Could not submit stats of " + name, e);
}
}
}).start();
}
/**
* Sends the data to the bStats server.
*
* @param data The data to send.
* @throws Exception If the request failed.
*/
private static void sendData(ObjectNode data) throws Exception {
if (data == null) {
throw new IllegalArgumentException("Data cannot be null!");
}
HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();
// Compress the data to save bandwidth
byte[] compressedData = compress(data.toString());
// Add headers
connection.setRequestMethod("POST");
connection.addRequestProperty("Accept", "application/json");
connection.addRequestProperty("Connection", "close");
connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format
connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);
// Send data
connection.setDoOutput(true);
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
outputStream.write(compressedData);
outputStream.flush();
outputStream.close();
connection.getInputStream().close(); // We don't care about the response - Just send our data :)
}
/**
* Gzips the given String.
*
* @param str The string to gzip.
* @return The gzipped String.
* @throws IOException If the compression failed.
*/
private static byte @NonNull [] compress(final @NonNull String str) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
gzip.write(str.getBytes(StandardCharsets.UTF_8));
gzip.close();
return outputStream.toByteArray();
}
/**
* Represents a custom chart.
*/
public static abstract class CustomChart {
// The id of the chart
final String chartId;
/**
* Class constructor.
*
* @param chartId The id of the chart.
*/
CustomChart(String chartId) {
if (chartId == null || chartId.isEmpty()) {
throw new IllegalArgumentException("ChartId cannot be null or empty!");
}
this.chartId = chartId;
}
private @Nullable ObjectNode getRequestJsonNode() {
ObjectNode chart = new ObjectMapper().createObjectNode();
chart.put("chartId", chartId);
try {
ObjectNode data = getChartData();
if (data == null) {
// If the data is null we don't send the chart.
return null;
}
chart.putPOJO("data", data);
} catch (Throwable t) {
if (logFailedRequests) {
logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t);
}
return null;
}
return chart;
}
protected abstract ObjectNode getChartData() throws Exception;
}
/**
* Represents a custom simple pie.
*/
public static class SimplePie extends CustomChart {
private final Callable<String> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimplePie(String chartId, Callable<String> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected @Nullable ObjectNode getChartData() throws Exception {
ObjectNode data = mapper.createObjectNode();
String value = callable.call();
if (value == null || value.isEmpty()) {
// Null = skip the chart
return null;
}
data.put("value", value);
return data;
}
}
/**
* Represents a custom advanced pie.
*/
public static class AdvancedPie extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected @Nullable ObjectNode getChartData() throws Exception {
ObjectNode data = mapper.createObjectNode();
ObjectNode values = mapper.createObjectNode();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
continue; // Skip this invalid
}
allSkipped = false;
values.put(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
data.putPOJO("values", values);
return data;
}
}
/**
* Represents a custom drilldown pie.
*/
public static class DrilldownPie extends CustomChart {
private final Callable<Map<String, Map<String, Integer>>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
super(chartId);
this.callable = callable;
}
@Override
public @Nullable ObjectNode getChartData() throws Exception {
ObjectNode data = mapper.createObjectNode();
ObjectNode values = mapper.createObjectNode();
Map<String, Map<String, Integer>> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean reallyAllSkipped = true;
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
ObjectNode value = mapper.createObjectNode();
boolean allSkipped = true;
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
value.put(valueEntry.getKey(), valueEntry.getValue());
allSkipped = false;
}
if (!allSkipped) {
reallyAllSkipped = false;
values.putPOJO(entryValues.getKey(), value);
}
}
if (reallyAllSkipped) {
// Null = skip the chart
return null;
}
data.putPOJO("values", values);
return data;
}
}
/**
* Represents a custom single line chart.
*/
public static class SingleLineChart extends CustomChart {
private final Callable<Integer> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SingleLineChart(String chartId, Callable<Integer> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected @Nullable ObjectNode getChartData() throws Exception {
ObjectNode data = mapper.createObjectNode();
int value = callable.call();
if (value == 0) {
// Null = skip the chart
return null;
}
data.put("value", value);
return data;
}
}
}

View File

@@ -54,8 +54,8 @@ public class SettingsUtils {
// Let's store these to avoid issues
boolean showCoordinates = session.getPreferencesCache().isAllowShowCoordinates();
boolean cooldownShown = CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED;
boolean customSkulls = session.getGeyser().getConfig().isAllowCustomSkulls();
boolean cooldownShown = session.getGeyser().config().gameplay().showCooldown() != CooldownUtils.CooldownType.DISABLED;
boolean customSkulls = session.getGeyser().config().gameplay().maxVisibleCustomSkulls() != 0;
// Only show the client title if any of the client settings are available
boolean showClientSettings = showCoordinates || cooldownShown || customSkulls;

View File

@@ -25,7 +25,7 @@
package org.geysermc.geyser.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonObject;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.event.ClickEvent;
@@ -49,7 +49,7 @@ import java.util.regex.Pattern;
public final class VersionCheckUtils {
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static @NonNull OptionalInt LATEST_BEDROCK_RELEASE = OptionalInt.empty();
private static final int SUPPORTED_JAVA_VERSION = 17;
private static final int SUPPORTED_JAVA_VERSION = 21;
public static void checkForOutdatedFloodgate(GeyserLogger logger) {
try {
@@ -93,9 +93,9 @@ public final class VersionCheckUtils {
public static void checkForGeyserUpdate(Supplier<GeyserCommandSource> recipient) {
CompletableFuture.runAsync(() -> {
try {
JsonNode json = WebUtils.getJson("https://api.geysermc.org/v2/versions/geyser");
JsonNode bedrock = json.get("bedrock").get("protocol");
int protocolVersion = bedrock.get("id").asInt();
JsonObject json = WebUtils.getJson("https://api.geysermc.org/v2/versions/geyser");
JsonObject bedrock = json.getAsJsonObject("bedrock").getAsJsonObject("protocol");
int protocolVersion = bedrock.get("id").getAsInt();
if (GameProtocol.getBedrockCodec(protocolVersion) != null) {
LATEST_BEDROCK_RELEASE = OptionalInt.empty();
// We support the latest version! No need to print a message.
@@ -103,7 +103,7 @@ public final class VersionCheckUtils {
}
LATEST_BEDROCK_RELEASE = OptionalInt.of(protocolVersion);
final String newBedrockVersion = bedrock.get("name").asText();
final String newBedrockVersion = bedrock.get("name").getAsString();
// Delayed for two reasons: save unnecessary processing, and wait to load locale if this is on join.
GeyserCommandSource sender = recipient.get();

View File

@@ -25,8 +25,10 @@
package org.geysermc.geyser.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.checkerframework.checker.nullness.qual.NonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
@@ -84,18 +86,22 @@ public class WebUtils {
}
/**
* Makes a web request to the given URL and returns the body as a {@link JsonNode}.
* Makes a web request to the given URL and returns the body as a {@link JsonObject}.
*
* @param reqURL URL to fetch
* @return the response as JSON
*/
public static JsonNode getJson(String reqURL) throws IOException {
public static JsonObject getJson(String reqURL) throws IOException {
HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection();
con.setRequestProperty("User-Agent", getUserAgent());
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
checkResponseCode(con);
return GeyserImpl.JSON_MAPPER.readTree(con.getInputStream());
try (InputStreamReader isr = new InputStreamReader(con.getInputStream());
JsonReader reader = GeyserImpl.GSON.newJsonReader(isr)) {
//noinspection deprecation
return new JsonParser().parse(reader).getAsJsonObject();
}
}
/**
@@ -105,14 +111,24 @@ public class WebUtils {
* @param fileLocation Location to save on disk
*/
public static void downloadFile(String reqURL, String fileLocation) {
downloadFile(reqURL, Paths.get(fileLocation));
}
/**
* Downloads a file from the given URL and saves it to disk
*
* @param reqURL File to fetch
* @param path Location to save on disk as a path
*/
public static void downloadFile(String reqURL, Path path) {
try {
HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection();
con.setRequestProperty("User-Agent", getUserAgent());
checkResponseCode(con);
InputStream in = con.getInputStream();
Files.copy(in, Paths.get(fileLocation), StandardCopyOption.REPLACE_EXISTING);
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
throw new RuntimeException("Unable to download and save file: " + fileLocation + " (" + reqURL + ")", e);
throw new RuntimeException("Unable to download and save file: " + path.toAbsolutePath() + " (" + reqURL + ")", e);
}
}
@@ -132,7 +148,7 @@ public class WebUtils {
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION);
con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().platformType().platformName() + "/" + GeyserImpl.VERSION);
con.setInstanceFollowRedirects(true);
int responseCode = con.getResponseCode();
@@ -362,7 +378,7 @@ public class WebUtils {
return ((String) attr.get(0)).split(" ");
}
} catch (Exception | NoClassDefFoundError ex) { // Check for a NoClassDefFoundError to prevent Android crashes
if (geyser.getConfig().isDebugMode()) {
if (geyser.config().debugMode()) {
geyser.getLogger().debug("Exception while trying to find an SRV record for the remote host.");
ex.printStackTrace(); // Otherwise we can get a stack trace for any domain that doesn't have an SRV record
}
@@ -393,6 +409,6 @@ public class WebUtils {
}
public static String getUserAgent() {
return "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION;
return "Geyser-" + GeyserImpl.getInstance().platformType().platformName() + "/" + GeyserImpl.VERSION;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util.metrics;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.PlatformType;
public interface MetricsPlatform {
boolean enabled();
String serverUuid();
boolean logFailedRequests();
boolean logSentData();
boolean logResponseStatusText();
// We're not relocating on Geyser-Standalone, and using JiJ on modded platforms
default boolean disableRelocateCheck() {
PlatformType platformType = GeyserImpl.getInstance().platformType();
return platformType == PlatformType.FABRIC ||
platformType == PlatformType.NEOFORGE ||
platformType == PlatformType.STANDALONE;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util.metrics;
import org.geysermc.geyser.GeyserImpl;
public final class ProvidedMetricsPlatform implements MetricsPlatform {
@Override
public boolean enabled() {
return GeyserImpl.getInstance().config().enableMetrics();
}
@Override
public String serverUuid() {
return GeyserImpl.getInstance().config().metricsUuid().toString();
}
@Override
public boolean logFailedRequests() {
return GeyserImpl.getInstance().config().debugMode();
}
@Override
public boolean logSentData() {
return GeyserImpl.getInstance().config().debugMode();
}
@Override
public boolean logResponseStatusText() {
return GeyserImpl.getInstance().config().debugMode();
}
}

Binary file not shown.

View File

@@ -0,0 +1,179 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.configuration;
import lombok.SneakyThrows;
import org.geysermc.geyser.api.util.PlatformType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.util.CheckedConsumer;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ConfigLoaderTest {
private static final String CONFIG_PREFIX = "configuration";
@TempDir
Path tempDirectory;
@Test
void testCreateNewConfig() throws Exception {
// Test that the result of generating a config, and the result of reading it back after writing it, is the same
File file = tempDirectory.resolve("config.yml").toFile();
forAllConfigs(type -> {
CommentedConfigurationNode config1 = new ConfigLoader(file).loadConfigurationNode(type, PlatformType.STANDALONE);
long initialModification = file.lastModified();
assertTrue(file.exists()); // should have been created
List<String> firstContents = Files.readAllLines(file.toPath());
CommentedConfigurationNode config2 = new ConfigLoader(file).loadConfigurationNode(type, PlatformType.STANDALONE);
List<String> secondContents = Files.readAllLines(file.toPath());
assertEquals(initialModification, file.lastModified()); // should not have been touched
assertEquals(firstContents, secondContents);
// Must ignore this, as when the config is read back, the header is interpreted as a comment on the first node in the map
config1.node("java").comment(null);
config2.node("java").comment(null);
assertEquals(config1, config2);
});
}
@Test
void testDefaultConfigMigration() throws Exception {
testConfiguration("default");
}
@Test
void testAllChangedConfigMigration() throws Exception {
testConfiguration("all-changed");
}
@Test
void testLegacyConfigMigration() throws Exception {
testConfiguration("legacy");
}
@Test
void allowCustomSkullsMigration() throws Exception {
testConfiguration("allow-custom-skulls");
}
@Test
void testNoEmotesMigration() throws Exception {
testConfiguration("migrate-no-emotes");
}
@Test
void testChewsOldConfig() throws Exception {
testConfiguration("chew");
}
@Test
void testInvalidConfig() throws Exception {
streamResourceFiles(CONFIG_PREFIX + "/invalid").forEach(resource -> {
try {
forAllConfigs(type -> {
assertThrows(ConfigurateException.class,
() -> new ConfigLoader(resource).loadConfigurationNode(type, getPlatformType(type)),
"Did not get exception while loading %s (file: %s)".formatted(type.getSimpleName(), resource.getName()));
});
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
public void testConfiguration(String folder) throws Exception {
forAllConfigs(type -> {
CommentedConfigurationNode oldTransformed = new ConfigLoader(copyResourceToTempFile(folder, "before.yml"))
.loadConfigurationNode(type, getPlatformType(type));
String configName = type == GeyserRemoteConfig.class ? "remote" : "plugin";
CommentedConfigurationNode newTransformed = new ConfigLoader(copyResourceToTempFile(folder, configName + ".yml"))
.loadConfigurationNode(type, getPlatformType(type));
assertEquals(oldTransformed, newTransformed);
});
}
File copyResourceToTempFile(String... path) throws Exception {
File resource = getConfigResource(CONFIG_PREFIX + "/" + String.join("/", path));
return Files.copy(resource.toPath(), tempDirectory.resolve(resource.getName()), StandardCopyOption.REPLACE_EXISTING).toFile();
}
PlatformType getPlatformType(Class<? extends GeyserConfig> configClass) {
if (configClass == GeyserRemoteConfig.class) {
return PlatformType.STANDALONE;
}
if (configClass == GeyserPluginConfig.class) {
return PlatformType.SPIGOT;
}
throw new IllegalArgumentException("Unsupported config class " + configClass);
}
void forAllConfigs(CheckedConsumer<Class<? extends GeyserConfig>, Exception> consumer) throws Exception {
consumer.accept(GeyserPluginConfig.class);
consumer.accept(GeyserRemoteConfig.class);
}
private static Stream<File> streamResourceFiles(String directory) throws IOException, URISyntaxException {
URL resourceUrl = ConfigLoaderTest.class.getClassLoader().getResource(directory);
Objects.requireNonNull(resourceUrl, "Resource directory not found: " + directory);
Path resourcePath = Path.of(resourceUrl.toURI());
// Walk the directory, but don't go into subdirectories (maxDepth = 1)
return Files.walk(resourcePath, 1)
.filter(path -> !path.equals(resourcePath)) // Exclude the directory itself
.filter(Files::isRegularFile) // Ensure we only get files
.map(Path::toFile);
}
@SneakyThrows
private File getConfigResource(String name) {
URL url = Objects.requireNonNull(getClass().getClassLoader().getResource(name), "No resource for name: " + name);
return Path.of(url.toURI()).toFile();
}
}

View File

@@ -25,20 +25,20 @@
package org.geysermc.geyser.scoreboard.network.util;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.mockito.Mockito;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class GeyserMockContext {
private final List<Object> mocksAndSpies = new ArrayList<>();
@@ -49,11 +49,11 @@ public class GeyserMockContext {
var context = new GeyserMockContext();
var geyserImpl = context.mock(GeyserImpl.class);
var config = context.mock(GeyserConfiguration.class);
when(config.getScoreboardPacketThreshold()).thenReturn(1_000);
when(geyserImpl.getConfig()).thenReturn(config);
var config = context.mock(GeyserConfig.class);
when(geyserImpl.config()).thenReturn(config);
var advancedConfig = context.mock(GeyserConfig.AdvancedConfig.class);
when(config.advanced()).thenReturn(advancedConfig);
when(advancedConfig.scoreboardPacketThreshold()).thenReturn(1_000);
var logger = context.storeObject(new EmptyGeyserLogger());
when(geyserImpl.getLogger()).thenReturn(logger);

View File

@@ -0,0 +1,219 @@
# --------------------------------
# Geyser Configuration File
#
# A bridge between Minecraft: Bedrock Edition and Minecraft: Java Edition.
#
# GitHub: https://github.com/GeyserMC/Geyser
# Discord: https://discord.gg/geysermc
# Wiki: https://wiki.geysermc.org/
#
# NOTICE: See https://wiki.geysermc.org/geyser/setup/ for the setup guide. Many video tutorials are outdated.
# In most cases, especially with server hosting providers, further hosting-specific configuration is required.
# --------------------------------
bedrock:
# The IP address that will listen for connections.
# Generally, you should only uncomment and change this if you want to limit what IPs can connect to your server.
address: 127.0.0.1
# The port that will listen for connections
port: 19122
# Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock.
# This option makes the Bedrock port the same as the Java port every time you start the server.
# This option is for the plugin version only.
clone-remote-port: true
# The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true
# If either of these are empty, the respective string will default to "Geyser"
motd1: "Gayser"
motd2: "Another Gayser server."
# The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.
server-name: "Gayser"
# How much to compress network traffic to the Bedrock client. The higher the number, the more CPU usage used, but
# the smaller the bandwidth used. Does not have any effect below -1 or above 9. Set to -1 to disable.
compression-level: 5
# The port to broadcast to Bedrock clients with the MOTD that they should use to connect to the server.
# DO NOT uncomment and change this unless Geyser runs on a different internal port than the one that is used to connect.
broadcast-port: 19133
# Whether to enable PROXY protocol or not for clients. You DO NOT WANT this feature unless you run UDP reverse proxy
# in front of your Geyser instance.
enable-proxy-protocol: true
# A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and
# should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.).
# Keeping this list empty means there is no IP address whitelist.
# IP addresses, subnets, and links to plain text files are supported.
proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/13" ]
remote:
# The IP address of the remote (Java Edition) server
# If it is "auto", for standalone version the remote address will be set to 127.0.0.1,
# for plugin versions, it is recommended to keep this as "auto" so Geyser will automatically configure address, port, and auth-type.
# Leave as "auto" if floodgate is installed.
address: test.geysermc.org
# The port of the remote (Java Edition) server
# For plugin versions, if address has been set to "auto", the port will also follow the server's listening port.
port: 25564
# Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate).
# For plugin versions, it's recommended to keep the `address` field to "auto" so Floodgate support is automatically configured.
# If Floodgate is installed and `address:` is set to "auto", then "auth-type: floodgate" will automatically be used.
auth-type: floodgate
# Whether to enable PROXY protocol or not while connecting to the server.
# This is useful only when:
# 1) Your server supports PROXY protocol (it probably doesn't)
# 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config.
# IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!
use-proxy-protocol: true
# Forward the hostname that the Bedrock client used to connect over to the Java server
# This is designed to be used for forced hosts on proxies
forward-hostname: true
# Floodgate uses encryption to ensure use from authorised sources.
# This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity)
# You can ignore this when not using Floodgate.
# If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate.
floodgate-key-file: keyyy.pem
# For online mode authentication type only.
# Stores a list of Bedrock players that should have their Java Edition account saved after login.
# This saves a token that can be reused to authenticate the player later. This does not save emails or passwords,
# but you should still be cautious when adding to this list and giving others access to this Geyser instance's files.
# Removing a name from this list will delete its cached login information on the next Geyser startup.
# The file that tokens will be saved in is in the same folder as this config, named "saved-refresh-tokens.json".
saved-user-logins:
- ThisExampleUsername
- ThisOther
# Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account.
# User is allowed to disconnect from the server during this period.
pending-authentication-timeout: 111
# Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands.
# Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.
command-suggestions: false
# The following three options enable "ping passthrough" - the MOTD, player count and/or protocol name gets retrieved from the Java server.
# Relay the MOTD from the remote server to Bedrock players.
passthrough-motd: false
# Relay the player count and max players from the remote server to Bedrock players.
passthrough-player-counts: false
# Enable LEGACY ping passthrough. There is no need to enable this unless your MOTD or player count does not appear properly.
# This option does nothing on standalone.
legacy-ping-passthrough: true
# How often to ping the remote server, in seconds. Only relevant for standalone or legacy ping passthrough.
# Increase if you are getting BrokenPipe errors.
ping-passthrough-interval: 2
# Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate
# ping, it may also cause players to time out more easily.
forward-player-ping: true
# Maximum amount of players that can connect. This is only visual at this time and does not actually limit player count.
max-players: 99
# If debug messages should be sent through console
debug-mode: true
# Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat.
# Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below:
# https://cdn.discordapp.com/attachments/613170125696270357/957075682230419466/Screenshot_from_2022-03-25_20-35-08.png
# This can be disabled by going into Bedrock settings under the accessibility tab and setting "Text Background Opacity" to 0
# This setting can be set to "title", "actionbar" or "false"
show-cooldown: actionbar
# Controls if coordinates are shown to players.
show-coordinates: false
# Whether Bedrock players are blocked from performing their scaffolding-style bridging.
disable-bedrock-scaffolding: true
# If set, when a Bedrock player performs any emote, it will swap the offhand and mainhand items, just like the Java Edition keybind
# There are three options this can be set to:
# disabled - the default/fallback, which doesn't apply this workaround
# no-emotes - emotes will NOT be sent to other Bedrock clients and offhand will be swapped. This effectively disables all emotes from being seen.
# emotes-and-offhand - emotes will be sent to Bedrock clients and offhand will be swapped
emote-offhand-workaround: "emotes-and-offhand"
# The default locale if we dont have the one the client requested. Uncomment to not use the default system language.
default-locale: en_uk
# Specify how many days images will be cached to disk to save downloading them from the internet.
# A value of 0 is disabled. (Default: 0)
cache-images: 10
# Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.
allow-custom-skulls: true
# The maximum number of custom skulls to be displayed per player. Increasing this may decrease performance on weaker devices.
# Setting this to -1 will cause all custom skulls to be displayed regardless of distance or number.
max-visible-custom-skulls: 111
# The radius in blocks around the player in which custom skulls are displayed.
custom-skull-render-distance: 19
# Whether to add any items and blocks which normally does not exist in Bedrock Edition.
# This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching.
# If this is disabled, furnace minecart items will be mapped to hopper minecart items.
# Geyser's block, item, and skull mappings systems will also be disabled.
# This option requires a restart of Geyser in order to change its setting.
add-non-bedrock-items: false
# Bedrock prevents building and displaying blocks above Y127 in the Nether.
# This config option works around that by changing the Nether dimension ID to the End ID.
# The main downside to this is that the entire Nether will have the same red fog rather than having different fog for each biome.
above-bedrock-nether-building: true
# Force clients to load all resource packs if there are any.
# If set to false, it allows the user to connect to the server even if they don't
# want to download the resource packs.
force-resource-packs: false
# Allows Xbox achievements to be unlocked.
xbox-achievements-enabled: true
# Whether player IP addresses will be logged by the server.
log-player-ip-addresses: false
# Whether to alert the console and operators that a new Geyser version is available that supports a Bedrock version
# that this Geyser version does not support. It's recommended to keep this option enabled, as many Bedrock platforms
# auto-update.
notify-on-new-bedrock-update: false
# Which item to use to mark unavailable slots in a Bedrock player inventory. Examples of this are the 2x2 crafting grid while in creative,
# or custom inventory menus with sizes different from the usual 3x9. A barrier block is the default item.
unusable-space-block: minecraft:apple
# bStats is a stat tracker that is entirely anonymous and tracks only basic information
# about Geyser, such as how many people are online, how many servers are using Geyser,
# what OS is being used, etc. You can learn more about bStats here: https://bstats.org/.
# https://bstats.org/plugin/server-implementation/GeyserMC
metrics:
# If metrics should be enabled
enabled: false
# UUID of server, don't change!
uuid: 00000000-0000-0000-0000-000000000000
# ADVANCED OPTIONS - DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING!
# Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle
# a lot of scoreboard packets per second can cause serious lag.
# This option allows you to specify after how many Scoreboard packets per seconds
# the Scoreboard updates will be limited to four updates per second.
scoreboard-packet-threshold: 10
# Allow connections from ProxyPass and Waterdog.
# See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP.
enable-proxy-connections: true
# The internet supports a maximum MTU of 1492 but could cause issues with packet fragmentation.
# 1400 is the default.
mtu: 1399
# Whether to connect directly into the Java server without creating a TCP connection.
# This should only be disabled if a plugin that interfaces with packets or the network does not work correctly with Geyser.
# If enabled on plugin versions, the remote address and port sections are ignored
# If disabled on plugin versions, expect performance decrease and latency increase
use-direct-connection: false
# Whether Geyser should attempt to disable compression for Bedrock players. This should be a benefit as there is no need to compress data
# when Java packets aren't being handled over the network.
# This requires use-direct-connection to be true.
disable-compression: false
config-version: 4

View File

@@ -0,0 +1,239 @@
# --------------------------------
# Geyser Configuration File
#
# A bridge between Minecraft: Bedrock Edition and Minecraft: Java Edition.
#
# GitHub: https://github.com/GeyserMC/Geyser
# Discord: https://discord.gg/geysermc
# Wiki: https://geysermc.org/wiki
#
# NOTICE: See https://geysermc.org/wiki/geyser/setup/ for the setup guide. Many video tutorials are outdated.
# In most cases, especially with server hosting providers, further hosting-specific configuration is required.
# --------------------------------
# Network settings for the Bedrock listener
bedrock:
# The IP address that Geyser will bind on to listen for incoming Bedrock connections.
# Generally, you should only change this if you want to limit what IPs can connect to your server.
address: 127.0.0.1
# The port that will Geyser will listen on for incoming Bedrock connections.
# Since Minecraft: Bedrock Edition uses UDP, this port must allow UDP traffic.
port: 19122
# Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock.
# This option makes the Bedrock port the same as the Java port every time you start the server.
clone-remote-port: true
# Network settings for the Java server connection
java:
# What type of authentication Bedrock players will be checked against when logging into the Java server.
# Can be "floodgate" (see https://wiki.geysermc.org/floodgate/), "online", or "offline".
auth-type: floodgate
# MOTD settings
motd:
# The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true.
# If either of these are empty, the respective string will default to "Geyser"
primary-motd: Gayser
secondary-motd: Another Gayser server.
# Whether Geyser should relay the MOTD from the Java server to Bedrock players.
passthrough-motd: false
# Maximum amount of players that can connect.
# This is only visual, and is only applied if passthrough-motd is disabled.
max-players: 99
# Whether to relay the player count and max players from the Java server to Bedrock players.
passthrough-player-counts: false
# Whether to use server API methods to determine the Java server's MOTD and ping passthrough.
# There is no need to disable this unless your MOTD or player count does not appear properly.
integrated-ping-passthrough: false
# How often to ping the Java server to refresh MOTD and player count, in seconds.
# Only relevant if integrated-ping-passthrough is disabled.
ping-passthrough-interval: 2
# Gameplay options that affect Bedrock players
gameplay:
# The server name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.
server-name: Gayser
# Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat.
# Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below:
# https://geysermc.org/img/external/cooldown_indicator.png
# This can be disabled by going into Bedrock settings under the accessibility tab and setting "Text Background Opacity" to 0
# This setting can be set to "title", "actionbar" or "false"
show-cooldown: actionbar
# Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands.
# Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.
command-suggestions: false
# Controls if coordinates are shown to players.
show-coordinates: false
# Whether Bedrock players are blocked from performing their scaffolding-style bridging.
disable-bedrock-scaffolding: true
# Bedrock prevents building and displaying blocks above Y127 in the Nether.
# This config option works around that by changing the Nether dimension ID to the End ID.
# The main downside to this is that the entire Nether will have the same red fog rather than having different fog for each biome.
nether-roof-workaround: true
# Whether to show Bedrock Edition emotes to other Bedrock Edition players.
show-emotes: true
# Which item to use to mark unavailable slots in a Bedrock player inventory. Examples of this are the 2x2 crafting grid while in creative,
# or custom inventory menus with sizes different from the usual 3x9. A barrier block is the default item.
# This config option can be set to any Bedrock item identifier. If you want to set this to a custom item, make sure that you specify the item in the following format: "geyser_custom:<mapping-name>"
unusable-space-block: minecraft:apple
# Whether to add any items and blocks which normally does not exist in Bedrock Edition.
# This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching.
# If this is disabled, furnace minecart items will be mapped to hopper minecart items.
# Geyser's block, item, and skull mappings systems will also be disabled.
# This option requires a restart of Geyser in order to change its setting.
enable-custom-content: false
# Force clients to load all resource packs if there are any.
# If set to false, it allows the user to connect to the server even if they don't
# want to download the resource packs.
force-resource-packs: false
# Whether to automatically serve a resource pack that is required for some Geyser features to all connecting Bedrock players.
# If enabled, force-resource-packs will be enabled.
enable-integrated-pack: true
# Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate
# ping, it may also cause players to time out more easily.
forward-player-ping: true
# Allows Xbox achievements to be unlocked.
# If a player types in an unknown command, they will receive a message that states cheats are disabled.
# Otherwise, commands work as expected.
xbox-achievements-enabled: true
# The maximum number of custom skulls to be displayed per player. Increasing this may decrease performance on weaker devices.
# A value of 0 will disable all custom skulls.
# Setting this to -1 will cause all custom skulls to be displayed regardless of distance or number.
max-visible-custom-skulls: 111
# The radius in blocks around the player in which custom skulls are displayed.
custom-skull-render-distance: 19
# The default locale if we don't have the one the client requested. If set to "system", the system's language will be used.
default-locale: en_uk
# Whether player IP addresses will be logged by the server.
log-player-ip-addresses: false
# For online mode authentication type only.
# Stores a list of Bedrock player usernames that should have their Java Edition account saved after login.
# This saves a token that can be reused to authenticate the player later. This does not save emails or passwords,
# but you should still be cautious when adding to this list and giving others access to this Geyser instance's files.
# Removing a name from this list will delete its cached login information on the next Geyser startup.
# The file that tokens will be saved in is in the same folder as this config, named "saved-refresh-tokens.json".
saved-user-logins:
- ThisExampleUsername
- ThisOther
# For online mode authentication type only.
# Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account.
# User is allowed to disconnect from the server during this period.
pending-authentication-timeout: 111
# Whether to alert the console and operators that a new Geyser version is available that supports a Bedrock version
# that this Geyser version does not support. It's recommended to keep this option enabled, as many Bedrock platforms
# auto-update.
notify-on-new-bedrock-update: false
# Advanced configuration options. These usually do not need modifications.
advanced:
# Specify how many days player skin images will be cached to disk to save downloading them from the internet.
# A value of 0 is disabled. (Default: 0)
cache-images: 10
# Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle
# a lot of scoreboard packets per second, this can cause serious lag.
# This option allows you to specify after how many Scoreboard packets per seconds
# the Scoreboard updates will be limited to four updates per second.
scoreboard-packet-threshold: 10
# Whether Geyser should send team names in command suggestions.
# Disable this if you have a lot of teams used that you don't need as suggestions.
add-team-suggestions: true
# A list of remote resource pack urls to send to the Bedrock client for downloading.
# The Bedrock client is very picky about how these are delivered - please see our wiki page for further info: https://geysermc.org/wiki/geyser/packs/
resource-pack-urls: []
# Floodgate uses encryption to ensure use from authorized sources.
# This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity)
# You can ignore this when not using Floodgate.
# If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate.
floodgate-key-file: keyyy.pem
# Advanced networking options for the Geyser to Java server connection
java:
# Whether to enable HAPROXY protocol when connecting to the Java server.
# This is useful only when:
# 1) Your Java server supports HAPROXY protocol (it probably doesn't)
# 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config.
# IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!
use-haproxy-protocol: true
# Whether to connect directly into the Java server without creating a TCP connection.
# This should only be disabled if a plugin that interfaces with packets or the network does not work correctly with Geyser.
# If enabled, the remote address and port sections are ignored.
# If disabled, expect performance decrease and latency increase.
use-direct-connection: false
# Whether Geyser should attempt to disable packet compression (from the Java Server to Geyser) for Bedrock players.
# This should be a benefit as there is no need to compress data when Java packets aren't being handled over the network.
# This requires use-direct-connection to be true.
disable-compression: false
# Advanced networking options for Geyser's Bedrock listener
bedrock:
# The port to broadcast to Bedrock clients with the MOTD that they should use to connect to the server.
# A value of 0 will broadcast the port specified above.
# DO NOT change this unless Geyser runs on a different port than the one that is used to connect.
broadcast-port: 19133
# How much to compress network traffic to the Bedrock client. The higher the number, the more CPU usage used, but
# the smaller the bandwidth used. Does not have any effect below -1 or above 9. Set to -1 to disable.
compression-level: 5
# Whether to expect HAPROXY protocol for connecting Bedrock clients.
# This is useful only when you are running a UDP reverse proxy in front of your Geyser instance.
# IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!
use-haproxy-protocol: true
# A list of allowed HAPROXY protocol speaking proxy IP addresses/subnets. Only effective when "use-proxy-protocol" is enabled, and
# should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.).
# Keeping this list empty means there is no IP address whitelist.
# IP addresses, subnets, and links to plain text files are supported.
haproxy-protocol-whitelisted-ips:
- 127.0.0.1
- 172.18.0.0/13
# The internet supports a maximum MTU of 1492 but could cause issues with packet fragmentation.
# 1400 is the default.
mtu: 1399
# This option disables the auth step Geyser performs for connecting Bedrock players.
# It can be used to allow connections from ProxyPass and WaterdogPE. In these cases, make sure that users
# cannot directly connect to this Geyser instance. See https://www.spigotmc.org/wiki/firewall-guide/ for
# assistance - and use UDP instead of TCP.
# Disabling Bedrock authentication for other use-cases is NOT SUPPORTED, as it allows anyone to spoof usernames, and is therefore a security risk.
# All Floodgate functionality (including skin uploading and account linking) will also not work when this option is disabled.
validate-bedrock-login: true
# If debug messages should be sent through console
debug-mode: true
# Do not change!
config-version: 5

Some files were not shown because too many files have changed in this diff Show More