hytale-server/com/hypixel/hytale/server/core/Options.java

363 lines
15 KiB
Java

package com.hypixel.hytale.server.core;
import com.hypixel.hytale.common.util.PathUtil;
import com.hypixel.hytale.common.util.java.ManifestUtil;
import com.hypixel.hytale.logger.backend.HytaleLoggerBackend;
import com.hypixel.hytale.server.core.io.transport.TransportType;
import com.hypixel.hytale.server.core.universe.world.ValidationOption;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.ValueConversionException;
import joptsimple.ValueConverter;
public class Options {
public static final OptionParser PARSER = new OptionParser();
public static final OptionSpec<Void> HELP = PARSER.accepts("help", "Print's this message.").forHelp();
public static final OptionSpec<Void> VERSION = PARSER.accepts("version", "Prints version information.");
public static final OptionSpec<Void> BARE = PARSER.accepts(
"bare",
"Runs the server bare. For example without loading worlds, binding to ports or creating directories. (Note: Plugins will still be loaded which may not respect this flag)"
);
public static final OptionSpec<Entry<String, Level>> LOG_LEVELS = PARSER.accepts("log", "Sets the logger level.")
.withRequiredArg()
.withValuesSeparatedBy(',')
.withValuesConvertedBy(new Options.LevelValueConverter());
public static final OptionSpec<InetSocketAddress> BIND = PARSER.acceptsAll(List.of("b", "bind"), "Port to listen on")
.withRequiredArg()
.withValuesSeparatedBy(',')
.withValuesConvertedBy(new Options.SocketAddressValueConverter())
.defaultsTo(new InetSocketAddress(5520), new InetSocketAddress[0]);
public static final OptionSpec<TransportType> TRANSPORT = PARSER.acceptsAll(List.of("t", "transport"), "Transport type")
.withRequiredArg()
.ofType(TransportType.class)
.defaultsTo(TransportType.QUIC, new TransportType[0]);
public static final OptionSpec<Void> DISABLE_CPB_BUILD = PARSER.accepts("disable-cpb-build", "Disables building of compact prefab buffers");
public static final OptionSpec<Path> PREFAB_CACHE_DIRECTORY = PARSER.accepts("prefab-cache", "Prefab cache directory for immutable assets")
.withRequiredArg()
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.ANY));
public static final OptionSpec<Path> ASSET_DIRECTORY = PARSER.acceptsAll(List.of("assets"), "Asset directory")
.withRequiredArg()
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.DIR_OR_ZIP))
.defaultsTo(Paths.get("../HytaleAssets"), new Path[0]);
public static final OptionSpec<Path> MODS_DIRECTORIES = PARSER.acceptsAll(List.of("mods"), "Additional mods directories")
.withRequiredArg()
.withValuesSeparatedBy(',')
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.DIR));
public static final OptionSpec<Void> ACCEPT_EARLY_PLUGINS = PARSER.accepts(
"accept-early-plugins", "You acknowledge that loading early plugins is unsupported and may cause stability issues."
);
public static final OptionSpec<Path> EARLY_PLUGIN_DIRECTORIES = PARSER.accepts("early-plugins", "Additional early plugin directories to load from")
.withRequiredArg()
.withValuesSeparatedBy(',')
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.DIR));
public static final OptionSpec<Void> VALIDATE_ASSETS = PARSER.accepts(
"validate-assets", "Causes the server to exit with an error code if any assets are invalid."
);
public static final OptionSpec<ValidationOption> VALIDATE_PREFABS = PARSER.accepts(
"validate-prefabs", "Causes the server to exit with an error code if any prefabs are invalid."
)
.withOptionalArg()
.withValuesSeparatedBy(',')
.ofType(ValidationOption.class);
public static final OptionSpec<Void> VALIDATE_WORLD_GEN = PARSER.accepts(
"validate-world-gen", "Causes the server to exit with an error code if default world gen is invalid."
);
public static final OptionSpec<Void> SHUTDOWN_AFTER_VALIDATE = PARSER.accepts(
"shutdown-after-validate", "Automatically shutdown the server after asset and/or prefab validation."
);
public static final OptionSpec<Void> GENERATE_SCHEMA = PARSER.accepts(
"generate-schema", "Causes the server generate schema, save it into the assets directory and then exit"
);
public static final OptionSpec<Path> WORLD_GEN_DIRECTORY = PARSER.accepts("world-gen", "World gen directory")
.withRequiredArg()
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.DIR));
public static final OptionSpec<Void> DISABLE_FILE_WATCHER = PARSER.accepts("disable-file-watcher");
public static final OptionSpec<Void> DISABLE_SENTRY = PARSER.accepts("disable-sentry");
public static final OptionSpec<Void> DISABLE_ASSET_COMPARE = PARSER.accepts("disable-asset-compare");
public static final OptionSpec<Void> BACKUP = PARSER.accepts("backup");
public static final OptionSpec<Integer> BACKUP_FREQUENCY_MINUTES = PARSER.accepts("backup-frequency")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(30, new Integer[0]);
public static final OptionSpec<Path> BACKUP_DIRECTORY = PARSER.accepts("backup-dir")
.requiredIf(BACKUP, new OptionSpec[0])
.withRequiredArg()
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.DIR));
public static final OptionSpec<Integer> BACKUP_MAX_COUNT = PARSER.accepts("backup-max-count")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(5, new Integer[0]);
public static final OptionSpec<Void> SINGLEPLAYER = PARSER.accepts("singleplayer");
public static final OptionSpec<String> OWNER_NAME = PARSER.accepts("owner-name").withRequiredArg();
public static final OptionSpec<UUID> OWNER_UUID = PARSER.accepts("owner-uuid").withRequiredArg().withValuesConvertedBy(new Options.UUIDConverter());
public static final OptionSpec<Integer> CLIENT_PID = PARSER.accepts("client-pid").withRequiredArg().ofType(Integer.class);
public static final OptionSpec<Path> UNIVERSE = PARSER.accepts("universe")
.withRequiredArg()
.withValuesConvertedBy(new Options.PathConverter(Options.PathConverter.PathType.DIR));
public static final OptionSpec<Void> EVENT_DEBUG = PARSER.accepts("event-debug");
public static final OptionSpec<Boolean> FORCE_NETWORK_FLUSH = PARSER.accepts("force-network-flush")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true, new Boolean[0]);
public static final OptionSpec<Map<String, Path>> MIGRATIONS = PARSER.accepts("migrations", "The migrations to run")
.withRequiredArg()
.withValuesConvertedBy(new Options.StringToPathMapConverter());
public static final OptionSpec<String> MIGRATE_WORLDS = PARSER.accepts("migrate-worlds", "Worlds to migrate")
.availableIf(MIGRATIONS, new OptionSpec[0])
.withRequiredArg()
.withValuesSeparatedBy(',');
public static final OptionSpec<String> BOOT_COMMAND = PARSER.accepts(
"boot-command", "Runs command on boot. If multiple commands are provided they are executed synchronously in order."
)
.withRequiredArg()
.withValuesSeparatedBy(',');
public static final String ALLOW_SELF_OP_COMMAND_STRING = "allow-op";
public static final OptionSpec<Void> ALLOW_SELF_OP_COMMAND = PARSER.accepts("allow-op");
public static final OptionSpec<Options.AuthMode> AUTH_MODE = PARSER.accepts("auth-mode", "Authentication mode")
.withRequiredArg()
.withValuesConvertedBy(new Options.AuthModeConverter())
.defaultsTo(Options.AuthMode.AUTHENTICATED, new Options.AuthMode[0]);
public static final OptionSpec<String> SESSION_TOKEN = PARSER.accepts("session-token", "Session token for Session Service API")
.withRequiredArg()
.ofType(String.class);
public static final OptionSpec<String> IDENTITY_TOKEN = PARSER.accepts("identity-token", "Identity token (JWT)").withRequiredArg().ofType(String.class);
private static OptionSet optionSet;
public static OptionSet getOptionSet() {
return optionSet;
}
public static <T> T getOrDefault(OptionSpec<T> optionSpec, @Nonnull OptionSet optionSet, T def) {
return (T)(!optionSet.has(optionSpec) ? def : optionSet.valueOf(optionSpec));
}
public static boolean parse(String[] args) throws IOException {
optionSet = PARSER.parse(args);
if (optionSet.has(HELP)) {
PARSER.printHelpOn(System.out);
return true;
} else if (optionSet.has(VERSION)) {
String version = ManifestUtil.getImplementationVersion();
String patchline = ManifestUtil.getPatchline();
String environment = "release";
if ("release".equals(patchline)) {
System.out.println("HytaleServer v" + version + " (" + patchline + ")");
} else {
System.out.println("HytaleServer v" + version + " (" + patchline + ", " + environment + ")");
}
return true;
} else {
List<?> nonOptionArguments = optionSet.nonOptionArguments();
if (!nonOptionArguments.isEmpty()) {
System.err.println("Unknown arguments: " + nonOptionArguments);
System.exit(1);
return true;
} else {
if (optionSet.has(LOG_LEVELS)) {
HytaleLoggerBackend.loadLevels(optionSet.valuesOf(LOG_LEVELS));
} else if (optionSet.has(SHUTDOWN_AFTER_VALIDATE)) {
HytaleLoggerBackend.loadLevels(List.of(Map.entry("", Level.WARNING)));
}
return false;
}
}
}
public static enum AuthMode {
AUTHENTICATED,
OFFLINE,
INSECURE;
}
private static class AuthModeConverter implements ValueConverter<Options.AuthMode> {
public Options.AuthMode convert(String value) {
return Options.AuthMode.valueOf(value.toUpperCase());
}
public Class<? extends Options.AuthMode> valueType() {
return Options.AuthMode.class;
}
public String valuePattern() {
return "authenticated|offline|insecure";
}
}
public static class LevelValueConverter implements ValueConverter<Entry<String, Level>> {
private static final Entry<String, Level> ENTRY = Map.entry("", Level.ALL);
@Nonnull
public Entry<String, Level> convert(@Nonnull String value) {
if (!value.contains(":")) {
return Map.entry("", Level.parse(value.toUpperCase()));
} else {
String[] split = value.split(":");
return Map.entry(split[0], Level.parse(split[1].toUpperCase()));
}
}
@Nonnull
public Class<Entry<String, Level>> valueType() {
return (Class<Entry<String, Level>>)ENTRY.getClass();
}
@Nullable
public String valuePattern() {
return null;
}
}
public static class PathConverter implements ValueConverter<Path> {
private final Options.PathConverter.PathType pathType;
public PathConverter(Options.PathConverter.PathType pathType) {
this.pathType = pathType;
}
@Nonnull
public Path convert(@Nonnull String s) {
try {
Path path = PathUtil.get(s);
if (Files.exists(path)) {
switch (this.pathType) {
case FILE:
if (!Files.isRegularFile(path)) {
throw new ValueConversionException("Path must be a file!");
}
break;
case DIR:
if (!Files.isDirectory(path)) {
throw new ValueConversionException("Path must be a directory!");
}
break;
case DIR_OR_ZIP:
if (!Files.isDirectory(path) && (!Files.exists(path) || !path.getFileName().toString().endsWith(".zip"))) {
throw new ValueConversionException("Path must be a directory or zip!");
}
}
}
return path;
} catch (InvalidPathException var3) {
throw new ValueConversionException("Failed to parse '" + s + "' to path!", var3);
}
}
@Nonnull
public Class<? extends Path> valueType() {
return Path.class;
}
@Nullable
public String valuePattern() {
return null;
}
public static enum PathType {
FILE,
DIR,
DIR_OR_ZIP,
ANY;
}
}
public static class SocketAddressValueConverter implements ValueConverter<InetSocketAddress> {
@Nonnull
public InetSocketAddress convert(@Nonnull String value) {
if (value.contains(":")) {
String[] split = value.split(":");
return new InetSocketAddress(split[0], Integer.parseInt(split[1]));
} else {
try {
return new InetSocketAddress(Integer.parseInt(value));
} catch (NumberFormatException var3) {
return new InetSocketAddress(value, 5520);
}
}
}
@Nonnull
public Class<? extends InetSocketAddress> valueType() {
return InetSocketAddress.class;
}
@Nullable
public String valuePattern() {
return null;
}
}
public static class StringToPathMapConverter implements ValueConverter<Map<String, Path>> {
private static final Map<String, Level> MAP = new Object2ObjectOpenHashMap();
@Nonnull
public Map<String, Path> convert(@Nonnull String value) {
HashMap<String, Path> map = new HashMap<>();
String[] strings = value.split(",");
for (String string : strings) {
String[] split = string.split("=");
if (split.length == 2) {
if (map.containsKey(split[0])) {
throw new ValueConversionException("String '" + split[0] + "' has already been specified!");
}
Path path = PathUtil.get(split[1]);
if (!Files.exists(path)) {
throw new ValueConversionException("No file found for '" + split[1] + "'!");
}
map.put(split[0], path);
}
}
return map;
}
@Nonnull
public Class<Map<String, Path>> valueType() {
return (Class<Map<String, Path>>)MAP.getClass();
}
@Nullable
public String valuePattern() {
return null;
}
}
public static class UUIDConverter implements ValueConverter<UUID> {
@Nonnull
public UUID convert(@Nonnull String s) {
return UUID.fromString(s);
}
@Nonnull
public Class<? extends UUID> valueType() {
return UUID.class;
}
@Nullable
public String valuePattern() {
return null;
}
}
}