3 Commits

Author SHA1 Message Date
72f0c15108 Use a Docker ARG for the data repository 2023-12-15 23:57:08 +01:00
d5b5e93522 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.
2023-12-15 23:43:52 +01:00
60e713f4ff Refactor ConfigContainer to use environment variables
BREAKING CHANGE:
This will make the config.json obsolete!
2023-12-15 23:41:53 +01:00
5 changed files with 352 additions and 434 deletions

View File

@ -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
@ -20,9 +21,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 +29,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" ]

View File

@ -1,5 +1,3 @@
#/bin/sh
$HOME/.bun/bin/bun run /app/generate-config.ts
java -jar /app/grasscutter.jar
java -jar /app/grasscutter.jar

View File

@ -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),
};
});
}

View File

@ -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 final class Grasscutter {
/** 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();
}
/**

View File

@ -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.
* <p>
* 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.
* <p>
* 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) { }
static int getIntFromEnv(String key, int defaultValue) {
var currentValue = System.getenv(key);
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();
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;
}
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.
* <p>
* 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.
* <p>
* 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<String>.
* <p>
* 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<String> getStringSetFromEnv(String key, Set<String> defaultValue, String separator) {
var currentValue = System.getenv(key);
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.
* <p>
* 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<Integer> getIntSetFromEnv(String key, Set<Integer> 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.
* <p>
* 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 <T> The type of the enum member
* @return The parsed enum member or the default value
*/
static <T extends Enum<T>> T getEnumFromEnv(String key, Class<T> 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.
* <p>
* 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<Region> getRegionsFromEnv(String key, List<Region> 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 class ConfigContainer {
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 class ConfigContainer {
}
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<Integer> debugWhitelist = Set.of();
public Set<Integer> debugBlacklist = Set.of();
public ServerRunMode runMode = ServerRunMode.HYBRID;
public boolean logCommands = false;
public Set<Integer> debugWhitelist = getIntSetFromEnv("SERVER_DEBUG_WHITELIST", Set.of(), ",");
public Set<Integer> 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 class ConfigContainer {
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 class ConfigContainer {
}
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<Region> regions = List.of();
public List<Region> 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 class ConfigContainer {
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 class ConfigContainer {
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 class ConfigContainer {
}
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
<type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/>
""";
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
<type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/>
""");
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 class ConfigContainer {
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;
}
}
}