3 Commits

Author SHA1 Message Date
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
9 changed files with 338 additions and 13 deletions

38
Dockerfile Normal file
View File

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

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:

5
entrypoint.sh Executable file
View File

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

228
generate-config.ts Normal file
View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
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.enums.ChallengeType;
import emu.grasscutter.game.dungeons.challenge.trigger.*;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.data.SceneGroup;
import java.util.List;
import java.util.*;
import lombok.val;
public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler {
@ -28,6 +29,16 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
Scene scene,
SceneGroup group) {
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(
scene,
realGroup,
@ -36,6 +47,6 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
List.of(targetCount, timeLimit),
timeLimit, // Limit
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 {
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;
}
@Override
public void onBegin(WorldChallenge challenge) {
// challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 0,
// challenge.getScore().get()));
}
public void onBegin(WorldChallenge challenge) {}
@Override
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);
scene.broadcastPacket(
new PacketChallengeDataNotify(
challenge, 2, timeLeft + increment + scene.getSceneTimeSeconds()));
}
}

View File

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