package com.hypixel.hytale.server.core; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.DocumentContainingCodec; import com.hypixel.hytale.codec.ExtraInfo; import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.codec.codecs.map.MapCodec; import com.hypixel.hytale.codec.codecs.map.ObjectMapCodec; import com.hypixel.hytale.codec.lookup.Priority; import com.hypixel.hytale.codec.util.RawJsonReader; import com.hypixel.hytale.common.plugin.PluginIdentifier; import com.hypixel.hytale.common.semver.SemverRange; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.protocol.GameMode; import com.hypixel.hytale.server.core.auth.AuthCredentialStoreProvider; import com.hypixel.hytale.server.core.codec.ProtocolCodecs; import com.hypixel.hytale.server.core.universe.playerdata.DefaultPlayerStorageProvider; import com.hypixel.hytale.server.core.universe.playerdata.DiskPlayerStorageProvider; import com.hypixel.hytale.server.core.universe.playerdata.PlayerStorageProvider; import com.hypixel.hytale.server.core.util.BsonUtil; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bson.BsonDocument; public class HytaleServerConfig { public static final int VERSION = 3; public static final int DEFAULT_MAX_VIEW_RADIUS = 32; @Nonnull public static final Path PATH = Path.of("config.json"); @Nonnull public static final BuilderCodec CODEC = BuilderCodec.builder(HytaleServerConfig.class, HytaleServerConfig::new) .versioned() .codecVersion(3) .append(new KeyedCodec<>("ServerName", Codec.STRING), (o, s) -> o.serverName = s, o -> o.serverName) .add() .append(new KeyedCodec<>("MOTD", Codec.STRING), (o, s) -> o.motd = s, o -> o.motd) .add() .append(new KeyedCodec<>("Password", Codec.STRING), (o, s) -> o.password = s, o -> o.password) .add() .append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER), (o, i) -> o.maxPlayers = i, o -> o.maxPlayers) .add() .append(new KeyedCodec<>("MaxViewRadius", Codec.INTEGER), (o, i) -> o.maxViewRadius = i, o -> o.maxViewRadius) .add() .append(new KeyedCodec<>("LocalCompressionEnabled", Codec.BOOLEAN), (o, i) -> o.localCompressionEnabled = i, o -> o.localCompressionEnabled) .add() .append(new KeyedCodec<>("Defaults", HytaleServerConfig.Defaults.CODEC), (o, obj) -> o.defaults = obj, o -> o.defaults) .add() .append( new KeyedCodec<>("ConnectionTimeouts", HytaleServerConfig.ConnectionTimeouts.CODEC), (o, m) -> o.connectionTimeouts = m, o -> o.connectionTimeouts ) .add() .append(new KeyedCodec<>("RateLimit", HytaleServerConfig.RateLimitConfig.CODEC), (o, m) -> o.rateLimitConfig = m, o -> o.rateLimitConfig) .add() .append(new KeyedCodec<>("Modules", new MapCodec<>(HytaleServerConfig.Module.CODEC, ConcurrentHashMap::new, false)), (o, m) -> { o.modules = m; o.unmodifiableModules = Collections.unmodifiableMap(m); }, o -> o.modules) .add() .append(new KeyedCodec<>("LogLevels", new MapCodec<>(Codec.LOG_LEVEL, ConcurrentHashMap::new, false)), (o, m) -> { o.logLevels = m; o.unmodifiableLogLevels = Collections.unmodifiableMap(o.logLevels); }, o -> o.logLevels) .add() .append( new KeyedCodec<>( "Plugins", new ObjectMapCodec<>( HytaleServerConfig.ModConfig.CODEC, Object2ObjectOpenHashMap::new, PluginIdentifier::toString, PluginIdentifier::fromString, false ) ), (o, i) -> o.legacyPluginConfig = i, o -> null ) .setVersionRange(0, 2) .add() .append( new KeyedCodec<>( "Mods", new ObjectMapCodec<>(HytaleServerConfig.ModConfig.CODEC, ConcurrentHashMap::new, PluginIdentifier::toString, PluginIdentifier::fromString, false) ), (o, i) -> o.modConfig = i, o -> o.modConfig ) .add() .append( new KeyedCodec<>("DisplayTmpTagsInStrings", Codec.BOOLEAN), (o, displayTmpTagsInStrings) -> o.displayTmpTagsInStrings = displayTmpTagsInStrings, o -> o.displayTmpTagsInStrings ) .add() .append(new KeyedCodec<>("PlayerStorage", PlayerStorageProvider.CODEC), (o, obj) -> o.playerStorageProvider = obj, o -> o.playerStorageProvider) .add() .append(new KeyedCodec<>("AuthCredentialStore", Codec.BSON_DOCUMENT), (o, value) -> o.authCredentialStoreConfig = value, o -> o.authCredentialStoreConfig) .add() .afterDecode(config -> { config.defaults.hytaleServerConfig = config; config.connectionTimeouts.hytaleServerConfig = config; config.rateLimitConfig.hytaleServerConfig = config; config.modules.values().forEach(m -> m.setHytaleServerConfig(config)); if (config.legacyPluginConfig != null && !config.legacyPluginConfig.isEmpty()) { for (Entry entry : config.legacyPluginConfig.entrySet()) { config.modConfig.putIfAbsent(entry.getKey(), entry.getValue()); } config.legacyPluginConfig = null; config.markChanged(); } }) .build(); @Nonnull private final transient AtomicBoolean hasChanged = new AtomicBoolean(); private String serverName = "Hytale Server"; private String motd = ""; private String password = ""; private int maxPlayers = 100; private int maxViewRadius = 32; private boolean localCompressionEnabled; @Nonnull private HytaleServerConfig.Defaults defaults = new HytaleServerConfig.Defaults(this); @Nonnull private HytaleServerConfig.ConnectionTimeouts connectionTimeouts = new HytaleServerConfig.ConnectionTimeouts(this); @Nonnull private HytaleServerConfig.RateLimitConfig rateLimitConfig = new HytaleServerConfig.RateLimitConfig(this); @Nonnull private Map modules = new ConcurrentHashMap<>(); @Nonnull private Map logLevels = Collections.emptyMap(); @Nullable private transient Map legacyPluginConfig; @Nonnull private Map modConfig = new ConcurrentHashMap<>(); @Nonnull private Map unmodifiableModules = Collections.unmodifiableMap(this.modules); @Nonnull private Map unmodifiableLogLevels = Collections.unmodifiableMap(this.logLevels); @Nonnull private PlayerStorageProvider playerStorageProvider = PlayerStorageProvider.CODEC.getDefault(); @Nullable private BsonDocument authCredentialStoreConfig = null; @Nullable private transient AuthCredentialStoreProvider authCredentialStoreProvider = null; private boolean displayTmpTagsInStrings; public String getServerName() { return this.serverName; } public void setServerName(@Nonnull String serverName) { this.serverName = serverName; this.markChanged(); } public String getMotd() { return this.motd; } public void setMotd(@Nonnull String motd) { this.motd = motd; this.markChanged(); } public String getPassword() { return this.password; } public void setPassword(@Nonnull String password) { this.password = password; this.markChanged(); } public boolean isDisplayTmpTagsInStrings() { return this.displayTmpTagsInStrings; } public void setDisplayTmpTagsInStrings(boolean displayTmpTagsInStrings) { this.displayTmpTagsInStrings = displayTmpTagsInStrings; } public int getMaxPlayers() { return this.maxPlayers; } public void setMaxPlayers(int maxPlayers) { this.maxPlayers = maxPlayers; this.markChanged(); } public int getMaxViewRadius() { return this.maxViewRadius; } public void setMaxViewRadius(int maxViewRadius) { this.maxViewRadius = maxViewRadius; this.markChanged(); } public boolean isLocalCompressionEnabled() { return this.localCompressionEnabled; } public void setLocalCompressionEnabled(boolean localCompression) { this.localCompressionEnabled = localCompression; this.markChanged(); } @Nonnull public HytaleServerConfig.Defaults getDefaults() { return this.defaults; } public void setDefaults(@Nonnull HytaleServerConfig.Defaults defaults) { this.defaults = defaults; this.markChanged(); } @Nonnull public HytaleServerConfig.ConnectionTimeouts getConnectionTimeouts() { return this.connectionTimeouts; } public void setConnectionTimeouts(@Nonnull HytaleServerConfig.ConnectionTimeouts connectionTimeouts) { this.connectionTimeouts = connectionTimeouts; this.markChanged(); } @Nonnull public HytaleServerConfig.RateLimitConfig getRateLimitConfig() { return this.rateLimitConfig; } public void setRateLimitConfig(@Nonnull HytaleServerConfig.RateLimitConfig rateLimitConfig) { this.rateLimitConfig = rateLimitConfig; this.markChanged(); } @Nonnull public Map getModules() { return this.unmodifiableModules; } @Nonnull public HytaleServerConfig.Module getModule(String moduleName) { return this.modules.computeIfAbsent(moduleName, k -> new HytaleServerConfig.Module(this)); } public void setModules(@Nonnull Map modules) { this.modules = modules; this.markChanged(); } @Nonnull public Map getLogLevels() { return this.unmodifiableLogLevels; } public void setLogLevels(@Nonnull Map logLevels) { this.logLevels = logLevels; this.markChanged(); } @Nonnull public Map getModConfig() { return this.modConfig; } public void setModConfig(@Nonnull Map modConfig) { this.modConfig = modConfig; this.markChanged(); } @Nonnull public PlayerStorageProvider getPlayerStorageProvider() { return this.playerStorageProvider; } public void setPlayerStorageProvider(@Nonnull PlayerStorageProvider playerStorageProvider) { this.playerStorageProvider = playerStorageProvider; this.markChanged(); } @Nonnull public AuthCredentialStoreProvider getAuthCredentialStoreProvider() { if (this.authCredentialStoreProvider != null) { return this.authCredentialStoreProvider; } else { if (this.authCredentialStoreConfig != null) { this.authCredentialStoreProvider = AuthCredentialStoreProvider.CODEC.decode(this.authCredentialStoreConfig); } else { this.authCredentialStoreProvider = AuthCredentialStoreProvider.CODEC.getDefault(); } return this.authCredentialStoreProvider; } } public void setAuthCredentialStoreProvider(@Nonnull AuthCredentialStoreProvider provider) { this.authCredentialStoreProvider = provider; this.authCredentialStoreConfig = (BsonDocument)AuthCredentialStoreProvider.CODEC.encode(provider); this.markChanged(); } public void removeModule(@Nonnull String module) { this.modules.remove(module); this.markChanged(); } public void markChanged() { this.hasChanged.set(true); } public boolean consumeHasChanged() { return this.hasChanged.getAndSet(false); } @Nonnull public static HytaleServerConfig load() { return load(PATH); } @Nonnull public static HytaleServerConfig load(@Nonnull Path path) { if (!Files.isRegularFile(path)) { HytaleServerConfig hytaleServerConfig = new HytaleServerConfig(); if (!Options.getOptionSet().has(Options.BARE)) { save(hytaleServerConfig).join(); } return hytaleServerConfig; } else { try { HytaleServerConfig config = RawJsonReader.readSyncWithBak(path, CODEC, HytaleLogger.getLogger()); if (config == null) { throw new RuntimeException("Failed to load server config from " + path); } else { return config; } } catch (Exception var2) { throw new RuntimeException("Failed to read server config!", var2); } } } @Nonnull public static CompletableFuture save(@Nonnull HytaleServerConfig hytaleServerConfig) { return save(PATH, hytaleServerConfig); } @Nonnull public static CompletableFuture save(@Nonnull Path path, @Nonnull HytaleServerConfig hytaleServerConfig) { BsonDocument document = CODEC.encode(hytaleServerConfig, ExtraInfo.THREAD_LOCAL.get()).asDocument(); return BsonUtil.writeDocument(path, document); } static { PlayerStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", DefaultPlayerStorageProvider.class, DefaultPlayerStorageProvider.CODEC); PlayerStorageProvider.CODEC.register("Disk", DiskPlayerStorageProvider.class, DiskPlayerStorageProvider.CODEC); HytaleServerConfig.Module.BUILDER_CODEC_BUILDER .addField( new KeyedCodec<>("Modules", new MapCodec<>(HytaleServerConfig.Module.CODEC, ConcurrentHashMap::new, false)), (o, m) -> o.modules = m, o -> o.modules ); } public static class ConnectionTimeouts { public static final Duration DEFAULT_INITIAL_TIMEOUT = Duration.of(10L, ChronoUnit.SECONDS); public static final Duration DEFAULT_AUTH_TIMEOUT = Duration.of(30L, ChronoUnit.SECONDS); public static final Duration DEFAULT_PLAY_TIMEOUT = Duration.of(1L, ChronoUnit.MINUTES); public static final Codec CODEC = BuilderCodec.builder( HytaleServerConfig.ConnectionTimeouts.class, HytaleServerConfig.ConnectionTimeouts::new ) .addField(new KeyedCodec<>("InitialTimeout", Codec.DURATION), (o, d) -> o.initialTimeout = d, o -> o.initialTimeout) .addField(new KeyedCodec<>("AuthTimeout", Codec.DURATION), (o, d) -> o.authTimeout = d, o -> o.authTimeout) .addField(new KeyedCodec<>("PlayTimeout", Codec.DURATION), (o, d) -> o.playTimeout = d, o -> o.playTimeout) .addField( new KeyedCodec<>("JoinTimeouts", new MapCodec<>(Codec.DURATION, ConcurrentHashMap::new, false)), (o, m) -> o.joinTimeouts = m, o -> o.joinTimeouts ) .build(); private Duration initialTimeout; private Duration authTimeout; private Duration playTimeout; private Map joinTimeouts = new ConcurrentHashMap<>(); @Nonnull private Map unmodifiableJoinTimeouts = Collections.unmodifiableMap(this.joinTimeouts); private transient HytaleServerConfig hytaleServerConfig; public ConnectionTimeouts() { } public ConnectionTimeouts(HytaleServerConfig hytaleServerConfig) { this.hytaleServerConfig = hytaleServerConfig; } public Duration getInitialTimeout() { return this.initialTimeout != null ? this.initialTimeout : DEFAULT_INITIAL_TIMEOUT; } public void setInitialTimeout(Duration initialTimeout) { this.initialTimeout = initialTimeout; this.hytaleServerConfig.markChanged(); } public Duration getAuthTimeout() { return this.authTimeout != null ? this.authTimeout : DEFAULT_AUTH_TIMEOUT; } public void setAuthTimeout(Duration authTimeout) { this.authTimeout = authTimeout; this.hytaleServerConfig.markChanged(); } public Duration getPlayTimeout() { return this.playTimeout != null ? this.playTimeout : DEFAULT_PLAY_TIMEOUT; } public void setPlayTimeout(Duration playTimeout) { this.playTimeout = playTimeout; this.hytaleServerConfig.markChanged(); } @Nonnull public Map getJoinTimeouts() { return this.unmodifiableJoinTimeouts; } public void setJoinTimeouts(Map joinTimeouts) { this.joinTimeouts = joinTimeouts; this.hytaleServerConfig.markChanged(); } } public static class Defaults { public static final KeyedCodec WORLD = new KeyedCodec<>("World", Codec.STRING); public static final KeyedCodec GAMEMODE = new KeyedCodec<>("GameMode", ProtocolCodecs.GAMEMODE_LEGACY); public static final BuilderCodec CODEC = BuilderCodec.builder( HytaleServerConfig.Defaults.class, HytaleServerConfig.Defaults::new ) .addField(WORLD, (o, i) -> o.world = i, o -> o.world) .addField(GAMEMODE, (o, s) -> o.gameMode = s, o -> o.gameMode) .build(); private transient HytaleServerConfig hytaleServerConfig; private String world = "default"; private GameMode gameMode = GameMode.Adventure; private Defaults() { } private Defaults(HytaleServerConfig hytaleServerConfig) { this.hytaleServerConfig = hytaleServerConfig; } public String getWorld() { return this.world; } public void setWorld(String world) { this.world = world; this.hytaleServerConfig.markChanged(); } public GameMode getGameMode() { return this.gameMode; } public void setGameMode(GameMode gameMode) { this.gameMode = gameMode; this.hytaleServerConfig.markChanged(); } } public static class ModConfig { public static final BuilderCodec CODEC = BuilderCodec.builder( HytaleServerConfig.ModConfig.class, HytaleServerConfig.ModConfig::new ) .append(new KeyedCodec<>("Enabled", Codec.BOOLEAN), (modConfig, enabled) -> modConfig.enabled = enabled, modConfig -> modConfig.enabled) .add() .append( new KeyedCodec<>("RequiredVersion", SemverRange.CODEC), (modConfig, semverRange) -> modConfig.requiredVersion = semverRange, modConfig -> modConfig.requiredVersion ) .add() .build(); @Nullable private Boolean enabled; @Nullable private SemverRange requiredVersion; @Nullable public Boolean getEnabled() { return this.enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } @Nullable public SemverRange getRequiredVersion() { return this.requiredVersion; } public void setRequiredVersion(SemverRange requiredVersion) { this.requiredVersion = requiredVersion; } public static void setBoot(HytaleServerConfig serverConfig, PluginIdentifier identifier, boolean enabled) { serverConfig.getModConfig().computeIfAbsent(identifier, id -> new HytaleServerConfig.ModConfig()).enabled = enabled; } } public static class Module { @Nonnull protected static BuilderCodec.Builder BUILDER_CODEC_BUILDER = BuilderCodec.builder( HytaleServerConfig.Module.class, HytaleServerConfig.Module::new ) .addField(new KeyedCodec<>("Enabled", Codec.BOOLEAN), (o, i) -> o.enabled = i, o -> o.enabled); @Nonnull protected static BuilderCodec BUILDER_CODEC = BUILDER_CODEC_BUILDER.build(); @Nonnull public static final DocumentContainingCodec CODEC = new DocumentContainingCodec<>( BUILDER_CODEC, (o, i) -> o.document = i, o -> o.document ); private transient HytaleServerConfig hytaleServerConfig; private Boolean enabled; @Nonnull private Map modules = new ConcurrentHashMap<>(); @Nonnull private BsonDocument document = new BsonDocument(); private Module() { } private Module(@Nonnull HytaleServerConfig hytaleServerConfig) { this.hytaleServerConfig = hytaleServerConfig; } public boolean isEnabled(boolean def) { return this.enabled != null ? this.enabled : def; } public void setEnabled(boolean enabled) { this.enabled = enabled; this.hytaleServerConfig.markChanged(); } public Boolean getEnabled() { return this.enabled; } @Nonnull public Map getModules() { return Collections.unmodifiableMap(this.modules); } @Nonnull public HytaleServerConfig.Module getModule(@Nonnull String moduleName) { return this.modules.computeIfAbsent(moduleName, k -> new HytaleServerConfig.Module(this.hytaleServerConfig)); } public void setModules(@Nonnull Map modules) { this.modules = modules; this.hytaleServerConfig.markChanged(); } @Nonnull public BsonDocument getDocument() { return this.document; } @Nullable public T decode(@Nonnull Codec codec) { return codec.decode(this.document); } public void encode(@Nonnull Codec codec, @Nonnull T t) { this.document = codec.encode(t).asDocument(); } @Nonnull public Optional getData(@Nonnull KeyedCodec keyedCodec) { return keyedCodec.get(this.document); } @Nullable public T getDataOrNull(@Nonnull KeyedCodec keyedCodec) { return keyedCodec.getOrNull(this.document); } public T getDataNow(@Nonnull KeyedCodec keyedCodec) { return keyedCodec.getNow(this.document); } public void put(@Nonnull KeyedCodec keyedCodec, T t) { keyedCodec.put(this.document, t); this.hytaleServerConfig.markChanged(); } public void setDocument(@Nonnull BsonDocument document) { this.document = document; this.hytaleServerConfig.markChanged(); } void setHytaleServerConfig(@Nonnull HytaleServerConfig hytaleServerConfig) { this.hytaleServerConfig = hytaleServerConfig; this.modules.values().forEach(module -> module.setHytaleServerConfig(hytaleServerConfig)); } } public static class RateLimitConfig { public static final int DEFAULT_PACKETS_PER_SECOND = 2000; public static final int DEFAULT_BURST_CAPACITY = 500; public static final Codec CODEC = BuilderCodec.builder( HytaleServerConfig.RateLimitConfig.class, HytaleServerConfig.RateLimitConfig::new ) .addField(new KeyedCodec<>("Enabled", Codec.BOOLEAN), (o, b) -> o.enabled = b, o -> o.enabled) .addField(new KeyedCodec<>("PacketsPerSecond", Codec.INTEGER), (o, i) -> o.packetsPerSecond = i, o -> o.packetsPerSecond) .addField(new KeyedCodec<>("BurstCapacity", Codec.INTEGER), (o, i) -> o.burstCapacity = i, o -> o.burstCapacity) .build(); private Boolean enabled; private Integer packetsPerSecond; private Integer burstCapacity; transient HytaleServerConfig hytaleServerConfig; public RateLimitConfig() { } public RateLimitConfig(HytaleServerConfig hytaleServerConfig) { this.hytaleServerConfig = hytaleServerConfig; } public boolean isEnabled() { return this.enabled != null ? this.enabled : true; } public void setEnabled(boolean enabled) { this.enabled = enabled; if (this.hytaleServerConfig != null) { this.hytaleServerConfig.markChanged(); } } public int getPacketsPerSecond() { return this.packetsPerSecond != null ? this.packetsPerSecond : 2000; } public void setPacketsPerSecond(int packetsPerSecond) { this.packetsPerSecond = packetsPerSecond; if (this.hytaleServerConfig != null) { this.hytaleServerConfig.markChanged(); } } public int getBurstCapacity() { return this.burstCapacity != null ? this.burstCapacity : 500; } public void setBurstCapacity(int burstCapacity) { this.burstCapacity = burstCapacity; if (this.hytaleServerConfig != null) { this.hytaleServerConfig.markChanged(); } } } }