mirror of
https://github.com/GeyserMC/Geyser.git
synced 2025-03-19 21:48:46 +01:00
Initial advanced config
This commit is contained in:
parent
d9d78cd9ca
commit
f113c8967e
21 changed files with 245 additions and 103 deletions
|
@ -142,7 +142,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener {
|
||||||
}
|
}
|
||||||
initChannel.invoke(channelInitializer, ch);
|
initChannel.invoke(channelInitializer, ch);
|
||||||
|
|
||||||
if (bootstrap.config().useDirectConnection()) {
|
if (bootstrap.config().advanced().disableCompression()) {
|
||||||
ch.pipeline().addAfter(PipelineUtils.PACKET_ENCODER, "geyser-compression-disabler",
|
ch.pipeline().addAfter(PipelineUtils.PACKET_ENCODER, "geyser-compression-disabler",
|
||||||
new GeyserBungeeCompressionDisabler());
|
new GeyserBungeeCompressionDisabler());
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ public class GeyserModInjector extends GeyserInjector {
|
||||||
int index = ch.pipeline().names().indexOf("encoder");
|
int index = ch.pipeline().names().indexOf("encoder");
|
||||||
String baseName = index != -1 ? "encoder" : "outbound_config";
|
String baseName = index != -1 ? "encoder" : "outbound_config";
|
||||||
|
|
||||||
if (bootstrap.config().disableCompression()) {
|
if (bootstrap.config().advanced().disableCompression()) {
|
||||||
ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserModCompressionDisabler());
|
ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserModCompressionDisabler());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ public class GeyserSpigotInjector extends GeyserInjector {
|
||||||
int index = ch.pipeline().names().indexOf("encoder");
|
int index = ch.pipeline().names().indexOf("encoder");
|
||||||
String baseName = index != -1 ? "encoder" : "outbound_config";
|
String baseName = index != -1 ? "encoder" : "outbound_config";
|
||||||
|
|
||||||
if (bootstrap.config().disableCompression() && GeyserSpigotCompressionDisabler.ENABLED) {
|
if (bootstrap.config().advanced().disableCompression() && GeyserSpigotCompressionDisabler.ENABLED) {
|
||||||
ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserSpigotCompressionDisabler());
|
ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserSpigotCompressionDisabler());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,7 +287,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Path getFloodgateKeyPath() {
|
public Path getFloodgateKeyPath() {
|
||||||
return Path.of(geyserConfig.floodgateKeyFile());
|
return Path.of(geyserConfig.advanced().floodgateKeyFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -85,7 +85,7 @@ public class GeyserVelocityInjector extends GeyserInjector {
|
||||||
protected void initChannel(@NonNull Channel ch) throws Exception {
|
protected void initChannel(@NonNull Channel ch) throws Exception {
|
||||||
initChannel.invoke(channelInitializer, ch);
|
initChannel.invoke(channelInitializer, ch);
|
||||||
|
|
||||||
if (bootstrap.config().disableCompression() && GeyserVelocityCompressionDisabler.ENABLED) {
|
if (bootstrap.config().advanced().disableCompression() && GeyserVelocityCompressionDisabler.ENABLED) {
|
||||||
ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler",
|
ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler",
|
||||||
new GeyserVelocityCompressionDisabler());
|
new GeyserVelocityCompressionDisabler());
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,7 +232,7 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserPlugin
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Path getFloodgateKeyPath() {
|
public Path getFloodgateKeyPath() {
|
||||||
return new File(ROOT_FOLDER, config.floodgateKeyFile()).toPath();
|
return new File(ROOT_FOLDER, config.advanced().floodgateKeyFile()).toPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
|
|
|
@ -46,6 +46,7 @@ public final class Constants {
|
||||||
public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/";
|
public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/";
|
||||||
|
|
||||||
public static final int CONFIG_VERSION = 5;
|
public static final int CONFIG_VERSION = 5;
|
||||||
|
public static final int ADVANCED_CONFIG_VERSION = 1;
|
||||||
|
|
||||||
public static final int BSTATS_ID = 5273;
|
public static final int BSTATS_ID = 5273;
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class FloodgateKeyLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Path floodgateKey = geyserDataFolder.resolve(config.floodgateKeyFile());
|
Path floodgateKey = geyserDataFolder.resolve(config.advanced().floodgateKeyFile());
|
||||||
|
|
||||||
if (!Files.exists(floodgateKey)) {
|
if (!Files.exists(floodgateKey)) {
|
||||||
logger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed"));
|
logger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed"));
|
||||||
|
|
|
@ -461,10 +461,10 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.metrics().enabled()) {
|
if (config.enableMetrics()) {
|
||||||
metrics = new MetricsBase(
|
metrics = new MetricsBase(
|
||||||
"server-implementation",
|
"server-implementation",
|
||||||
config.metrics().uuid().toString(),
|
config.advanced().metricsUuid().toString(),
|
||||||
Constants.BSTATS_ID,
|
Constants.BSTATS_ID,
|
||||||
true, // Already checked above.
|
true, // Already checked above.
|
||||||
builder -> {
|
builder -> {
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* 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.Constants;
|
||||||
|
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.objectmapping.ConfigSerializable;
|
||||||
|
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ConfigSerializable
|
||||||
|
public interface AdvancedConfig {
|
||||||
|
// 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("""
|
||||||
|
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.""")
|
||||||
|
@DefaultNumeric(128)
|
||||||
|
int maxVisibleCustomSkulls();
|
||||||
|
|
||||||
|
@Comment("The radius in blocks around the player in which custom skulls are displayed.")
|
||||||
|
@DefaultNumeric(32)
|
||||||
|
int customSkullRenderDistance();
|
||||||
|
|
||||||
|
@Comment("""
|
||||||
|
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)""")
|
||||||
|
int cacheImages();
|
||||||
|
|
||||||
|
@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.""")
|
||||||
|
@DefaultString("minecraft:barrier")
|
||||||
|
String unusableSpaceBlock();
|
||||||
|
|
||||||
|
@Comment("""
|
||||||
|
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.""")
|
||||||
|
@DefaultNumeric(20)
|
||||||
|
int scoreboardPacketThreshold();
|
||||||
|
|
||||||
|
@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("""
|
||||||
|
Only for plugin versions of Geyser.
|
||||||
|
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
|
||||||
|
""")
|
||||||
|
@DefaultBoolean(true)
|
||||||
|
boolean useDirectConnection();
|
||||||
|
|
||||||
|
@Comment("""
|
||||||
|
Only for plugin versions of Geyser.
|
||||||
|
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.
|
||||||
|
""")
|
||||||
|
@DefaultBoolean(true)
|
||||||
|
boolean disableCompression();
|
||||||
|
|
||||||
|
@Comment("Do not touch!")
|
||||||
|
default UUID metricsUuid() {
|
||||||
|
return UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Comment("Do not touch!")
|
||||||
|
default int version() {
|
||||||
|
return Constants.ADVANCED_CONFIG_VERSION;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,10 @@
|
||||||
package org.geysermc.geyser.configuration;
|
package org.geysermc.geyser.configuration;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
import org.geysermc.geyser.Constants;
|
||||||
import org.spongepowered.configurate.CommentedConfigurationNode;
|
import org.spongepowered.configurate.CommentedConfigurationNode;
|
||||||
|
import org.spongepowered.configurate.ConfigurationNode;
|
||||||
|
import org.spongepowered.configurate.NodePath;
|
||||||
import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions;
|
import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions;
|
||||||
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
|
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
|
||||||
import org.spongepowered.configurate.yaml.NodeStyle;
|
import org.spongepowered.configurate.yaml.NodeStyle;
|
||||||
|
@ -34,8 +37,10 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.spongepowered.configurate.NodePath.path;
|
import static org.spongepowered.configurate.NodePath.path;
|
||||||
import static org.spongepowered.configurate.transformation.TransformAction.remove;
|
import static org.spongepowered.configurate.transformation.TransformAction.remove;
|
||||||
|
@ -56,20 +61,21 @@ public final class ConfigLoader {
|
||||||
In most cases, especially with server hosting providers, further hosting-specific configuration is required.
|
In most cases, especially with server hosting providers, further hosting-specific configuration is required.
|
||||||
--------------------------------""";
|
--------------------------------""";
|
||||||
|
|
||||||
|
private static final String ADVANCED_HEADER = """
|
||||||
|
--------------------------------
|
||||||
|
Geyser ADVANCED Configuration File
|
||||||
|
|
||||||
|
In most cases, you do *not* need to mess with this file to get Geyser running.
|
||||||
|
Tread with caution.
|
||||||
|
--------------------------------
|
||||||
|
""";
|
||||||
|
|
||||||
public static <T extends GeyserConfig> T load(File file, Class<T> configClass) throws IOException {
|
public static <T extends GeyserConfig> T load(File file, Class<T> configClass) throws IOException {
|
||||||
return load(file, configClass, null);
|
return load(file, configClass, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends GeyserConfig> T load(File file, Class<T> configClass, @Nullable Consumer<CommentedConfigurationNode> transformer) throws IOException {
|
public static <T extends GeyserConfig> T load(File file, Class<T> configClass, @Nullable Consumer<CommentedConfigurationNode> transformer) throws IOException {
|
||||||
var loader = YamlConfigurationLoader.builder()
|
var loader = createLoader(file, HEADER);
|
||||||
.file(file)
|
|
||||||
.indent(2)
|
|
||||||
.nodeStyle(NodeStyle.BLOCK)
|
|
||||||
.defaultOptions(options -> InterfaceDefaultOptions.addTo(options)
|
|
||||||
.shouldCopyDefaults(false) // If we use ConfigurationNode#get(type, default), do not write the default back to the node.
|
|
||||||
.header(HEADER)
|
|
||||||
.serializers(builder -> builder.register(new LowercaseEnumSerializer())))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
CommentedConfigurationNode node = loader.load();
|
CommentedConfigurationNode node = loader.load();
|
||||||
boolean originallyEmpty = !file.exists() || node.isNull();
|
boolean originallyEmpty = !file.exists() || node.isNull();
|
||||||
|
@ -111,10 +117,21 @@ public final class ConfigLoader {
|
||||||
.addAction(path("metrics", "uuid"), (path, value) -> {
|
.addAction(path("metrics", "uuid"), (path, value) -> {
|
||||||
if ("generateduuid".equals(value.getString())) {
|
if ("generateduuid".equals(value.getString())) {
|
||||||
// Manually copied config without Metrics UUID creation?
|
// Manually copied config without Metrics UUID creation?
|
||||||
return new Object[]{UUID.randomUUID()};
|
value.set(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
.addAction(path("remote", "address"), (path, value) -> {
|
||||||
|
if ("auto".equals(value.getString())) {
|
||||||
|
// Auto-convert back to localhost
|
||||||
|
value.set("127.0.0.1");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.addAction(path("metrics", "enabled"), (path, value) -> {
|
||||||
|
// Move to the root, not in the Metrics class.
|
||||||
|
return new Object[]{"enable-metrics"};
|
||||||
|
})
|
||||||
.addAction(path("bedrock", "motd1"), rename("primary-motd"))
|
.addAction(path("bedrock", "motd1"), rename("primary-motd"))
|
||||||
.addAction(path("bedrock", "motd2"), rename("secondary-motd"))
|
.addAction(path("bedrock", "motd2"), rename("secondary-motd"))
|
||||||
// Legacy config values
|
// Legacy config values
|
||||||
|
@ -139,6 +156,11 @@ public final class ConfigLoader {
|
||||||
CommentedConfigurationNode newRoot = CommentedConfigurationNode.root(loader.defaultOptions());
|
CommentedConfigurationNode newRoot = CommentedConfigurationNode.root(loader.defaultOptions());
|
||||||
newRoot.set(config);
|
newRoot.set(config);
|
||||||
|
|
||||||
|
// Create the path in a way that Standalone changing the config name will be fine.
|
||||||
|
int extensionIndex = file.getName().lastIndexOf(".");
|
||||||
|
File advancedConfigPath = new File(file.getParent(), file.getName().substring(0, extensionIndex) + "_advanced" + file.getName().substring(extensionIndex));
|
||||||
|
AdvancedConfig advancedConfig = null;
|
||||||
|
|
||||||
if (originallyEmpty || currentVersion != newVersion) {
|
if (originallyEmpty || currentVersion != newVersion) {
|
||||||
|
|
||||||
if (!originallyEmpty && currentVersion > 4) {
|
if (!originallyEmpty && currentVersion > 4) {
|
||||||
|
@ -148,10 +170,15 @@ public final class ConfigLoader {
|
||||||
// These get treated as comments on lower nodes, which produces very undesirable results.
|
// These get treated as comments on lower nodes, which produces very undesirable results.
|
||||||
|
|
||||||
ConfigurationCommentMover.moveComments(node, newRoot);
|
ConfigurationCommentMover.moveComments(node, newRoot);
|
||||||
|
} else if (currentVersion <= 4) {
|
||||||
|
advancedConfig = migrateToAdvancedConfig(advancedConfigPath, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.save(newRoot);
|
loader.save(newRoot);
|
||||||
}
|
}
|
||||||
|
if (advancedConfig == null) {
|
||||||
|
advancedConfig = loadAdvancedConfig(advancedConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (transformer != null) {
|
if (transformer != null) {
|
||||||
// We transform AFTER saving so that these specific transformations aren't applied to file.
|
// We transform AFTER saving so that these specific transformations aren't applied to file.
|
||||||
|
@ -159,9 +186,69 @@ public final class ConfigLoader {
|
||||||
config = newRoot.get(configClass);
|
config = newRoot.get(configClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.advanced(advancedConfig);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AdvancedConfig migrateToAdvancedConfig(File file, ConfigurationNode configRoot) throws IOException {
|
||||||
|
List<NodePath> copyFromOldConfig = Stream.of("max-visible-custom-skulls", "custom-skull-render-distance", "scoreboard-packet-threshold", "mtu",
|
||||||
|
"floodgate-key-file", "use-direct-connection", "disable-compression")
|
||||||
|
.map(NodePath::path).toList();
|
||||||
|
|
||||||
|
var loader = createLoader(file, ADVANCED_HEADER);
|
||||||
|
|
||||||
|
CommentedConfigurationNode advancedNode = CommentedConfigurationNode.root(loader.defaultOptions());
|
||||||
|
copyFromOldConfig.forEach(path -> {
|
||||||
|
ConfigurationNode node = configRoot.node(path);
|
||||||
|
if (!node.virtual()) {
|
||||||
|
advancedNode.node(path).mergeFrom(node);
|
||||||
|
configRoot.removeChild(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ConfigurationNode metricsUuid = configRoot.node("metrics", "uuid");
|
||||||
|
if (!metricsUuid.virtual()) {
|
||||||
|
advancedNode.node("metrics-uuid").set(metricsUuid.get(UUID.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
advancedNode.node("version").set(Constants.ADVANCED_CONFIG_VERSION);
|
||||||
|
|
||||||
|
AdvancedConfig advancedConfig = advancedNode.get(AdvancedConfig.class);
|
||||||
|
// Ensure all fields get populated
|
||||||
|
CommentedConfigurationNode newNode = CommentedConfigurationNode.root(loader.defaultOptions());
|
||||||
|
newNode.set(advancedConfig);
|
||||||
|
loader.save(newNode);
|
||||||
|
return advancedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdvancedConfig loadAdvancedConfig(File file) throws IOException {
|
||||||
|
var loader = createLoader(file, ADVANCED_HEADER);
|
||||||
|
if (file.exists()) {
|
||||||
|
ConfigurationNode node = loader.load();
|
||||||
|
return node.get(AdvancedConfig.class);
|
||||||
|
} else {
|
||||||
|
ConfigurationNode node = CommentedConfigurationNode.root(loader.defaultOptions());
|
||||||
|
node.node("version").set(Constants.ADVANCED_CONFIG_VERSION);
|
||||||
|
AdvancedConfig advancedConfig = node.get(AdvancedConfig.class);
|
||||||
|
node.set(advancedConfig);
|
||||||
|
loader.save(node);
|
||||||
|
return advancedConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static YamlConfigurationLoader createLoader(File file, String header) {
|
||||||
|
return YamlConfigurationLoader.builder()
|
||||||
|
.file(file)
|
||||||
|
.indent(2)
|
||||||
|
.nodeStyle(NodeStyle.BLOCK)
|
||||||
|
.defaultOptions(options -> InterfaceDefaultOptions.addTo(options)
|
||||||
|
.shouldCopyDefaults(false) // If we use ConfigurationNode#get(type, default), do not write the default back to the node.
|
||||||
|
.header(header)
|
||||||
|
.serializers(builder -> builder.register(new LowercaseEnumSerializer())))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private ConfigLoader() {
|
private ConfigLoader() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.geysermc.geyser.api.network.RemoteServer;
|
||||||
import org.geysermc.geyser.network.GameProtocol;
|
import org.geysermc.geyser.network.GameProtocol;
|
||||||
import org.geysermc.geyser.util.CooldownUtils;
|
import org.geysermc.geyser.util.CooldownUtils;
|
||||||
import org.spongepowered.configurate.interfaces.meta.Exclude;
|
import org.spongepowered.configurate.interfaces.meta.Exclude;
|
||||||
|
import org.spongepowered.configurate.interfaces.meta.Field;
|
||||||
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
|
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
|
||||||
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
|
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
|
||||||
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
|
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
|
||||||
|
@ -43,7 +44,6 @@ import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@ConfigSerializable
|
@ConfigSerializable
|
||||||
public interface GeyserConfig {
|
public interface GeyserConfig {
|
||||||
|
@ -51,10 +51,6 @@ public interface GeyserConfig {
|
||||||
|
|
||||||
JavaConfig java();
|
JavaConfig java();
|
||||||
|
|
||||||
// Cannot be type File yet because we want to hide it in plugin instances.
|
|
||||||
@DefaultString("key.pem")
|
|
||||||
String floodgateKeyFile();
|
|
||||||
|
|
||||||
@Comment("""
|
@Comment("""
|
||||||
For online mode authentication type only.
|
For online mode authentication type only.
|
||||||
Stores a list of Bedrock players that should have their Java Edition account saved after login.
|
Stores a list of Bedrock players that should have their Java Edition account saved after login.
|
||||||
|
@ -128,25 +124,10 @@ public interface GeyserConfig {
|
||||||
@DefaultString("system")
|
@DefaultString("system")
|
||||||
String defaultLocale();
|
String defaultLocale();
|
||||||
|
|
||||||
@Comment("""
|
|
||||||
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)""")
|
|
||||||
int cacheImages();
|
|
||||||
|
|
||||||
@Comment("Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.")
|
@Comment("Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.")
|
||||||
@DefaultBoolean(true)
|
@DefaultBoolean(true)
|
||||||
boolean allowCustomSkulls();
|
boolean allowCustomSkulls();
|
||||||
|
|
||||||
@Comment("""
|
|
||||||
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.""")
|
|
||||||
@DefaultNumeric(128)
|
|
||||||
int maxVisibleCustomSkulls();
|
|
||||||
|
|
||||||
@Comment("The radius in blocks around the player in which custom skulls are displayed.")
|
|
||||||
@DefaultNumeric(32)
|
|
||||||
int customSkullRenderDistance();
|
|
||||||
|
|
||||||
@Comment("""
|
@Comment("""
|
||||||
Whether to add any items and blocks which normally does not exist in Bedrock Edition.
|
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.
|
This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching.
|
||||||
|
@ -186,18 +167,23 @@ public interface GeyserConfig {
|
||||||
@DefaultBoolean(true)
|
@DefaultBoolean(true)
|
||||||
boolean notifyOnNewBedrockUpdate();
|
boolean notifyOnNewBedrockUpdate();
|
||||||
|
|
||||||
@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.""")
|
|
||||||
@DefaultString("minecraft:barrier")
|
|
||||||
String unusableSpaceBlock();
|
|
||||||
|
|
||||||
@Comment("""
|
@Comment("""
|
||||||
bStats is a stat tracker that is entirely anonymous and tracks only basic information
|
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,
|
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/.
|
what OS is being used, etc. You can learn more about bStats here: https://bstats.org/.
|
||||||
https://bstats.org/plugin/server-implementation/GeyserMC""")
|
https://bstats.org/plugin/server-implementation/GeyserMC""")
|
||||||
MetricsInfo metrics();
|
@DefaultBoolean(true)
|
||||||
|
boolean enableMetrics();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A separate config file added to this class manually.
|
||||||
|
*/
|
||||||
|
@Field
|
||||||
|
@NonNull
|
||||||
|
AdvancedConfig advanced();
|
||||||
|
|
||||||
|
@Field
|
||||||
|
void advanced(AdvancedConfig config);
|
||||||
|
|
||||||
@ConfigSerializable
|
@ConfigSerializable
|
||||||
interface BedrockConfig extends BedrockListener {
|
interface BedrockConfig extends BedrockListener {
|
||||||
|
@ -315,39 +301,14 @@ public interface GeyserConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigSerializable
|
|
||||||
interface MetricsInfo {
|
|
||||||
@Comment("If metrics should be enabled")
|
|
||||||
@DefaultBoolean(true)
|
|
||||||
boolean enabled();
|
|
||||||
|
|
||||||
@Comment("UUID of server. Don't change!")
|
|
||||||
default UUID uuid() { //TODO rename?
|
|
||||||
return UUID.randomUUID();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Comment("""
|
|
||||||
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.""")
|
|
||||||
@DefaultNumeric(20)
|
|
||||||
int scoreboardPacketThreshold();
|
|
||||||
|
|
||||||
@Comment("""
|
@Comment("""
|
||||||
Allow connections from ProxyPass and Waterdog.
|
Allow connections from ProxyPass and Waterdog.
|
||||||
See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP.""")
|
See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP.""")
|
||||||
// if u have offline mode enabled pls be safe
|
// if u have offline mode enabled pls be safe
|
||||||
boolean enableProxyConnections();
|
boolean enableProxyConnections();
|
||||||
|
|
||||||
@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("Do not change!")
|
@Comment("Do not change!")
|
||||||
|
@SuppressWarnings("unused")
|
||||||
default int configVersion() {
|
default int configVersion() {
|
||||||
return Constants.CONFIG_VERSION;
|
return Constants.CONFIG_VERSION;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ package org.geysermc.geyser.configuration;
|
||||||
|
|
||||||
import org.spongepowered.configurate.interfaces.meta.Exclude;
|
import org.spongepowered.configurate.interfaces.meta.Exclude;
|
||||||
import org.spongepowered.configurate.interfaces.meta.Field;
|
import org.spongepowered.configurate.interfaces.meta.Field;
|
||||||
import org.spongepowered.configurate.interfaces.meta.Hidden;
|
|
||||||
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
|
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
|
||||||
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||||
|
@ -72,10 +71,6 @@ public interface GeyserPluginConfig extends GeyserConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Hidden
|
|
||||||
String floodgateKeyFile();
|
|
||||||
|
|
||||||
@Comment("""
|
@Comment("""
|
||||||
Use server API methods to determine the Java server's MOTD and ping passthrough.
|
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.""")
|
There is no need to disable this unless your MOTD or player count does not appear properly.""")
|
||||||
|
@ -87,12 +82,4 @@ public interface GeyserPluginConfig extends GeyserConfig {
|
||||||
Only relevant if integrated-ping-passthrough is disabled.""")
|
Only relevant if integrated-ping-passthrough is disabled.""")
|
||||||
@Override
|
@Override
|
||||||
int pingPassthroughInterval();
|
int pingPassthroughInterval();
|
||||||
|
|
||||||
@Hidden
|
|
||||||
@DefaultBoolean(true)
|
|
||||||
boolean useDirectConnection();
|
|
||||||
|
|
||||||
@Hidden
|
|
||||||
@DefaultBoolean(true)
|
|
||||||
boolean disableCompression();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,14 +39,6 @@ public interface GeyserRemoteConfig extends GeyserConfig {
|
||||||
@Override
|
@Override
|
||||||
RemoteConfig java();
|
RemoteConfig java();
|
||||||
|
|
||||||
@Override
|
|
||||||
@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.""")
|
|
||||||
String floodgateKeyFile();
|
|
||||||
|
|
||||||
@ConfigSerializable
|
@ConfigSerializable
|
||||||
interface RemoteConfig extends JavaConfig {
|
interface RemoteConfig extends JavaConfig {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.geysermc.floodgate.util.DeviceOs;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.api.GeyserApi;
|
import org.geysermc.geyser.api.GeyserApi;
|
||||||
import org.geysermc.geyser.api.extension.Extension;
|
import org.geysermc.geyser.api.extension.Extension;
|
||||||
|
import org.geysermc.geyser.configuration.AdvancedConfig;
|
||||||
import org.geysermc.geyser.configuration.GeyserConfig;
|
import org.geysermc.geyser.configuration.GeyserConfig;
|
||||||
import org.geysermc.geyser.network.GameProtocol;
|
import org.geysermc.geyser.network.GameProtocol;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
@ -73,6 +74,7 @@ public class DumpInfo {
|
||||||
private final String systemEncoding;
|
private final String systemEncoding;
|
||||||
private final GitInfo gitInfo;
|
private final GitInfo gitInfo;
|
||||||
private final GeyserConfig config;
|
private final GeyserConfig config;
|
||||||
|
private final AdvancedConfig advancedConfig;
|
||||||
private final Object2IntMap<DeviceOs> userPlatforms;
|
private final Object2IntMap<DeviceOs> userPlatforms;
|
||||||
private final int connectionAttempts;
|
private final int connectionAttempts;
|
||||||
private final HashInfo hashInfo;
|
private final HashInfo hashInfo;
|
||||||
|
@ -93,6 +95,7 @@ public class DumpInfo {
|
||||||
this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY);
|
this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY);
|
||||||
|
|
||||||
this.config = geyser.config();
|
this.config = geyser.config();
|
||||||
|
this.advancedConfig = geyser.config().advanced();
|
||||||
|
|
||||||
String md5Hash = "unknown";
|
String md5Hash = "unknown";
|
||||||
String sha256Hash = "unknown";
|
String sha256Hash = "unknown";
|
||||||
|
|
|
@ -50,7 +50,7 @@ public abstract class GeyserInjector {
|
||||||
* @param bootstrap the bootstrap of the Geyser instance.
|
* @param bootstrap the bootstrap of the Geyser instance.
|
||||||
*/
|
*/
|
||||||
public void initializeLocalChannel(GeyserPluginBootstrap bootstrap) {
|
public void initializeLocalChannel(GeyserPluginBootstrap bootstrap) {
|
||||||
if (!bootstrap.config().useDirectConnection()) {
|
if (!bootstrap.config().advanced().useDirectConnection()) {
|
||||||
bootstrap.getGeyserLogger().debug("Disabling direct injection!");
|
bootstrap.getGeyserLogger().debug("Disabling direct injection!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,7 +224,7 @@ public final class GeyserServer {
|
||||||
|
|
||||||
GeyserServerInitializer serverInitializer = new GeyserServerInitializer(this.geyser);
|
GeyserServerInitializer serverInitializer = new GeyserServerInitializer(this.geyser);
|
||||||
playerGroup = serverInitializer.getEventLoopGroup();
|
playerGroup = serverInitializer.getEventLoopGroup();
|
||||||
this.geyser.getLogger().debug("Setting MTU to " + this.geyser.config().mtu());
|
this.geyser.getLogger().debug("Setting MTU to " + this.geyser.config().advanced().mtu());
|
||||||
|
|
||||||
int rakPacketLimit = positivePropOrDefault("Geyser.RakPacketLimit", DEFAULT_PACKET_LIMIT);
|
int rakPacketLimit = positivePropOrDefault("Geyser.RakPacketLimit", DEFAULT_PACKET_LIMIT);
|
||||||
this.geyser.getLogger().debug("Setting RakNet packet limit to " + rakPacketLimit);
|
this.geyser.getLogger().debug("Setting RakNet packet limit to " + rakPacketLimit);
|
||||||
|
@ -239,7 +239,7 @@ public final class GeyserServer {
|
||||||
.channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannel()))
|
.channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannel()))
|
||||||
.group(group, childGroup)
|
.group(group, childGroup)
|
||||||
.option(RakChannelOption.RAK_HANDLE_PING, true)
|
.option(RakChannelOption.RAK_HANDLE_PING, true)
|
||||||
.option(RakChannelOption.RAK_MAX_MTU, this.geyser.config().mtu())
|
.option(RakChannelOption.RAK_MAX_MTU, this.geyser.config().advanced().mtu())
|
||||||
.option(RakChannelOption.RAK_PACKET_LIMIT, rakPacketLimit)
|
.option(RakChannelOption.RAK_PACKET_LIMIT, rakPacketLimit)
|
||||||
.option(RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, rakGlobalPacketLimit)
|
.option(RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, rakGlobalPacketLimit)
|
||||||
.option(RakChannelOption.RAK_SEND_COOKIE, rakSendCookie)
|
.option(RakChannelOption.RAK_SEND_COOKIE, rakSendCookie)
|
||||||
|
|
|
@ -47,7 +47,7 @@ public final class ScoreboardUpdater extends Thread {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
GeyserConfig config = GeyserImpl.getInstance().config();
|
GeyserConfig config = GeyserImpl.getInstance().config();
|
||||||
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.scoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
|
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.advanced().scoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
|
||||||
DEBUG_ENABLED = config.debugMode();
|
DEBUG_ENABLED = config.debugMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,11 +74,11 @@ public class SkullCache {
|
||||||
|
|
||||||
public SkullCache(GeyserSession session) {
|
public SkullCache(GeyserSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.maxVisibleSkulls = session.getGeyser().config().maxVisibleCustomSkulls();
|
this.maxVisibleSkulls = session.getGeyser().config().advanced().maxVisibleCustomSkulls();
|
||||||
this.cullingEnabled = this.maxVisibleSkulls != -1;
|
this.cullingEnabled = this.maxVisibleSkulls != -1;
|
||||||
|
|
||||||
// Normal skulls are not rendered beyond 64 blocks
|
// Normal skulls are not rendered beyond 64 blocks
|
||||||
int distance = Math.min(session.getGeyser().config().customSkullRenderDistance(), 64);
|
int distance = Math.min(session.getGeyser().config().advanced().customSkullRenderDistance(), 64);
|
||||||
this.skullRenderDistanceSquared = distance * distance;
|
this.skullRenderDistanceSquared = distance * distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ public class SkinProvider {
|
||||||
|
|
||||||
public static void registerCacheImageTask(GeyserImpl geyser) {
|
public static void registerCacheImageTask(GeyserImpl geyser) {
|
||||||
// Schedule Daily Image Expiry if we are caching them
|
// Schedule Daily Image Expiry if we are caching them
|
||||||
if (geyser.config().cacheImages() > 0) {
|
if (geyser.config().advanced().cacheImages() > 0) {
|
||||||
geyser.getScheduledThread().scheduleAtFixedRate(() -> {
|
geyser.getScheduledThread().scheduleAtFixedRate(() -> {
|
||||||
File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile();
|
File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile();
|
||||||
if (!cacheFolder.exists()) {
|
if (!cacheFolder.exists()) {
|
||||||
|
@ -152,7 +152,7 @@ public class SkinProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
final long expireTime = ((long) GeyserImpl.getInstance().config().cacheImages()) * ((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())) {
|
for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) {
|
||||||
if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) {
|
if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
@ -422,7 +422,7 @@ public class SkinProvider {
|
||||||
GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl);
|
GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl);
|
||||||
|
|
||||||
// Write to cache if we are allowed
|
// Write to cache if we are allowed
|
||||||
if (GeyserImpl.getInstance().config().cacheImages() > 0) {
|
if (GeyserImpl.getInstance().config().advanced().cacheImages() > 0) {
|
||||||
imageFile.getParentFile().mkdirs();
|
imageFile.getParentFile().mkdirs();
|
||||||
try {
|
try {
|
||||||
ImageIO.write(image, "png", imageFile);
|
ImageIO.write(image, "png", imageFile);
|
||||||
|
|
|
@ -236,7 +236,7 @@ public class InventoryUtils {
|
||||||
|
|
||||||
private static ItemDefinition getUnusableSpaceBlockDefinition(int protocolVersion) {
|
private static ItemDefinition getUnusableSpaceBlockDefinition(int protocolVersion) {
|
||||||
ItemMappings mappings = Registries.ITEMS.forVersion(protocolVersion);
|
ItemMappings mappings = Registries.ITEMS.forVersion(protocolVersion);
|
||||||
String unusableSpaceBlock = GeyserImpl.getInstance().config().unusableSpaceBlock();
|
String unusableSpaceBlock = GeyserImpl.getInstance().config().advanced().unusableSpaceBlock();
|
||||||
ItemDefinition itemDefinition = mappings.getDefinition(unusableSpaceBlock);
|
ItemDefinition itemDefinition = mappings.getDefinition(unusableSpaceBlock);
|
||||||
|
|
||||||
if (itemDefinition == null) {
|
if (itemDefinition == null) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue