6 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
e1e0bb6928 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
2023-12-14 23:53:37 +01:00
770a793c69 Format code [skip actions] 2023-12-14 05:36:30 +00:00
c4402cc287 Fix some more dungeons (#2449)
* Monds weapon mats domain: Fix time between kill not refreshing
* Inaz husk domain: Fix broken domain challenge
    * `EVENT_ANY_MONSTER_LIVE` is likely sent on tick, not on create. See scene40801_group240801001.lua:
        1. `condition_EVENT_ANY_MONSTER_LIVE_1023` checks for mob 1008 to spawn AND for variable `challenge` to be 1
        2. Mob 1008 spawns during `action_EVENT_SELECT_OPTION_1003`, at `ScriptLib.AddExtraGroupSuite(context, 240801001, 2)`
        3. This spawn triggers `EVENT_ANY_MONSTER_LIVE` for mob 1008 but still fails the condition because `challenge` is still 0.
        4. `challenge` is set to 1 at the end of `action_EVENT_SELECT_OPTION_1003`. By now, `EVENT_ANY_MONSTER_LIVE` for mob 1008 no longer fires, causing the domain challenge to fail to start.
2023-12-14 00:34:50 -05:00
10 changed files with 454 additions and 211 deletions

36
Dockerfile Normal file
View File

@ -0,0 +1,36 @@
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_REPOSITORY=https://gitlab.com/YuukiPS/GC-Resources.git
ARG DATA_BRANCH=4.0
WORKDIR /app
RUN git clone --branch ${DATA_BRANCH} --depth 1 ${DATA_REPOSITORY}
FROM bitnami/java:21.0.1-12
RUN apt-get update && apt-get install unzip
WORKDIR /app
# 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 /app/
CMD [ "sh", "/app/entrypoint.sh" ]
EXPOSE 80 443 8888 22102

30
docker-compose.yml Normal file
View File

@ -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:

3
entrypoint.sh Executable file
View File

@ -0,0 +1,3 @@
#/bin/sh
java -jar /app/grasscutter.jar

View File

@ -77,8 +77,6 @@ public final class Grasscutter {
// Load server configuration. // Load server configuration.
Grasscutter.loadConfig(); Grasscutter.loadConfig();
// Attempt to update configuration.
ConfigContainer.updateConfig();
Grasscutter.getLogger().info("Loading Grasscutter..."); Grasscutter.getLogger().info("Loading Grasscutter...");
@ -238,22 +236,7 @@ public final class Grasscutter {
/** Attempts to load the configuration from a file. */ /** Attempts to load the configuration from a file. */
public static void loadConfig() { public static void loadConfig() {
// Check if config.json exists. If not, we generate a new config. // 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(); 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);
}
} }
/** /**

View File

@ -1,81 +1,257 @@
package emu.grasscutter.config; package emu.grasscutter.config;
import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Level;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import emu.grasscutter.Grasscutter; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.Utils;
import lombok.NoArgsConstructor; 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* * *when your JVM fails*
*/ */
public class ConfigContainer { public class ConfigContainer {
/* /**
* Configuration changes: * Retrieves the given key from the environment variables.
* Version 5 - 'questing' has been changed from a boolean * <p>
* to a container of options ('questOptions'). * When the key is not set it will return the given default value.
* This field will be removed in future versions. *
* Version 6 - 'questing' has been fully replaced with 'questOptions'. * @param key The name of the environment variable
* The field for 'legacyResources' has been removed. * @param defaultValue The default value when the key is not set
* Version 7 - 'regionKey' is being added for authentication * @return The value from the environment variable or the default value
* 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.
*/ */
private static int version() { static String getStringFromEnv(String key, String defaultValue) {
return 13; 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() { static int getIntFromEnv(String key, int defaultValue) {
try { // Check if the server is using a legacy config. var currentValue = System.getenv(key);
var configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class);
if (!configObject.has("version")) { if (currentValue == null) {
Grasscutter.getLogger().info("Updating legacy config..."); return defaultValue;
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 { try {
field.set(updated, field.get(config)); return Integer.parseInt(currentValue, 10);
} catch (Exception exception) { } catch (Exception e) {
Grasscutter.getLogger().error("Failed to update a configuration field.", exception); return defaultValue;
}
} }
}); updated.version = version();
try { // Save configuration and reload. /**
Grasscutter.saveConfig(updated); * Retrieves the given key from the environment variables and tries to parse it as float.
Grasscutter.loadConfig(); * <p>
} catch (Exception exception) { * If the environment variable is not present or the parsing fails then the default value will be returned.
Grasscutter.getLogger().warn("Failed to save the updated configuration.", exception); *
* @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(); public Structure folderStructure = new Structure();
@ -84,9 +260,6 @@ public class ConfigContainer {
public Account account = new Account(); public Account account = new Account();
public Server server = new Server(); public Server server = new Server();
// DO NOT. TOUCH. THE VERSION NUMBER.
public int version = version();
/* Option containers. */ /* Option containers. */
public static class Database { public static class Database {
@ -100,28 +273,29 @@ public class ConfigContainer {
} }
public static class Structure { public static class Structure {
public String resources = "./resources/"; public String resources = getStringFromEnv("FOLDER_STRUCTURE_RESOURCES", "./resources/");
public String data = "./data/"; public String data = getStringFromEnv("FOLDER_STRUCTURE_DATA", "./data/");
public String packets = "./packets/"; public String packets = getStringFromEnv("FOLDER_STRUCTURE_PACKETS", "./packets/");
public String scripts = "resources:Scripts/"; public String scripts = getStringFromEnv("FOLDER_STRUCTURE_SCRIPTS", "resources:Scripts/");
public String plugins = "./plugins/"; public String plugins = getStringFromEnv("FOLDER_STRUCTURE_PLUGINS", "./plugins/");
public String cache = "./cache/"; public String cache = getStringFromEnv("FOLDER_STRUCTURE_CACHE", "./cache/");
// UNUSED (potentially added later?) // UNUSED (potentially added later?)
// public String dumps = "./dumps/"; // public String dumps = "./dumps/";
} }
public static class Server { public static class Server {
public Set<Integer> debugWhitelist = Set.of(); public Set<Integer> debugWhitelist = getIntSetFromEnv("SERVER_DEBUG_WHITELIST", Set.of(), ",");
public Set<Integer> debugBlacklist = Set.of(); public Set<Integer> debugBlacklist = getIntSetFromEnv("SERVER_DEBUG_BLACKLIST", Set.of(), ",");
public ServerRunMode runMode = ServerRunMode.HYBRID; public ServerRunMode runMode = getEnumFromEnv("SERVER_RUN_MODE", ServerRunMode.class, ServerRunMode.HYBRID);
public boolean logCommands = false;
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 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) * 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 HTTP http = new HTTP();
public Game game = new Game(); public Game game = new Game();
@ -133,29 +307,29 @@ public class ConfigContainer {
public static class Language { public static class Language {
public Locale language = Locale.getDefault(); public Locale language = Locale.getDefault();
public Locale fallback = Locale.US; public Locale fallback = Locale.US;
public String document = "EN"; public String document = getStringFromEnv("LANGUAGE_DOCUMENT", "EN");
} }
public static class Account { public static class Account {
public boolean autoCreate = false; public boolean autoCreate = getBoolFromEnv("ACCOUNT_AUTO_CREATE", false);
public boolean EXPERIMENTAL_RealPassword = false; public boolean EXPERIMENTAL_RealPassword = getBoolFromEnv("ACCOUNT_EXPERIMENTAL_REAL_PASSWORD", false);
public String[] defaultPermissions = {}; public String[] defaultPermissions = getStringArrayFromEnv("ACCOUNT_DEFAULT_PERMISSIONS", new String[]{}, ",");
public int maxPlayer = -1; public int maxPlayer = getIntFromEnv("ACCOUNT_MAX_PLAYER", -1);
} }
/* Server options. */ /* Server options. */
public static class HTTP { public static class HTTP {
/* This starts the HTTP server before the game server. */ /* 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 String bindAddress = getStringFromEnv("SERVER_HTTP_BIND_ADDRESS", "0.0.0.0");
public int bindPort = 443; public int bindPort = getIntFromEnv("SERVER_HTTP_BIND_PORT", 443);
/* This is the address used in URLs. */ /* 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. */ /* 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 Encryption encryption = new Encryption();
public Policies policies = new Policies(); public Policies policies = new Policies();
@ -163,66 +337,65 @@ public class ConfigContainer {
} }
public static class Game { public static class Game {
public String bindAddress = "0.0.0.0"; public String bindAddress = getStringFromEnv("SERVER_GAME_BIND_ADDRESS", "0.0.0.0");
public int bindPort = 22102; public int bindPort = getIntFromEnv("SERVER_GAME_BIND_PORT", 22102);
/* This is the address used in the default region. */ /* 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. */ /* 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. */ /* 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 */ /* 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. */ /* Start in 'unstable-quests', Lua scripts will be enabled by default. */
public boolean enableScriptInBigWorld = true; public boolean enableScriptInBigWorld = getBoolFromEnv("SERVER_GAME_ENABLE_SCRIPT_IN_BIG_WORLD", true);
public boolean enableConsole = true; public boolean enableConsole = getBoolFromEnv("SERVER_GAME_ENABLE_CONSOLE", true);
/* Kcp internal work interval (milliseconds) */ /* 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 */ /* 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) */ /* 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 */ /* 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 GameOptions gameOptions = new GameOptions();
public JoinOptions joinOptions = new JoinOptions(); public JoinOptions joinOptions = new JoinOptions();
public ConsoleAccount serverAccount = new ConsoleAccount(); public ConsoleAccount serverAccount = new ConsoleAccount();
public VisionOptions[] visionOptions = new VisionOptions[] { public VisionOptions[] visionOptions = getVisionOptionsFromEnv("SERVER_GAME_VISION_OPTIONS", new VisionOptions[]{
new VisionOptions("VISION_LEVEL_NORMAL" , 80 , 20), new VisionOptions("VISION_LEVEL_NORMAL", 80, 20),
new VisionOptions("VISION_LEVEL_LITTLE_REMOTE" , 16 , 40), new VisionOptions("VISION_LEVEL_LITTLE_REMOTE", 16, 40),
new VisionOptions("VISION_LEVEL_REMOTE" , 1000 , 250), new VisionOptions("VISION_LEVEL_REMOTE", 1000, 250),
new VisionOptions("VISION_LEVEL_SUPER" , 4000 , 1000), new VisionOptions("VISION_LEVEL_SUPER", 4000, 1000),
new VisionOptions("VISION_LEVEL_NEARBY" , 40 , 20), new VisionOptions("VISION_LEVEL_NEARBY", 40, 20),
new VisionOptions("VISION_LEVEL_SUPER_NEARBY" , 20 , 20) new VisionOptions("VISION_LEVEL_SUPER_NEARBY", 20, 20)
}; }, "|", ",");
} }
/* Data containers. */ /* Data containers. */
public static class Dispatch { public static class Dispatch {
/* An array of servers. */ /* 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. */ /* 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. */ /* 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. */ /* A unique key used for authentication. */
public String dispatchKey = Utils.base64Encode( public String dispatchKey = getStringFromEnv("SERVER_DISPATCH_DISPATCH_KEY", Utils.base64Encode(Crypto.createSessionKey(32)));
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 */ /* 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 /* 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; public Level servicesLoggersLevel = Level.INFO;
/* Controls whether packets should be logged in console or not */ /* 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) */ /* 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 */ /* 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 */ /* 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 static class Encryption {
public boolean useEncryption = true; public boolean useEncryption = getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_ENCRYPTION", true);
/* Should 'https' be appended to URLs? */ /* Should 'https' be appended to URLs? */
public boolean useInRouting = true; public boolean useInRouting = getBoolFromEnv("SERVER_HTTP_ENCRYPTION_USE_IN_ROUTING", true);
public String keystore = "./keystore.p12"; public String keystore = getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE", "./keystore.p12");
public String keystorePassword = "123456"; public String keystorePassword = getStringFromEnv("SERVER_HTTP_ENCRYPTION_KEYSTORE_PASSWORD", "123456");
} }
public static class Policies { public static class Policies {
public Policies.CORS cors = new Policies.CORS(); public Policies.CORS cors = new Policies.CORS();
public static class CORS { public static class CORS {
public boolean enabled = true; public boolean enabled = getBoolFromEnv("SERVER_HTTP_POLICIES_CORS_ENABLED", true);
public String[] allowedOrigins = new String[]{"*"}; public String[] allowedOrigins = getStringArrayFromEnv("SERVER_HTTP_POLICIES_ALLOWED_ORIGINS", new String[]{"*"}, ",");
} }
} }
public static class GameOptions { public static class GameOptions {
public InventoryLimits inventoryLimits = new InventoryLimits(); public InventoryLimits inventoryLimits = new InventoryLimits();
public AvatarLimits avatarLimits = new AvatarLimits(); 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 watchGachaConfig = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_WATCH_GACHA_CONFIG", false);
public boolean enableShopItems = true; public boolean enableShopItems = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENABLE_SHOP_ITEMS", true);
public boolean staminaUsage = true; public boolean staminaUsage = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_STAMINA_USAGE", true);
public boolean energyUsage = true; public boolean energyUsage = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_ENERGY_USAGE", true);
public boolean fishhookTeleport = true; public boolean fishhookTeleport = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_FISHHOOK_TELEPORT", true);
public boolean trialCostumes = false; public boolean trialCostumes = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_TRIAL_COSTUMES", false);
@SerializedName(value = "questing", alternate = "questOptions") @SerializedName(value = "questing", alternate = "questOptions")
public Questing questing = new Questing(); public Questing questing = new Questing();
@ -285,63 +458,63 @@ public class ConfigContainer {
public HandbookOptions handbook = new HandbookOptions(); public HandbookOptions handbook = new HandbookOptions();
public static class InventoryLimits { public static class InventoryLimits {
public int weapons = 2000; public int weapons = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_WEAPONS", 2000);
public int relics = 2000; public int relics = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_RELICS", 2000);
public int materials = 2000; public int materials = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_MATERIALS", 2000);
public int furniture = 2000; public int furniture = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_FURNITURE", 2000);
public int all = 30000; public int all = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_INVENTORY_LIMITS_ALL", 30000);
} }
public static class AvatarLimits { public static class AvatarLimits {
public int singlePlayerTeam = 4; public int singlePlayerTeam = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_SINGLE_PLAYER_TEAM", 4);
public int multiplayerTeam = 4; public int multiplayerTeam = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_AVATAR_LIMITS_MULTIPLAYER_TEAM", 4);
} }
public static class Rates { public static class Rates {
public float adventureExp = 1.0f; public float adventureExp = getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_ADVENTURE_EXP", 1.0f);
public float mora = 1.0f; public float mora = getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_MORA", 1.0f);
public float leyLines = 1.0f; public float leyLines = getFloatFromEnv("SERVER_GAME_GAME_OPTIONS_RATES_LEY_LINES", 1.0f);
} }
public static class ResinOptions { public static class ResinOptions {
public boolean resinUsage = false; public boolean resinUsage = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RESIN_USAGE", false);
public int cap = 160; public int cap = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_CAP", 160);
public int rechargeTime = 480; public int rechargeTime = getIntFromEnv("SERVER_GAME_GAME_OPTIONS_RESIN_OPTIONS_RECHARGE_TIME", 480);
} }
public static class Questing { public static class Questing {
/* Should questing behavior be used? */ /* 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 static class HandbookOptions {
public boolean enable = false; public boolean enable = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_ENABLE", false);
public boolean allowCommands = true; public boolean allowCommands = getBoolFromEnv("SERVER_GAME_GAME_OPTIONS_HANDBOOK_OPTIONS_ALLOW_COMMANDS", true);
public Limits limits = new Limits(); public Limits limits = new Limits();
public Server server = new Server(); public Server server = new Server();
public static class Limits { public static class Limits {
/* Are rate limits checked? */ /* 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. */ /* 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. */ /* 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. */ /* 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 { public static class Server {
/* Are the server settings sent to the handbook? */ /* 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. */ /* 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. */ /* 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? */ /* 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 static class JoinOptions {
public int[] welcomeEmotes = {2007, 1002, 4010}; public int[] welcomeEmotes = getIntArrayFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_EMOTES", new int[]{2007, 1002, 4010}, ",");
public String welcomeMessage = "Welcome to a Grasscutter server."; public String welcomeMessage = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MESSAGE", "Welcome to a Grasscutter server.");
public JoinOptions.Mail welcomeMail = new JoinOptions.Mail(); public JoinOptions.Mail welcomeMail = new JoinOptions.Mail();
public static class Mail { public static class Mail {
public String title = "Welcome to Grasscutter!"; public String title = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_TITLE", "Welcome to Grasscutter!");
public String content = """ public String content = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_CONTENT", """
Hi there!\r 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 First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r
\r \r
Check out our:\r Check out our:\r
<type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/> <type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/>
"""; """);
public String sender = "Lawnmower"; public String sender = getStringFromEnv("SERVER_GAME_JOIN_OPTIONS_WELCOME_MAIL_SENDER", "Lawnmower");
public emu.grasscutter.game.mail.Mail.MailItem[] items = { 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)}, "|", ",");
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 static class ConsoleAccount {
public int avatarId = 10000007; public int avatarId = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_AVATAR_ID", 10000007);
public int nameCardId = 210001; public int nameCardId = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_NAME_CARD_ID", 210001);
public int adventureRank = 1; public int adventureRank = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_ADVENTURE_RANK", 1);
public int worldLevel = 0; public int worldLevel = getIntFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_WORLD_LEVEL", 0);
public String nickName = "Server"; public String nickName = getStringFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_NICK_NAME", "Server");
public String signature = "Welcome to Grasscutter!"; public String signature = getStringFromEnv("SERVER_GAME_CONSOLE_ACCOUNT_SIGNATURE", "Welcome to Grasscutter!");
} }
public static class Files { public static class Files {
public String indexFile = "./index.html"; public String indexFile = getStringFromEnv("SERVER_HTTP_FILES_INDEX_FILE", "./index.html");
public String errorFile = "./404.html"; public String errorFile = getStringFromEnv("SERVER_HTTP_FILES_ERROR_FILE", "./404.html");
} }
/* Objects. */ /* Objects. */
@ -404,10 +574,7 @@ public class ConfigContainer {
public String Ip = "127.0.0.1"; public String Ip = "127.0.0.1";
public int Port = 22102; public int Port = 22102;
public Region( public Region(String name, String title, String address, int port) {
String name, String title,
String address, int port
) {
this.Name = name; this.Name = name;
this.Title = title; this.Title = title;
this.Ip = address; this.Ip = address;

View File

@ -29,7 +29,7 @@ public class WorldChallenge {
private final AtomicInteger score; private final AtomicInteger score;
private boolean progress; private boolean progress;
private boolean success; private boolean success;
private long startedAt; private int startedAt;
private int finishedTime; private int finishedTime;
/** /**

View File

@ -36,6 +36,6 @@ public class KillMonsterCountInTimeIncChallengeFactoryHandler implements Challen
List.of( List.of(
new KillMonsterCountTrigger(), new KillMonsterCountTrigger(),
new InTimeTrigger(), new InTimeTrigger(),
new KillMonsterTimeIncTrigger(timeInc))); new KillMonsterTimeIncTrigger(timeLimit, timeInc)));
} }
} }

View File

@ -1,11 +1,12 @@
package emu.grasscutter.game.dungeons.challenge.factory; package emu.grasscutter.game.dungeons.challenge.factory;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
import emu.grasscutter.game.dungeons.challenge.trigger.*; import emu.grasscutter.game.dungeons.challenge.trigger.*;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.data.SceneGroup; import emu.grasscutter.scripts.data.SceneGroup;
import java.util.List; import java.util.*;
import lombok.val; import lombok.val;
public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler { public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler {
@ -28,6 +29,16 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
Scene scene, Scene scene,
SceneGroup group) { SceneGroup group) {
val realGroup = scene.getScriptManager().getGroupById(groupId); val realGroup = scene.getScriptManager().getGroupById(groupId);
val challengeTriggers = new ArrayList<ChallengeTrigger>();
challengeTriggers.addAll(List.of(new KillMonsterCountTrigger(), new InTimeTrigger()));
val challengeData = GameData.getDungeonChallengeConfigDataMap().get(challengeId);
val challengeType = challengeData.getChallengeType();
if (challengeType == ChallengeType.CHALLENGE_KILL_COUNT_FAST) {
challengeTriggers.add(
new KillMonsterTimeIncTrigger(timeLimit, 0 /* refresh to original limit on kill */));
}
return new WorldChallenge( return new WorldChallenge(
scene, scene,
realGroup, realGroup,
@ -36,6 +47,6 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
List.of(targetCount, timeLimit), List.of(targetCount, timeLimit),
timeLimit, // Limit timeLimit, // Limit
targetCount, // Goal targetCount, // Goal
List.of(new KillMonsterCountTrigger(), new InTimeTrigger())); challengeTriggers);
} }
} }

View File

@ -6,22 +6,33 @@ import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
public class KillMonsterTimeIncTrigger extends ChallengeTrigger { public class KillMonsterTimeIncTrigger extends ChallengeTrigger {
private int increment; private final int maxTime;
private final int increment;
public KillMonsterTimeIncTrigger(int increment) { public KillMonsterTimeIncTrigger(int maxTime, int increment) {
this.maxTime = maxTime;
this.increment = increment; this.increment = increment;
} }
@Override @Override
public void onBegin(WorldChallenge challenge) { public void onBegin(WorldChallenge challenge) {}
// challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 0,
// challenge.getScore().get()));
}
@Override @Override
public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) { public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) {
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 0, increment)); var scene = challenge.getScene();
var elapsed = scene.getSceneTimeSeconds() - challenge.getStartedAt();
var timeLeft = challenge.getTimeLimit() - elapsed;
var increment = this.increment;
if (increment == 0) {
// Refresh time limit back to max
increment = maxTime - timeLeft;
} else if (maxTime < timeLeft + increment) {
// Don't add back more time than original limit
increment -= timeLeft + increment - maxTime;
}
challenge.setTimeLimit(challenge.getTimeLimit() + increment); challenge.setTimeLimit(challenge.getTimeLimit() + increment);
scene.broadcastPacket(
new PacketChallengeDataNotify(
challenge, 2, timeLeft + increment + scene.getSceneTimeSeconds()));
} }
} }

View File

@ -222,7 +222,9 @@ public class EntityMonster extends GameEntity {
} }
@Override @Override
public void onCreate() { public void onTick(int sceneTime) {
super.onTick(sceneTime);
// Lua event // Lua event
getScene() getScene()
.getScriptManager() .getScriptManager()