From e1e0bb692848a29de22b525f9338d648807736b0 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Thu, 14 Dec 2023 23:51:19 +0100 Subject: [PATCH 01/10] Added docker support - Added the multi-staged Dockerfile - Added the docker-compose.yml file - It also includes a MongoDB service - Added the entrypoint for the Docker image --- Dockerfile | 38 ++++++++ docker-compose.yml | 30 ++++++ entrypoint.sh | 5 + generate-config.ts | 228 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100755 entrypoint.sh create mode 100644 generate-config.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..47d0b75654a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM gradle:8.5.0-jdk17-alpine as builder + +WORKDIR /app + +COPY ./ /app/ + +RUN gradle jar --no-daemon + +FROM bitnami/git:2.43.0-debian-11-r1 as data + +ARG DATA_BRANCH=4.0 + +WORKDIR /app + +RUN git clone --branch ${DATA_BRANCH} --depth 1 https://gitlab.com/YuukiPS/GC-Resources.git + +FROM bitnami/java:21.0.1-12 + +RUN apt-get update && apt-get install unzip + +WORKDIR /app + +# Install bun for generating the configuration file +RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.0.0" + +# Copy built assets +COPY --from=builder /app/grasscutter-1.7.4.jar /app/grasscutter.jar +COPY --from=builder /app/keystore.p12 /app/keystore.p12 + +# Copy the resources +COPY --from=data /app/GC-Resources/Resources /app/resources/ + +# Copy startup files +COPY ./entrypoint.sh ./generate-config.ts /app/ + +CMD [ "sh", "/app/entrypoint.sh" ] + +EXPOSE 80 443 8888 22102 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..6b8d08bf3a2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.8" +services: + grasscutter: + image: grasscutter:latest + build: . + ports: + - "80:80" + - "443:443" + - "8080:8080" + - "8888:8888" + - "22102:22102" + environment: + DATABASE_INFO_SERVER_CONNECTION_URI: "mongodb://lawnmower:grasscutter@database:27017" + DATABASE_INFO_SERVER_COLLECTION: grasscutter + DATABASE_INFO_GAME_CONNECTION_URI: "mongodb://lawnmower:grasscutter@database:27017" + DATABASE_INFO_GAME_COLLECTION: grasscutter + + stdin_open: true + + database: + image: mongo:7.0.4 + environment: + MONGO_INITDB_ROOT_USERNAME: lawnmower + MONGO_INITDB_ROOT_PASSWORD: grasscutter + MONGO_INITDB_DATABASE: grasscutter + volumes: + - mongodata:/data/db + +volumes: + mongodata: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000000..298252a3014 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#/bin/sh + +$HOME/.bun/bin/bun run /app/generate-config.ts + +java -jar /app/grasscutter.jar \ No newline at end of file diff --git a/generate-config.ts b/generate-config.ts new file mode 100644 index 00000000000..bf034ad6185 --- /dev/null +++ b/generate-config.ts @@ -0,0 +1,228 @@ +import { writeFileSync } from "fs"; + +const configToSave = { + folderStructure: { + resources: getStringFromEnv("FOLDER_STRUCTURE_RESOURCES", "./resources/"), + data: getStringFromEnv("FOLDER_STRUCTURE_DATA", "./data/"), + packets: getStringFromEnv("FOLDER_STRUCTURE_PACKETS", "./packets/"), + scripts: getStringFromEnv("FOLDER_STRUCTURE_SCRIPTS", "./resources/Scripts/"), + plugins: getStringFromEnv("FOLDER_STRUCTURE_PLUGINS", "./plugins/"), + }, + databaseInfo: { + server: { + connectionUri: getStringFromEnv("DATABASE_INFO_SERVER_CONNECTION_URI", "mongodb://localhost:27017"), + collection: getStringFromEnv("DATABASE_INFO_SERVER_COLLECTION", "grasscutter"), + }, + game: { + connectionUri: getStringFromEnv("DATABASE_INFO_GAME_CONNECTION_URI", "mongodb://localhost:27017"), + collection: getStringFromEnv("DATABASE_INFO_GAME_COLLECTION", "grasscutter"), + }, + }, + language: { + language: getStringFromEnv("LANGUAGE_LANGUAGE", "en_US"), + fallback: getStringFromEnv("LANGUAGE_FALLBACK", "en_US"), + document: getStringFromEnv("LANGUAGE_DOCUMENT", "EN"), + }, + account: { + autoCreate: getBoolFromEnv("ACCOUNT_AUTO_CREATE", false), + EXPERIMENTAL_RealPassword: getBoolFromEnv("ACCOUNT_EXPERIMENTAL_REAL_PASSWORD", false), + defaultPermissions: getStringArrayFromEnv("ACCOUNT_DEFAULT_PERMISSIONS", []), + maxPlayer: getIntFromEnv("ACCOUNT_MAX_PLAYER", -1), + }, + server: { + debugWhitelist: getStringArrayFromEnv("SERVER_DEBUG_WHITELIST", []), + debugBlacklist: getStringArrayFromEnv("SERVER_DEBUG_BLACKLIST", []), + runMode: getStringFromEnv("SERVER_RUN_MODE", "HYBRID"), + logCommands: getBoolFromEnv("SERVER_LOG_COMMANDS", false), + http: { + bindAddress: getStringFromEnv("SERVER_HTTP_BIND_ADDRESS", "0.0.0.0"), + bindPort: getIntFromEnv("SERVER_HTTP_BIND_PORT", 443), + accessAddress: getStringFromEnv("SERVER_HTTP_ACCESS_ADDRESS", "127.0.0.1"), + accessPort: getIntFromEnv("SERVER_HTTP_ACCESS_PORT", 0), + encryption: { + useEncryption: getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_ENCRYPTION", true), + useInRouting: getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_IN_ROUTING", true), + keystore: getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE", "./keystore.p12"), + keystorePassword: getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE_PASSWORD", "123456"), + }, + policies: { + cors: { + enabled: getBoolFromEnv("SERVER_HTTP_POLICIES_CORS_ENABLED", false), + allowedOrigins: getStringArrayFromEnv("SERVER_HTTP_POLICIES_CORS_ALLOWED_ORIGINS", ["*"]), + }, + }, + files: { + indexFile: getStringFromEnv("SERVER_HTTP_FILES_INDEX_FILE", "./index.html"), + errorFile: getStringFromEnv("SERVER_HTTP_FILES_ERROR_FILE", "./404.html"), + }, + }, + game: { + bindAddress: getStringFromEnv("SERVER_GAME_BIND_ADDRESS", "0.0.0.0"), + bindPort: getIntFromEnv("SERVER_GAME_BIND_PORT", 22102), + accessAddress: getStringFromEnv("SERVER_GAME_ACCESS_ADDRESS", "127.0.0.1"), + accessPort: getIntFromEnv("SERVER_GAME_ACCESS_PORT", 0), + loadEntitiesForPlayerRange: getIntFromEnv("SERVER_GAME_LOAD_ENTITIES_FOR_PLAYER_RANGE", 100), + enableScriptInBigWorld: getBoolFromEnv("SERVER_GAME_ENABLE_SCRIPT_IN_BIG_WORLD", false), + enableConsole: getBoolFromEnv("SERVER_GAME_ENABLE_CONSOLE", true), + kcpInterval: getIntFromEnv("SERVER_GAME_KCP_INTERVAL", 20), + logPackets: getStringFromEnv("SERVER_GAME_LOG_PACKETS", "NONE"), + gameOptions: { + inventoryLimits: { + weapons: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_WEAPONS", 2000), + relics: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_RELICS", 2000), + materials: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_MATERIALS", 2000), + furniture: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_FURNITURE", 2000), + all: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_ALL", 30000), + }, + avatarLimits: { + singlePlayerTeam: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_SINGLE_PLAYER_TEAM", 4), + multiplayerTeam: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_MULTIPLAYER_TEAM", 4), + }, + sceneEntityLimit: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_SCENE_ENTITY_LIMIT", 1000), + watchGachaConfig: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_WATCH_GACHA_CONFIG", false), + enableShopItems: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENABLE_SHOP_ITEMS", true), + staminaUsage: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_STAMINA_USAGE", true), + energyUsage: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENERGY_USAGE", true), + fishhookTeleport: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_FISHHOOK_TELEPORT", true), + resinOptions: { + resinUsage: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RESIN_USAGE", false), + cap: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_CAP", 160), + rechargeTime: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RECHARGE_TIME", 480), + }, + rates: { + adventureExp: getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_ADVENTURE_EXP", 1.0), + mora: getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_MORA", 1.0), + leyLines: getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_LEY_LINES", 1.0), + }, + }, + joinOptions: { + welcomeEmotes: [2007, 1002, 4010], + welcomeMessage: getStringFromEnv( + "SERVER_GAME_JOIN_OPTIONS_WELCOME_MESSAGE", + "Welcome to a Grasscutter server." + ), + welcomeMail: { + title: getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_TITLE", "Welcome to Grasscutter!"), + content: getStringFromEnv( + "SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_CONTENT", + 'Hi there!\r\nFirst of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r\n\r\nCheck out our:\r\n\u003ctype\u003d"browser" text\u003d"Discord" href\u003d"https://discord.gg/T5vZU6UyeG"/\u003e\n' + ), + sender: getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_SENDER", "Lawnmower"), + items: getItemsFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_ITEMS", [ + { + itemId: 13509, + itemCount: 1, + itemLevel: 1, + }, + { + itemId: 201, + itemCount: 99999, + itemLevel: 1, + }, + ]), + }, + }, + serverAccount: { + avatarId: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_AVATAR_ID", 10000007), + nameCardId: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_NAME_CARD_ID", 210001), + adventureRank: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_ADVENTURE_RANK", 1), + worldLevel: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_WORLD_LEVEL", 0), + nickName: getStringFromEnv("SERVER_GAME_SERVER_ACCOUNT_NICK_NAME", "Server"), + signature: getStringFromEnv("SERVER_GAME_SERVER_ACCOUNT_SIGNATURE", "Welcome to Grasscutter!"), + }, + }, + dispatch: { + regions: getStringArrayFromEnv("SERVER_DISPATCH_REGIONS", []), + defaultName: getStringFromEnv("SERVER_DISPATCH_DEFAULT_NAME", "Grasscutter"), + logRequests: getStringFromEnv("SERVER_DISPATCH_LOG_REQUESTS", "NONE"), + }, + }, + version: 4, +}; + +writeFileSync("./config.json", JSON.stringify(configToSave, null, 4)); + +function getStringFromEnv(key: string, defaultValue: string): string { + return process.env[key] || defaultValue; +} + +function getBoolFromEnv(key: string, defaultValue: boolean): boolean { + switch (process.env[key]) { + case "true": + case "on": + case "1": + return true; + + case "false": + case "off": + case "0": + return false; + + default: + return defaultValue; + } +} + +function getIntFromEnv(key: string, defaultValue: number): number { + const currentValue = process.env[key]; + + if (currentValue === undefined || currentValue === null) { + return defaultValue; + } + + try { + return parseInt(currentValue, 10); + } catch (error) { + return defaultValue; + } +} + +function getFloatFromEnv(key: string, defaultValue: number): number { + const currentValue = process.env[key]; + + if (currentValue === undefined || currentValue === null) { + return defaultValue; + } + + try { + return parseFloat(currentValue); + } catch (error) { + return defaultValue; + } +} + +function getStringArrayFromEnv(key: string, defaultValue: string[], separator: string = ","): string[] { + const currentValue = process.env[key]; + + if (currentValue === undefined || currentValue === null) { + return defaultValue; + } + + return currentValue.split(separator); +} + +type ItemInfo = { + itemId: number; + itemCount: number; + itemLevel: number; +}; + +function getItemsFromEnv(key: string, defaultValue: ItemInfo[]): ItemInfo[] { + const currentValue = process.env[key]; + + if (currentValue === undefined || currentValue === null) { + return defaultValue; + } + + const parts = currentValue.split("|"); + + return parts.map((part: string) => { + const [rawItemId, rawItemCount, rawItemLevel] = part.split(","); + + return { + itemId: parseInt(rawItemId, 10), + itemCount: parseInt(rawItemCount, 10), + itemLevel: parseInt(rawItemLevel, 10), + }; + }); +} From 60e713f4ff8c6ae62e33ecedc6c8e6a26b8f789e Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Fri, 15 Dec 2023 23:41:53 +0100 Subject: [PATCH 02/10] Refactor ConfigContainer to use environment variables BREAKING CHANGE: This will make the config.json obsolete! --- .../java/emu/grasscutter/Grasscutter.java | 19 +- .../grasscutter/config/ConfigContainer.java | 527 ++++++++++++------ 2 files changed, 348 insertions(+), 198 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 8c3248dd7c2..197126ec065 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -77,8 +77,6 @@ public final class Grasscutter { // Load server configuration. Grasscutter.loadConfig(); - // Attempt to update configuration. - ConfigContainer.updateConfig(); Grasscutter.getLogger().info("Loading Grasscutter..."); @@ -238,22 +236,7 @@ public static void loadLanguage() { /** Attempts to load the configuration from a file. */ public static void loadConfig() { // Check if config.json exists. If not, we generate a new config. - if (!configFile.exists()) { - getLogger().info("config.json could not be found. Generating a default configuration ..."); - config = new ConfigContainer(); - Grasscutter.saveConfig(config); - return; - } - - // If the file already exists, we attempt to load it. - try { - config = JsonUtils.loadToClass(configFile.toPath(), ConfigContainer.class); - } catch (Exception exception) { - getLogger() - .error( - "There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); - System.exit(1); - } + config = new ConfigContainer(); } /** diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index fd264e26bb8..e087ffe4579 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -1,81 +1,257 @@ package emu.grasscutter.config; import ch.qos.logback.classic.Level; -import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.utils.*; +import emu.grasscutter.utils.Crypto; +import emu.grasscutter.utils.Utils; import lombok.NoArgsConstructor; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; -import static emu.grasscutter.Grasscutter.*; +import static emu.grasscutter.Grasscutter.ServerDebugMode; +import static emu.grasscutter.Grasscutter.ServerRunMode; /** * *when your JVM fails* */ public class ConfigContainer { - /* - * Configuration changes: - * Version 5 - 'questing' has been changed from a boolean - * to a container of options ('questOptions'). - * This field will be removed in future versions. - * Version 6 - 'questing' has been fully replaced with 'questOptions'. - * The field for 'legacyResources' has been removed. - * Version 7 - 'regionKey' is being added for authentication - * with the new dispatch server. - * Version 8 - 'server' is being added for enforcing handbook server - * addresses. - * Version 9 - 'limits' was added for handbook requests. - * Version 10 - 'trialCostumes' was added for enabling costumes - * on trial avatars. - * Version 11 - 'server.fastRequire' was added for disabling the new - * Lua script require system if performance is a concern. - * Version 12 - 'http.startImmediately' was added to control whether the - * HTTP server should start immediately. - * Version 13 - 'game.useUniquePacketKey' was added to control whether the - * encryption key used for packets is a constant or randomly generated. + /** + * Retrieves the given key from the environment variables. + *

+ * When the key is not set it will return the given default value. + * + * @param key The name of the environment variable + * @param defaultValue The default value when the key is not set + * @return The value from the environment variable or the default value */ - private static int version() { - return 13; + static String getStringFromEnv(String key, String defaultValue) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + return currentValue; } /** - * Attempts to update the server's existing configuration. + * Retrieves the given key from the environment variables and tries to parse it as integer. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exists or is not a valid integer + * @return The parsed integer or the default value */ - public static void updateConfig() { - try { // Check if the server is using a legacy config. - var configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class); - if (!configObject.has("version")) { - Grasscutter.getLogger().info("Updating legacy config..."); - Grasscutter.saveConfig(null); - } - } catch (Exception ignored) { } - - var existing = config.version; - var latest = version(); - - if (existing == latest) - return; - - // Create a new configuration instance. - var updated = new ConfigContainer(); - // Update all configuration fields. - var fields = ConfigContainer.class.getDeclaredFields(); - Arrays.stream(fields).forEach(field -> { - try { - field.set(updated, field.get(config)); - } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to update a configuration field.", exception); - } - }); updated.version = version(); + static int getIntFromEnv(String key, int defaultValue) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + try { + return Integer.parseInt(currentValue, 10); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Retrieves the given key from the environment variables and tries to parse it as float. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist or is not a valid float + * @return The parsed float or the default value + */ + static float getFloatFromEnv(String key, float defaultValue) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + try { + return Float.parseFloat(currentValue); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Retrieves the given key from the environment variables and tries to parse it as float. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exists or is not a valid bool + * @return The parsed boolean or the default value + */ + static boolean getBoolFromEnv(String key, boolean defaultValue) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + return switch (currentValue.trim()) { + case "true", "on", "1" -> true; + case "false", "off", "0" -> false; + default -> defaultValue; + }; + } + + /** + * Retrieves the given from the environment variables and tries to parse it as a Set. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist or is not a valid set + * @param separator The separator which will be used for splitting up the string + * @return The parsed set or the default value + */ + static Set getStringSetFromEnv(String key, Set defaultValue, String separator) { + var currentValue = System.getenv(key); - try { // Save configuration and reload. - Grasscutter.saveConfig(updated); - Grasscutter.loadConfig(); - } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to save the updated configuration.", exception); + if (currentValue == null) { + return defaultValue; } + + var parts = currentValue.split(separator); + + return Set.of(parts); + } + + /** + * Retrieves the given key from the environment variables and tries to parse it as a string array. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable + * @param defaultValue The default value when the environment variable does not exist + * @param separator The separator which will be used for splitting up the environment variable + * @return The parsed integer set or the default value + */ + static Set getIntSetFromEnv(String key, Set defaultValue, String separator) { + var defaultValues = defaultValue.stream().map(Object::toString).collect(Collectors.toSet()); + var currentValue = getStringSetFromEnv(key, defaultValues, separator); + + return currentValue.stream().map(entry -> Integer.parseInt(entry, 10)).collect(Collectors.toSet()); + } + + /** + * Retrieves the given key from the environment variables and tries to parse it as an enum member. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable to parse + * @param enumClass The enum class which contains all members + * @param defaultValue The default value when the environment variable does not exists or is not a valid enum member + * @param The type of the enum member + * @return The parsed enum member or the default value + */ + static > T getEnumFromEnv(String key, Class enumClass, T defaultValue) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + try { + return Enum.valueOf(enumClass, currentValue); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Retrieves the given key from the environment variables and tries to parse it as string array. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist + * @param separator The separator which will be used for splitting up the string + * @return The parsed string array or the default value + */ + static String[] getStringArrayFromEnv(String key, String[] defaultValue, String separator) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + return currentValue.split(separator); + } + + static int[] getIntArrayFromEnv(String key, int[] defaultValue, String separator) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + return Arrays.stream(currentValue.split(separator)).mapToInt(Integer::parseInt).toArray(); + } + + static emu.grasscutter.game.mail.Mail.MailItem[] getMailItemsFromEnv(String key, emu.grasscutter.game.mail.Mail.MailItem[] defaultValue, String partsSeparator, String valuesSeparator) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + var parts = Arrays.stream(currentValue.split(partsSeparator)).map(part -> part.split(valuesSeparator)); + + return (emu.grasscutter.game.mail.Mail.MailItem[]) parts.filter(part -> part.length != 3).map(part -> { + var itemId = Integer.parseInt(part[0], 10); + var itemCount = Integer.parseInt(part[1], 10); + var itemLevel = Integer.parseInt(part[2], 10); + + return new emu.grasscutter.game.mail.Mail.MailItem(itemId, itemCount, itemLevel); + }).toArray(); + } + + static VisionOptions[] getVisionOptionsFromEnv(String key, VisionOptions[] defaultValue, String partsSeparator, String valuesSeparator) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + var parts = currentValue.split(partsSeparator); + return (VisionOptions[]) Arrays.stream(parts).map(part -> part.split(valuesSeparator)).filter(values -> values.length == 3).map(values -> { + var name = values[0]; + var visionRange = Integer.parseInt(values[1]); + var gridWidth = Integer.parseInt(values[2]); + + return new VisionOptions(name, visionRange, gridWidth); + }).toArray(); + } + + static List getRegionsFromEnv(String key, List defaultValue, String partsSeparator, String valuesSeparator) { + var currentValue = System.getenv(key); + + if (currentValue == null) { + return defaultValue; + } + + var parts = currentValue.split(partsSeparator); + return Arrays.stream(parts).map(part -> part.split(valuesSeparator)).filter(values -> values.length == 4).map(values -> { + var name = values[0]; + var title = values[1]; + var address = values[2]; + var port = Integer.parseInt(values[3]); + + return new Region(name, title, address, port); + }).collect(Collectors.toList()); } public Structure folderStructure = new Structure(); @@ -84,9 +260,6 @@ public static void updateConfig() { public Account account = new Account(); public Server server = new Server(); - // DO NOT. TOUCH. THE VERSION NUMBER. - public int version = version(); - /* Option containers. */ public static class Database { @@ -100,28 +273,29 @@ public static class DataStore { } public static class Structure { - public String resources = "./resources/"; - public String data = "./data/"; - public String packets = "./packets/"; - public String scripts = "resources:Scripts/"; - public String plugins = "./plugins/"; - public String cache = "./cache/"; + public String resources = getStringFromEnv("FOLDER_STRUCTURE_RESOURCES", "./resources/"); + public String data = getStringFromEnv("FOLDER_STRUCTURE_DATA", "./data/"); + public String packets = getStringFromEnv("FOLDER_STRUCTURE_PACKETS", "./packets/"); + public String scripts = getStringFromEnv("FOLDER_STRUCTURE_SCRIPTS", "resources:Scripts/"); + public String plugins = getStringFromEnv("FOLDER_STRUCTURE_PLUGINS", "./plugins/"); + public String cache = getStringFromEnv("FOLDER_STRUCTURE_CACHE", "./cache/"); // UNUSED (potentially added later?) // public String dumps = "./dumps/"; } public static class Server { - public Set debugWhitelist = Set.of(); - public Set debugBlacklist = Set.of(); - public ServerRunMode runMode = ServerRunMode.HYBRID; - public boolean logCommands = false; + public Set debugWhitelist = getIntSetFromEnv("SERVER_DEBUG_WHITELIST", Set.of(), ","); + public Set debugBlacklist = getIntSetFromEnv("SERVER_DEBUG_BLACKLIST", Set.of(), ","); + public ServerRunMode runMode = getEnumFromEnv("SERVER_RUN_MODE", ServerRunMode.class, ServerRunMode.HYBRID); + + public boolean logCommands = getBoolFromEnv("SERVER_LOG_COMMANDS", false); /** * If enabled, the 'require' Lua function will load the script's compiled varient into the context. (faster; doesn't work as well) * If disabled, all 'require' calls will be replaced with the referenced script's source. (slower; works better) */ - public boolean fastRequire = true; + public boolean fastRequire = getBoolFromEnv("SERVER_FAST_REQUIRE", true); public HTTP http = new HTTP(); public Game game = new Game(); @@ -133,29 +307,29 @@ public static class Server { public static class Language { public Locale language = Locale.getDefault(); public Locale fallback = Locale.US; - public String document = "EN"; + public String document = getStringFromEnv("LANGUAGE_DOCUMENT", "EN"); } public static class Account { - public boolean autoCreate = false; - public boolean EXPERIMENTAL_RealPassword = false; - public String[] defaultPermissions = {}; - public int maxPlayer = -1; + public boolean autoCreate = getBoolFromEnv("ACCOUNT_AUTO_CREATE", false); + public boolean EXPERIMENTAL_RealPassword = getBoolFromEnv("ACCOUNT_EXPERIMENTAL_REAL_PASSWORD", false); + public String[] defaultPermissions = getStringArrayFromEnv("ACCOUNT_DEFAULT_PERMISSIONS", new String[]{}, ","); + public int maxPlayer = getIntFromEnv("ACCOUNT_MAX_PLAYER", -1); } /* Server options. */ public static class HTTP { /* This starts the HTTP server before the game server. */ - public boolean startImmediately = false; + public boolean startImmediately = getBoolFromEnv("SERVER_HTTP_START_IMMEDIATELY", false); - public String bindAddress = "0.0.0.0"; - public int bindPort = 443; + public String bindAddress = getStringFromEnv("SERVER_HTTP_BIND_ADDRESS", "0.0.0.0"); + public int bindPort = getIntFromEnv("SERVER_HTTP_BIND_PORT", 443); /* This is the address used in URLs. */ - public String accessAddress = "127.0.0.1"; + public String accessAddress = getStringFromEnv("SERVER_HTTP_ACCESS_ADDRESS", "127.0.0.1"); /* This is the port used in URLs. */ - public int accessPort = 0; + public int accessPort = getIntFromEnv("SERVER_HTTP_ACCESS_PORT", 0); public Encryption encryption = new Encryption(); public Policies policies = new Policies(); @@ -163,66 +337,65 @@ public static class HTTP { } public static class Game { - public String bindAddress = "0.0.0.0"; - public int bindPort = 22102; + public String bindAddress = getStringFromEnv("SERVER_GAME_BIND_ADDRESS", "0.0.0.0"); + public int bindPort = getIntFromEnv("SERVER_GAME_BIND_PORT", 22102); /* This is the address used in the default region. */ - public String accessAddress = "127.0.0.1"; + public String accessAddress = getStringFromEnv("SERVER_GAME_ACCESS_ADDRESS", "127.0.0.1"); /* This is the port used in the default region. */ - public int accessPort = 0; + public int accessPort = getIntFromEnv("SERVER_GAME_ACCESS_PORT", 0); /* Enabling this will generate a unique packet encryption key for each player. */ - public boolean useUniquePacketKey = true; + public boolean useUniquePacketKey = getBoolFromEnv("SERVER_GAME_USE_UNIQUE_PACKET_KEY", true); /* Entities within a certain range will be loaded for the player */ - public int loadEntitiesForPlayerRange = 300; + public int loadEntitiesForPlayerRange = getIntFromEnv("SERVER_GAME_LOAD_ENTITIES_FOR_PLAYER_RANGE", 300); /* Start in 'unstable-quests', Lua scripts will be enabled by default. */ - public boolean enableScriptInBigWorld = true; - public boolean enableConsole = true; + public boolean enableScriptInBigWorld = getBoolFromEnv("SERVER_GAME_ENABLE_SCRIPT_IN_BIG_WORLD", true); + public boolean enableConsole = getBoolFromEnv("SERVER_GAME_ENABLE_CONSOLE", true); /* Kcp internal work interval (milliseconds) */ - public int kcpInterval = 20; + public int kcpInterval = getIntFromEnv("SERVER_GAME_KCP_INTERVAL", 20); /* Controls whether packets should be logged in console or not */ - public ServerDebugMode logPackets = ServerDebugMode.NONE; + public ServerDebugMode logPackets = getEnumFromEnv("SERVER_GAME_LOG_PACKETS", ServerDebugMode.class, ServerDebugMode.NONE); /* Show packet payload in console or no (in any case the payload is shown in encrypted view) */ - public boolean isShowPacketPayload = false; + public boolean isShowPacketPayload = getBoolFromEnv("SERVER_GAME_IS_SHOW_PACKET_PAYLOAD", false); /* Show annoying loop packets or no */ - public boolean isShowLoopPackets = false; + public boolean isShowLoopPackets = getBoolFromEnv("SERVER_GAME_IS_SHOW_LOOP_PACKETS", false); - public boolean cacheSceneEntitiesEveryRun = false; + public boolean cacheSceneEntitiesEveryRun = getBoolFromEnv("SERVER_GAME_CACHE_SCENE_ENTITIES_EVERY_RUN", false); public GameOptions gameOptions = new GameOptions(); public JoinOptions joinOptions = new JoinOptions(); public ConsoleAccount serverAccount = new ConsoleAccount(); - public VisionOptions[] visionOptions = new VisionOptions[] { - new VisionOptions("VISION_LEVEL_NORMAL" , 80 , 20), - new VisionOptions("VISION_LEVEL_LITTLE_REMOTE" , 16 , 40), - new VisionOptions("VISION_LEVEL_REMOTE" , 1000 , 250), - new VisionOptions("VISION_LEVEL_SUPER" , 4000 , 1000), - new VisionOptions("VISION_LEVEL_NEARBY" , 40 , 20), - new VisionOptions("VISION_LEVEL_SUPER_NEARBY" , 20 , 20) - }; + public VisionOptions[] visionOptions = getVisionOptionsFromEnv("SERVER_GAME_VISION_OPTIONS", new VisionOptions[]{ + new VisionOptions("VISION_LEVEL_NORMAL", 80, 20), + new VisionOptions("VISION_LEVEL_LITTLE_REMOTE", 16, 40), + new VisionOptions("VISION_LEVEL_REMOTE", 1000, 250), + new VisionOptions("VISION_LEVEL_SUPER", 4000, 1000), + new VisionOptions("VISION_LEVEL_NEARBY", 40, 20), + new VisionOptions("VISION_LEVEL_SUPER_NEARBY", 20, 20) + }, "|", ","); } /* Data containers. */ public static class Dispatch { /* An array of servers. */ - public List regions = List.of(); + public List regions = getRegionsFromEnv("SERVER_DISPATCH_REGIONS", List.of(), "|", ","); /* The URL used to make HTTP requests to the dispatch server. */ - public String dispatchUrl = "ws://127.0.0.1:1111"; + public String dispatchUrl = getStringFromEnv("SERVER_DISPATCH_DISPATCH_URL", "ws://127.0.0.1:1111"); /* A unique key used for encryption. */ - public byte[] encryptionKey = Crypto.createSessionKey(32); + public byte[] encryptionKey = Utils.base64Decode(getStringFromEnv("SERVER_DISPATCH_ENCRYPTION_KEY", Utils.base64Encode(Crypto.createSessionKey(32)))); /* A unique key used for authentication. */ - public String dispatchKey = Utils.base64Encode( - Crypto.createSessionKey(32)); + public String dispatchKey = getStringFromEnv("SERVER_DISPATCH_DISPATCH_KEY", Utils.base64Encode(Crypto.createSessionKey(32))); - public String defaultName = "Grasscutter"; + public String defaultName = getStringFromEnv("SERVER_DISPATCH_DEFAULT_NAME", "Grasscutter"); /* Controls whether http requests should be logged in console or not */ - public ServerDebugMode logRequests = ServerDebugMode.NONE; + public ServerDebugMode logRequests = getEnumFromEnv("SERVER_DISPATCH_SERVER_DEBUG_MODE", ServerDebugMode.class, ServerDebugMode.NONE); } /* Debug options container, used when jar launch argument is -debug | -debugall and override default values @@ -236,46 +409,46 @@ public static class DebugMode { public Level servicesLoggersLevel = Level.INFO; /* Controls whether packets should be logged in console or not */ - public ServerDebugMode logPackets = ServerDebugMode.ALL; + public ServerDebugMode logPackets = getEnumFromEnv("SERVER_DEBUG_MODE_LOG_PACKETS", ServerDebugMode.class, ServerDebugMode.ALL); /* Show packet payload in console or no (in any case the payload is shown in encrypted view) */ - public boolean isShowPacketPayload = false; + public boolean isShowPacketPayload = getBoolFromEnv("SERVER_DEBUG_MODE_IS_SHOW_PACKET_PAYLOAD", false); /* Show annoying loop packets or no */ - public boolean isShowLoopPackets = false; + public boolean isShowLoopPackets = getBoolFromEnv("SERVER_DEBUG_MODE_IS_SHOW_LOOP_PACKETS", false); /* Controls whether http requests should be logged in console or not */ - public ServerDebugMode logRequests = ServerDebugMode.ALL; + public ServerDebugMode logRequests = getEnumFromEnv("SERVER_DEBUG_MODE_LOG_REQUESTS", ServerDebugMode.class, ServerDebugMode.ALL); } public static class Encryption { - public boolean useEncryption = true; + public boolean useEncryption = getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_ENCRYPTION", true); /* Should 'https' be appended to URLs? */ - public boolean useInRouting = true; - public String keystore = "./keystore.p12"; - public String keystorePassword = "123456"; + public boolean useInRouting = getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_IN_ROUTING", true); + public String keystore = getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE", "./keystore.p12"); + public String keystorePassword = getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE_PASSWORD", "123456"); } public static class Policies { public Policies.CORS cors = new Policies.CORS(); public static class CORS { - public boolean enabled = true; - public String[] allowedOrigins = new String[]{"*"}; + public boolean enabled = getBoolFromEnv("SERVER_HTTP_POLICIES_CORS_ENABLED", true); + public String[] allowedOrigins = getStringArrayFromEnv("SERVER_HTTP_POLICIES_ALLOWED_ORIGINS", new String[]{"*"}, ","); } } public static class GameOptions { public InventoryLimits inventoryLimits = new InventoryLimits(); public AvatarLimits avatarLimits = new AvatarLimits(); - public int sceneEntityLimit = 1000; // Unenforced. TODO: Implement. + public int sceneEntityLimit = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_SCENE_ENTITY_LIMIT", 1000); // Unenforced. TODO: Implement. - public boolean watchGachaConfig = false; - public boolean enableShopItems = true; - public boolean staminaUsage = true; - public boolean energyUsage = true; - public boolean fishhookTeleport = true; - public boolean trialCostumes = false; + public boolean watchGachaConfig = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_WATCH_GACHA_CONFIG", false); + public boolean enableShopItems = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENABLE_SHOP_ITEMS", true); + public boolean staminaUsage = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_STAMINA_USAGE", true); + public boolean energyUsage = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENERGY_USAGE", true); + public boolean fishhookTeleport = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_FISHHOOK_TELEPORT", true); + public boolean trialCostumes = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_TRIAL_COSTUMES", false); @SerializedName(value = "questing", alternate = "questOptions") public Questing questing = new Questing(); @@ -285,63 +458,63 @@ public static class GameOptions { public HandbookOptions handbook = new HandbookOptions(); public static class InventoryLimits { - public int weapons = 2000; - public int relics = 2000; - public int materials = 2000; - public int furniture = 2000; - public int all = 30000; + public int weapons = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_WEAPONS", 2000); + public int relics = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_RELICS", 2000); + public int materials = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_MATERIALS", 2000); + public int furniture = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_FURNITURE", 2000); + public int all = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_ALL", 30000); } public static class AvatarLimits { - public int singlePlayerTeam = 4; - public int multiplayerTeam = 4; + public int singlePlayerTeam = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_SINGLE_PLAYER_TEAM", 4); + public int multiplayerTeam = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_MULTIPLAYER_TEAM", 4); } public static class Rates { - public float adventureExp = 1.0f; - public float mora = 1.0f; - public float leyLines = 1.0f; + public float adventureExp = getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_ADVENTURE_EXP", 1.0f); + public float mora = getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_MORA", 1.0f); + public float leyLines = getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_LEY_LINES", 1.0f); } public static class ResinOptions { - public boolean resinUsage = false; - public int cap = 160; - public int rechargeTime = 480; + public boolean resinUsage = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RESIN_USAGE", false); + public int cap = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_CAP", 160); + public int rechargeTime = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RECHARGE_TIME", 480); } public static class Questing { /* Should questing behavior be used? */ - public boolean enabled = true; + public boolean enabled = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_QUESTING_ENABLED", true); } public static class HandbookOptions { - public boolean enable = false; - public boolean allowCommands = true; + public boolean enable = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_ENABLE", false); + public boolean allowCommands = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_ALLOW_COMMANDS", true); public Limits limits = new Limits(); public Server server = new Server(); public static class Limits { /* Are rate limits checked? */ - public boolean enabled = false; + public boolean enabled = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_LIMITS_ENABLED", false); /* The time for limits to expire. */ - public int interval = 3; + public int interval = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_LIMITS_INTERVAL", 3); /* The maximum amount of normal requests. */ - public int maxRequests = 10; + public int maxRequests = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_LIMITS_MAX_REQUESTS", 10); /* The maximum amount of entities to be spawned in one request. */ - public int maxEntities = 25; + public int maxEntities = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_LIMITS_MAX_ENTITIES", 25); } public static class Server { /* Are the server settings sent to the handbook? */ - public boolean enforced = false; + public boolean enforced = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_CONFIG_SERVER_ENFORCED", false); /* The default server address for the handbook's authentication. */ - public String address = "127.0.0.1"; + public String address = getStringFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_CONFIG_SERVER_ADDRESS", "127.0.0.1"); /* The default server port for the handbook's authentication. */ - public int port = 443; + public int port = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_CONFIG_SERVER_PORT", 443); /* Should the defaults be enforced? */ - public boolean canChange = true; + public boolean canChange = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_CONFIG_SERVER_CAN_CHANGE", true); } } } @@ -359,40 +532,37 @@ public VisionOptions(String name, int visionRange, int gridWidth) { } public static class JoinOptions { - public int[] welcomeEmotes = {2007, 1002, 4010}; - public String welcomeMessage = "Welcome to a Grasscutter server."; + public int[] welcomeEmotes = getIntArrayFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_EMOTES", new int[]{2007, 1002, 4010}, ","); + public String welcomeMessage = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MESSAGE", "Welcome to a Grasscutter server."); public JoinOptions.Mail welcomeMail = new JoinOptions.Mail(); public static class Mail { - public String title = "Welcome to Grasscutter!"; - public String content = """ - Hi there!\r - First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r - \r - Check out our:\r - - """; - public String sender = "Lawnmower"; - public emu.grasscutter.game.mail.Mail.MailItem[] items = { - new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1), - new emu.grasscutter.game.mail.Mail.MailItem(201, 99999, 1) - }; + public String title = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_TITLE", "Welcome to Grasscutter!"); + public String content = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_CONTENT", """ + Hi there!\r + First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r + \r + Check out our:\r + + """); + public String sender = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_SENDER", "Lawnmower"); + public emu.grasscutter.game.mail.Mail.MailItem[] items = getMailItemsFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_ITEMS", new emu.grasscutter.game.mail.Mail.MailItem[]{new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1), new emu.grasscutter.game.mail.Mail.MailItem(201, 99999, 1)}, "|", ","); } } public static class ConsoleAccount { - public int avatarId = 10000007; - public int nameCardId = 210001; - public int adventureRank = 1; - public int worldLevel = 0; + public int avatarId = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_AVATAR_ID", 10000007); + public int nameCardId = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_NAME_CARD_ID", 210001); + public int adventureRank = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_ADVENTURE_RANK", 1); + public int worldLevel = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_WORLD_LEVEL", 0); - public String nickName = "Server"; - public String signature = "Welcome to Grasscutter!"; + public String nickName = getStringFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_NICK_NAME", "Server"); + public String signature = getStringFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_SIGNATURE", "Welcome to Grasscutter!"); } public static class Files { - public String indexFile = "./index.html"; - public String errorFile = "./404.html"; + public String indexFile = getStringFromEnv("SERVER_HTTP_FILES_INDEX_FILE", "./index.html"); + public String errorFile = getStringFromEnv("SERVER_HTTP_FILES_ERROR_FILE", "./404.html"); } /* Objects. */ @@ -404,14 +574,11 @@ public static class Region { public String Ip = "127.0.0.1"; public int Port = 22102; - public Region( - String name, String title, - String address, int port - ) { + public Region(String name, String title, String address, int port) { this.Name = name; this.Title = title; this.Ip = address; - this.Port = port; + this.Port = port; } } } From d5b5e935224b62d8a5a654548dcf465ed4d117d5 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Fri, 15 Dec 2023 23:43:52 +0100 Subject: [PATCH 03/10] Removed config generation from Docker Since the ConfigContainer now uses environment variables by default, there is no need for the config generation script which does the same before launching the Docker container. --- Dockerfile | 5 +- entrypoint.sh | 4 +- generate-config.ts | 228 --------------------------------------------- 3 files changed, 2 insertions(+), 235 deletions(-) delete mode 100644 generate-config.ts diff --git a/Dockerfile b/Dockerfile index 47d0b75654a..16551e17839 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,9 +20,6 @@ RUN apt-get update && apt-get install unzip WORKDIR /app -# Install bun for generating the configuration file -RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.0.0" - # Copy built assets COPY --from=builder /app/grasscutter-1.7.4.jar /app/grasscutter.jar COPY --from=builder /app/keystore.p12 /app/keystore.p12 @@ -31,7 +28,7 @@ COPY --from=builder /app/keystore.p12 /app/keystore.p12 COPY --from=data /app/GC-Resources/Resources /app/resources/ # Copy startup files -COPY ./entrypoint.sh ./generate-config.ts /app/ +COPY ./entrypoint.sh /app/ CMD [ "sh", "/app/entrypoint.sh" ] diff --git a/entrypoint.sh b/entrypoint.sh index 298252a3014..41bfceffad3 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,3 @@ #/bin/sh -$HOME/.bun/bin/bun run /app/generate-config.ts - -java -jar /app/grasscutter.jar \ No newline at end of file +java -jar /app/grasscutter.jar diff --git a/generate-config.ts b/generate-config.ts deleted file mode 100644 index bf034ad6185..00000000000 --- a/generate-config.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { writeFileSync } from "fs"; - -const configToSave = { - folderStructure: { - resources: getStringFromEnv("FOLDER_STRUCTURE_RESOURCES", "./resources/"), - data: getStringFromEnv("FOLDER_STRUCTURE_DATA", "./data/"), - packets: getStringFromEnv("FOLDER_STRUCTURE_PACKETS", "./packets/"), - scripts: getStringFromEnv("FOLDER_STRUCTURE_SCRIPTS", "./resources/Scripts/"), - plugins: getStringFromEnv("FOLDER_STRUCTURE_PLUGINS", "./plugins/"), - }, - databaseInfo: { - server: { - connectionUri: getStringFromEnv("DATABASE_INFO_SERVER_CONNECTION_URI", "mongodb://localhost:27017"), - collection: getStringFromEnv("DATABASE_INFO_SERVER_COLLECTION", "grasscutter"), - }, - game: { - connectionUri: getStringFromEnv("DATABASE_INFO_GAME_CONNECTION_URI", "mongodb://localhost:27017"), - collection: getStringFromEnv("DATABASE_INFO_GAME_COLLECTION", "grasscutter"), - }, - }, - language: { - language: getStringFromEnv("LANGUAGE_LANGUAGE", "en_US"), - fallback: getStringFromEnv("LANGUAGE_FALLBACK", "en_US"), - document: getStringFromEnv("LANGUAGE_DOCUMENT", "EN"), - }, - account: { - autoCreate: getBoolFromEnv("ACCOUNT_AUTO_CREATE", false), - EXPERIMENTAL_RealPassword: getBoolFromEnv("ACCOUNT_EXPERIMENTAL_REAL_PASSWORD", false), - defaultPermissions: getStringArrayFromEnv("ACCOUNT_DEFAULT_PERMISSIONS", []), - maxPlayer: getIntFromEnv("ACCOUNT_MAX_PLAYER", -1), - }, - server: { - debugWhitelist: getStringArrayFromEnv("SERVER_DEBUG_WHITELIST", []), - debugBlacklist: getStringArrayFromEnv("SERVER_DEBUG_BLACKLIST", []), - runMode: getStringFromEnv("SERVER_RUN_MODE", "HYBRID"), - logCommands: getBoolFromEnv("SERVER_LOG_COMMANDS", false), - http: { - bindAddress: getStringFromEnv("SERVER_HTTP_BIND_ADDRESS", "0.0.0.0"), - bindPort: getIntFromEnv("SERVER_HTTP_BIND_PORT", 443), - accessAddress: getStringFromEnv("SERVER_HTTP_ACCESS_ADDRESS", "127.0.0.1"), - accessPort: getIntFromEnv("SERVER_HTTP_ACCESS_PORT", 0), - encryption: { - useEncryption: getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_ENCRYPTION", true), - useInRouting: getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_IN_ROUTING", true), - keystore: getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE", "./keystore.p12"), - keystorePassword: getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE_PASSWORD", "123456"), - }, - policies: { - cors: { - enabled: getBoolFromEnv("SERVER_HTTP_POLICIES_CORS_ENABLED", false), - allowedOrigins: getStringArrayFromEnv("SERVER_HTTP_POLICIES_CORS_ALLOWED_ORIGINS", ["*"]), - }, - }, - files: { - indexFile: getStringFromEnv("SERVER_HTTP_FILES_INDEX_FILE", "./index.html"), - errorFile: getStringFromEnv("SERVER_HTTP_FILES_ERROR_FILE", "./404.html"), - }, - }, - game: { - bindAddress: getStringFromEnv("SERVER_GAME_BIND_ADDRESS", "0.0.0.0"), - bindPort: getIntFromEnv("SERVER_GAME_BIND_PORT", 22102), - accessAddress: getStringFromEnv("SERVER_GAME_ACCESS_ADDRESS", "127.0.0.1"), - accessPort: getIntFromEnv("SERVER_GAME_ACCESS_PORT", 0), - loadEntitiesForPlayerRange: getIntFromEnv("SERVER_GAME_LOAD_ENTITIES_FOR_PLAYER_RANGE", 100), - enableScriptInBigWorld: getBoolFromEnv("SERVER_GAME_ENABLE_SCRIPT_IN_BIG_WORLD", false), - enableConsole: getBoolFromEnv("SERVER_GAME_ENABLE_CONSOLE", true), - kcpInterval: getIntFromEnv("SERVER_GAME_KCP_INTERVAL", 20), - logPackets: getStringFromEnv("SERVER_GAME_LOG_PACKETS", "NONE"), - gameOptions: { - inventoryLimits: { - weapons: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_WEAPONS", 2000), - relics: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_RELICS", 2000), - materials: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_MATERIALS", 2000), - furniture: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_FURNITURE", 2000), - all: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_ALL", 30000), - }, - avatarLimits: { - singlePlayerTeam: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_SINGLE_PLAYER_TEAM", 4), - multiplayerTeam: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_MULTIPLAYER_TEAM", 4), - }, - sceneEntityLimit: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_SCENE_ENTITY_LIMIT", 1000), - watchGachaConfig: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_WATCH_GACHA_CONFIG", false), - enableShopItems: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENABLE_SHOP_ITEMS", true), - staminaUsage: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_STAMINA_USAGE", true), - energyUsage: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENERGY_USAGE", true), - fishhookTeleport: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_FISHHOOK_TELEPORT", true), - resinOptions: { - resinUsage: getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RESIN_USAGE", false), - cap: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_CAP", 160), - rechargeTime: getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RECHARGE_TIME", 480), - }, - rates: { - adventureExp: getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_ADVENTURE_EXP", 1.0), - mora: getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_MORA", 1.0), - leyLines: getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_LEY_LINES", 1.0), - }, - }, - joinOptions: { - welcomeEmotes: [2007, 1002, 4010], - welcomeMessage: getStringFromEnv( - "SERVER_GAME_JOIN_OPTIONS_WELCOME_MESSAGE", - "Welcome to a Grasscutter server." - ), - welcomeMail: { - title: getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_TITLE", "Welcome to Grasscutter!"), - content: getStringFromEnv( - "SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_CONTENT", - 'Hi there!\r\nFirst of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r\n\r\nCheck out our:\r\n\u003ctype\u003d"browser" text\u003d"Discord" href\u003d"https://discord.gg/T5vZU6UyeG"/\u003e\n' - ), - sender: getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_SENDER", "Lawnmower"), - items: getItemsFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_ITEMS", [ - { - itemId: 13509, - itemCount: 1, - itemLevel: 1, - }, - { - itemId: 201, - itemCount: 99999, - itemLevel: 1, - }, - ]), - }, - }, - serverAccount: { - avatarId: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_AVATAR_ID", 10000007), - nameCardId: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_NAME_CARD_ID", 210001), - adventureRank: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_ADVENTURE_RANK", 1), - worldLevel: getIntFromEnv("SERVER_GAME_SERVER_ACCOUNT_WORLD_LEVEL", 0), - nickName: getStringFromEnv("SERVER_GAME_SERVER_ACCOUNT_NICK_NAME", "Server"), - signature: getStringFromEnv("SERVER_GAME_SERVER_ACCOUNT_SIGNATURE", "Welcome to Grasscutter!"), - }, - }, - dispatch: { - regions: getStringArrayFromEnv("SERVER_DISPATCH_REGIONS", []), - defaultName: getStringFromEnv("SERVER_DISPATCH_DEFAULT_NAME", "Grasscutter"), - logRequests: getStringFromEnv("SERVER_DISPATCH_LOG_REQUESTS", "NONE"), - }, - }, - version: 4, -}; - -writeFileSync("./config.json", JSON.stringify(configToSave, null, 4)); - -function getStringFromEnv(key: string, defaultValue: string): string { - return process.env[key] || defaultValue; -} - -function getBoolFromEnv(key: string, defaultValue: boolean): boolean { - switch (process.env[key]) { - case "true": - case "on": - case "1": - return true; - - case "false": - case "off": - case "0": - return false; - - default: - return defaultValue; - } -} - -function getIntFromEnv(key: string, defaultValue: number): number { - const currentValue = process.env[key]; - - if (currentValue === undefined || currentValue === null) { - return defaultValue; - } - - try { - return parseInt(currentValue, 10); - } catch (error) { - return defaultValue; - } -} - -function getFloatFromEnv(key: string, defaultValue: number): number { - const currentValue = process.env[key]; - - if (currentValue === undefined || currentValue === null) { - return defaultValue; - } - - try { - return parseFloat(currentValue); - } catch (error) { - return defaultValue; - } -} - -function getStringArrayFromEnv(key: string, defaultValue: string[], separator: string = ","): string[] { - const currentValue = process.env[key]; - - if (currentValue === undefined || currentValue === null) { - return defaultValue; - } - - return currentValue.split(separator); -} - -type ItemInfo = { - itemId: number; - itemCount: number; - itemLevel: number; -}; - -function getItemsFromEnv(key: string, defaultValue: ItemInfo[]): ItemInfo[] { - const currentValue = process.env[key]; - - if (currentValue === undefined || currentValue === null) { - return defaultValue; - } - - const parts = currentValue.split("|"); - - return parts.map((part: string) => { - const [rawItemId, rawItemCount, rawItemLevel] = part.split(","); - - return { - itemId: parseInt(rawItemId, 10), - itemCount: parseInt(rawItemCount, 10), - itemLevel: parseInt(rawItemLevel, 10), - }; - }); -} From 72f0c15108dff32eb335149c9485d6d593c0e8c6 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Fri, 15 Dec 2023 23:57:08 +0100 Subject: [PATCH 04/10] Use a Docker ARG for the data repository --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 16551e17839..748713d92f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,12 @@ RUN gradle jar --no-daemon FROM bitnami/git:2.43.0-debian-11-r1 as data +ARG DATA_REPOSITORY=https://gitlab.com/YuukiPS/GC-Resources.git ARG DATA_BRANCH=4.0 WORKDIR /app -RUN git clone --branch ${DATA_BRANCH} --depth 1 https://gitlab.com/YuukiPS/GC-Resources.git +RUN git clone --branch ${DATA_BRANCH} --depth 1 ${DATA_REPOSITORY} FROM bitnami/java:21.0.1-12 From 7d79fed0763612bcc8bfbe990e70f56ef49e5355 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Sat, 16 Dec 2023 00:35:08 +0100 Subject: [PATCH 05/10] Fixed ConfigContainer database connection Before the parameters for connecting to the MongoDB weren't read from the environment variables. --- .../emu/grasscutter/config/ConfigContainer.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index e087ffe4579..419e2435ffd 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -263,12 +263,17 @@ static List getRegionsFromEnv(String key, List defaultValue, Str /* Option containers. */ public static class Database { - public DataStore server = new DataStore(); - public DataStore game = new DataStore(); + public DataStore server = new DataStore("SERVER"); + public DataStore game = new DataStore("GAME"); public static class DataStore { - public String connectionUri = "mongodb://localhost:27017"; - public String collection = "grasscutter"; + public String connectionUri; + public String collection; + + public DataStore(String key) { + this.connectionUri = getStringFromEnv("DATABASE_INFO_" + key + "_CONNECTION_URI", "mongodb://localhost:27017"); + this.collection = getStringFromEnv("DABASE_INFO_" + key + "_COLLECTION", "grasscutter"); + } } } From 5c385476cbf07b2026c309aea61420639f376ea0 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Sat, 16 Dec 2023 00:36:23 +0100 Subject: [PATCH 06/10] Use Java 17 for running GC in Docker --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 748713d92f2..29217e86391 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ WORKDIR /app RUN git clone --branch ${DATA_BRANCH} --depth 1 ${DATA_REPOSITORY} -FROM bitnami/java:21.0.1-12 +FROM bitnami/java:17.0.9-11 RUN apt-get update && apt-get install unzip From 3a49892fa3fdac2aac5a4d7be8cbad97e55ce8e5 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Sat, 16 Dec 2023 00:37:27 +0100 Subject: [PATCH 07/10] Fixed typo in ConfigContainer for an environment variable --- src/main/java/emu/grasscutter/config/ConfigContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index 419e2435ffd..28dfbdc3e91 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -272,7 +272,7 @@ public static class DataStore { public DataStore(String key) { this.connectionUri = getStringFromEnv("DATABASE_INFO_" + key + "_CONNECTION_URI", "mongodb://localhost:27017"); - this.collection = getStringFromEnv("DABASE_INFO_" + key + "_COLLECTION", "grasscutter"); + this.collection = getStringFromEnv("DATABASE_INFO_" + key + "_COLLECTION", "grasscutter"); } } } From c9b1c55982f3dc100f45b1ce6cafb6b7f37b4c25 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Sat, 16 Dec 2023 01:30:21 +0100 Subject: [PATCH 08/10] added more documentation for the ConfigContainer helper methods --- .../grasscutter/config/ConfigContainer.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index 28dfbdc3e91..ac418ad200d 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -191,6 +191,24 @@ static String[] getStringArrayFromEnv(String key, String[] defaultValue, String return currentValue.split(separator); } + /** + * Retrieves the given key from the environment variables and tries to parse it as an int array. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + *

+ * 1,2 + *

+ * | |- second entry + *

+ * |- first entry + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist + * @param separator The separator which will be used for splitting up the string + * @return The parsed int array or the default value + */ static int[] getIntArrayFromEnv(String key, int[] defaultValue, String separator) { var currentValue = System.getenv(key); @@ -201,6 +219,27 @@ static int[] getIntArrayFromEnv(String key, int[] defaultValue, String separator return Arrays.stream(currentValue.split(separator)).mapToInt(Integer::parseInt).toArray(); } + /** + * Retrieves the given key from the environment variables and tries to parse it as a MailItem array. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + *

+ * 1,1,1|2,1,1 + *

+ * | | |- Item level + *

+ * | |- Item count + *

+ * |- Item ID + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist + * @param partsSeparator The separator which will be used for splitting up the string into parts + * @param valuesSeparator The separator which will be used for splitting up the parts into values + * @return The parsed MailItem array or the default value + */ static emu.grasscutter.game.mail.Mail.MailItem[] getMailItemsFromEnv(String key, emu.grasscutter.game.mail.Mail.MailItem[] defaultValue, String partsSeparator, String valuesSeparator) { var currentValue = System.getenv(key); @@ -219,6 +258,26 @@ static emu.grasscutter.game.mail.Mail.MailItem[] getMailItemsFromEnv(String key, }).toArray(); } + /** + * Retrieves the given key from the environment variables and tries to parse it as a MailItem array. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + * VISION_LEVEL_NORMAL,80,20|VISION_LEVEL_LITTLE_REMOTE,16,40 + *

+ * | | |- Grid width + *

+ * | |- Vision range + *

+ * |- Name + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist + * @param partsSeparator The separator which will be used for splitting up the string into parts + * @param valuesSeparator The separator which will be used for splitting up the parts into values + * @return The parsed VisionOptions or the default value + */ static VisionOptions[] getVisionOptionsFromEnv(String key, VisionOptions[] defaultValue, String partsSeparator, String valuesSeparator) { var currentValue = System.getenv(key); @@ -236,6 +295,29 @@ static VisionOptions[] getVisionOptionsFromEnv(String key, VisionOptions[] defau }).toArray(); } + /** + * Retrieves the given key from the environment variables and tries to parse it as a Region array. + *

+ * If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + *

+ * my region,my title,127.0.0.1,1337|my second region, my second title,127.0.0.1,42 + *

+ * | | | |- port + *

+ * | | |- address + *

+ * | |- title + *

+ * |- name + * + * @param key The name of the environment variable to parse + * @param defaultValue The default value when the environment variable does not exist + * @param partsSeparator The separator which will be used for splitting up the string into parts + * @param valuesSeparator The separator which will be used for splitting up the parts into values + * @return The parsed Region list or the default value + */ static List getRegionsFromEnv(String key, List defaultValue, String partsSeparator, String valuesSeparator) { var currentValue = System.getenv(key); From 25d858890a77e61594a37fcbc8cdef258fc20b90 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Sat, 16 Dec 2023 01:33:23 +0100 Subject: [PATCH 09/10] added structure samples to the ConfigContainer helper methods --- .../grasscutter/config/ConfigContainer.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index ac418ad200d..9af0f877929 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -111,6 +111,14 @@ static boolean getBoolFromEnv(String key, boolean defaultValue) { * Retrieves the given from the environment variables and tries to parse it as a Set. *

* If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + *

+ * hello,world + *

+ * | |- second entry + *

+ * |- first entry * * @param key The name of the environment variable to parse * @param defaultValue The default value when the environment variable does not exist or is not a valid set @@ -130,9 +138,17 @@ static Set getStringSetFromEnv(String key, Set defaultValue, Str } /** - * Retrieves the given key from the environment variables and tries to parse it as a string array. + * Retrieves the given key from the environment variables and tries to parse it as a Set. *

* If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + *

+ * 42,1337 + *

+ * | |- second entry + *

+ * |- first entry * * @param key The name of the environment variable * @param defaultValue The default value when the environment variable does not exist @@ -175,6 +191,14 @@ static > T getEnumFromEnv(String key, Class enumClass, T de * Retrieves the given key from the environment variables and tries to parse it as string array. *

* If the environment variable is not present or the parsing fails then the default value will be returned. + *

+ * It expects the following structure: + *

+ * hello,world + *

+ * | |- second entry + *

+ * |- first entry * * @param key The name of the environment variable to parse * @param defaultValue The default value when the environment variable does not exist From d8cf0acf41aabceb8f6917fdd5fe03c7ce5335e8 Mon Sep 17 00:00:00 2001 From: Yannick Fricke Date: Sat, 16 Dec 2023 01:50:32 +0100 Subject: [PATCH 10/10] Fixed a typo in ConfigContainer varient -> variant --- src/main/java/emu/grasscutter/config/ConfigContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index 9af0f877929..e145360a012 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -403,7 +403,7 @@ public static class Server { public boolean logCommands = getBoolFromEnv("SERVER_LOG_COMMANDS", false); /** - * If enabled, the 'require' Lua function will load the script's compiled varient into the context. (faster; doesn't work as well) + * If enabled, the 'require' Lua function will load the script's compiled variant into the context. (faster; doesn't work as well) * If disabled, all 'require' calls will be replaced with the referenced script's source. (slower; works better) */ public boolean fastRequire = getBoolFromEnv("SERVER_FAST_REQUIRE", true);