19 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
5ebad71e9d Bump to version 1.7.4 2023-11-30 23:41:55 -05:00
564b609028 Update README_ja-JP.md (#2438)
* Update README_ja-JP.md

* fix sentences correctly
2023-11-19 19:34:14 -05:00
cdb0dc560a Format code [skip actions] 2023-11-17 04:58:02 +00:00
d8c3da8fcd Handle mob summon and limbo state (#2432)
Mob summon: Something like Monster_Apparatus_Perpetual can summon helper mobs. Ensure these helpers actually get summoned and, on their defeat, possibly change the summoner's mob state. Like, temporarily enter weak state.
* Take summon tags from BinOutput/Monster/ConfigMonster_*.json and put them in SceneMonsterInfo
* Handle Summon action in ability modifiers from BinOutput/Ability/Temp/MonsterAbilities/ConfigAbility_Monster_*.json
* On summoner's kill, also kill the summoned mobs

Limbo state: Something like Monster_Invoker_Herald_Water should be invulnerable at a certain HP threshold. Like, shouldn't die when creating their elemental shield. Or, Monster_Apparatus_Perpetual's helper mobs shouldn't die before their summoner.
* Look through ConfigAbility (AbilityData in GC) like Invoker_Herald_Water_StateControl. If any AbilityModifier within specifies state Limbo and properties.Actor_HpThresholdRatio, account for this threshold in GameEntity::damage.
* Don't let the entity die while in limbo. They will be killed by other events.
2023-11-16 23:56:37 -05:00
13c40b53a7 Format code [skip actions] 2023-11-10 02:57:50 +00:00
f1c1a84683 fix: NPE related to teapot when player logs in. (#2429)
* fix: NPE related to home when player logs in.

* fix: NPE related to home when player logs in.

* forgot to save player after fixing module id
2023-11-09 21:56:21 -05:00
2bcbd41026 Format code [skip actions] 2023-11-09 02:16:38 +00:00
adf8031684 Fix a typo from "culivation" to "cultivation" in readme EN, zh-CN, zh-TW (#2431)
* fix a singular typo in readme.md

fixed "culivation" to cultivation

* Update README_zh-CN.md

culivation to cultivation

* Update zh-TW to fix "culivation"

Cultivation from culivaton
2023-11-08 21:15:57 -05:00
0bbeaf254b Fix tower mob level and hp scaling (#2430) 2023-11-08 21:15:10 -05:00
1fac319eb2 Format code [skip actions] 2023-11-05 19:58:28 +00:00
d224178a64 Only deduct energy when elemental burst actually fires (#2424) 2023-11-05 14:57:17 -05:00
d461ee2eb3 Format code [skip actions] 2023-11-03 02:02:24 +00:00
24874e7fba Implement abyss defense objective (#2422) 2023-11-02 22:00:05 -04:00
52 changed files with 1169 additions and 420 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

View File

@ -29,7 +29,7 @@
- Get game version REL4.0.x (4.0.x client can be found here if you don't have it): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
- After opening Culivation (as admin), press the download button in the upper right corner.
- After opening Cultivation (as admin), press the download button in the upper right corner.
- Click `Download All-in-One`
- Click the gear in the upper right corner
- Set the game Install path to where your game is located.

View File

@ -58,7 +58,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
group = 'io.grasscutter'
version = '1.7.3'
version = '1.7.4'
java {
withJavadocJar()

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:

View File

@ -3,81 +3,65 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) | [हिंदी](README_hn-IN.md)
[EN](README.md) | [简中](docs/README_zh-CN.md) | [繁中](docs/README_zh-TW.md) | [FR](docs/README_fr-FR.md) | [ES](docs/README_es-ES.md) | [HE](docs/README_HE.md) | [RU](docs/README_ru-RU.md) | [PL](docs/README_pl-PL.md) | [ID](docs/README_id-ID.md) | [KR](docs/README_ko-KR.md) | [FIL/PH](docs/README_fil-PH.md) | [NL](docs/README_NL.md) | [JP](docs/README_ja-JP.md) | [IT](docs/README_it-IT.md) | [VI](docs/README_vi-VN.md)
**:** 私たちはプロジェクトへの貢献者をいつでも歓迎します。貢献を追加する前に、我々の [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
**Attention:** 私たちはプロジェクトへのコントリビュータをいつでも歓迎します。コントリビュートする前に、私たちの [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
## 現在機能している
## 現在実装されている機能
* ログイン
* 戦闘
* フレンドリスト
* テレポート
* 祈願(ガチャ)
* マルチプレイは一部機能しています
* コンソールを使用してモンスタースポーンさせる
* 祈願 (ガチャ)
* マルチプレイ (一部)
* コンソールを通したモンスタースポーン
* インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど)
## クイックセットアップガイド
## かんたんセットアップガイド
**:** サポートが必要な場合はGrasscutterの[Discord](https://discord.gg/T5vZU6UyeG)に参加してください。
**Note:** サポートが必要な場合はGrasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加してください。
### 動作環境
### パパっとスタートアップ
* [JAVAのバージョン17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
- [Java (バージョン17以降)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) を用意する
- [MongoDB Community Server](https://www.mongodb.com/try/download/community) を用意する
- ゲームバージョンがREL4.0.Xのものを用意する (4.0.Xのクライアントを持っていない場合は右のリンクからダウンロード): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
- [最新の Cultivation](https://github.com/Grasscutters/Cultivation/releases/latest)をダウンロードする。`.msi`インストーラを使ってください。
- 管理者権限を付与して Cultivation を実行した後、右上端にあるダウンロードアイコンのボタンを押す。
- `Download All-in-One` をクリックする
- 右上端にある歯車アイコンのボタンをクリックする。
- `Game Install Path` にゲームファイルのパスを指定する。
- `Custom Java Path` に、自分が用意したJavaのパスを指定する。 (例: `C:\Program Files\Java\jdk-17\bin\java.exe`)
- その他の設定には手を付けず次の段階に進む。
**:** サーバーを動作させるだけならjreのみで十分です。 開発をしたい場合JDKが必要になるかもしれません
- Launch の隣にある小さいボタンを押す
- Launchボタンを押す
- 好きなユーザ名でログインする。パスワードは特段気にすることはない。
* [MongoDB](https://www.mongodb.com/try/download/community) (バージョン4.0以降を推奨)
* プロキシツール: [mitmproxy](https://mitmproxy.org/) (mitmdump, 推奨)、[Fiddler Classic](https://telerik-fiddler.s3.amazonaws.com/fiddler/FiddlerSetup.exe)、その他。
### 起動方法
**:** もしサーバーをアップデートしたい場合は`config.json`を削除してから再生成してください。
1. `grasscutter.jar`を入手する
- [releases](https://github.com/Grasscutters/Grasscutter/releases/latest) か [action](https://github.com/Grasscutters/Grasscutter/actions) からダウンロードするか、[自分でビルド](#ビルド)してください。
2. `grasscutter.jar` があるディレクトリに `resources` フォルダーを作成し、そこに `BinOutput, ExcelBinOutput, Readables, Scripts, Subtitle, TextMap` を移動してください *(`resources` フォルダの中身の入手方法については [wiki](https://github.com/Grasscutters/Grasscutter/wiki) を参照してください.)*
3. コマンドプロンプトに`java -jar grasscutter.jar`を入力しGrasscutterを起動してください。**このときMongoDBも実行する必要があります。**
### クライアントとの接続
½. [このコマンド](https://github.com/Grasscutters/Grasscutter/wiki/Commands#commands-for-server-admins)をサーバーコンソールから使用してアカウントを作成してください。
1. 通信内容をリダイレクトする: (どちらか一つを選択してください)
- mitmdump: `mitmdump -s proxy.py -k`
- CA証明書を信頼する:
- **:** CA証明書は`%USERPROFILE%\.mitmproxy`に保存されています。ダブルクリックして[インストール](https://docs.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate#installing-a-trusted-root-certificate)するか...
- コマンドライン経由でインストールします
```shell
certutil -addstore root %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer
```
- Fiddler Classic: Fiddler Classicを起動し(Tools -> Options -> HTTPS)から`Decrypt https traffic`をオンにしてください。 (Tools -> Options -> Connections) に有るポート番号の設定を`8888`以外に設定してください。その後この[スクリプト](https://github.com/Grasscutters/Grasscutter/wiki/Resources#fiddler-classic-jscript)をFiddlerScriptタブにコピペしてロードします。
- [ホストファイル](https://github.com/Grasscutters/Grasscutter/wiki/Resources#hosts-file)
2. ネットワークプロキシを `127.0.0.1:(自分で設定したポート番号)` に設定してください。
- mitmproxyを使用した場合プロキシの設定と証明書のインストールが終わった後、http://mitm.it/ でトラフィックがmitmproxyを通過しているか確認しましょう。
**`start.cmd`でmitmdumpとサーバーをまとめて起動することが出来ます。ただ、事前に`start_config.cmd`でJAVAのパスを指定している必要があります。**
### ビルド
GrasscutterはGradleを使用して依存関係とビルド処理しています。
Grasscutterは依存関係とビルド処理にGradleを使用しています。
**要件:**
**必要要件:**
- [Java SE Development Kits - 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
- [Java SE Development Kit 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download) (任意、ハンドブックの生成に必要)
##### Windows
##### Clone
```shell
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
```
##### Compile
**Note:** 環境によってはハンドブックの生成が失敗する場合があります。ハンドブックの生成をさせない場合は `gradlew jar` コマンドに `-PskipHandbook=1` を付け加えてください。
Windows:
```shell
git clone https://github.com/Grasscutters/Grasscutter.git
@ -86,7 +70,7 @@ cd Grasscutter
.\gradlew jar # コンパイル
```
##### Linux
Linux:
```bash
git clone https://github.com/Grasscutters/Grasscutter.git
@ -95,13 +79,8 @@ chmod +x gradlew
./gradlew jar # コンパイル
```
生成されたjarファイルはプロジェクトフォルダのルートにります。
生成されたjarファイルはプロジェクトフォルダのルートにります。
### コマンドリストは[wiki](https://github.com/Grasscutters/Grasscutter/wiki/Commands)へ移動しました。
### トラブルシューティング
# トラブルシューティング
* コンパイルが失敗した場合JDKがインストールされているか確認してください。(JDKのバージョンが17以降であることと、環境変数でJDKのパスが設定されている必要があります)
* クライアントが接続できない・ログインできない・エラーコード4206・またその他場合、ほとんどは、プロキシデーモンの設定が問題です。Fiddlerを使っている場合はデフォルトポートを8888以外の別のポートに変更してみてください。
Fiddlerを使用している場合はポートが8888以外に設定されていることを確認してください。
* 起動シーケンス(順番): MongoDB > Grasscutter > プロキシツール (mitmdumpかfiddler、その他) > ゲーム
よく散見されるトラブルとそれに対する解決策のまとめリストや、質問し誰かの助けを得たい場合は、Grasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加し、サポートチャンネルを参照してください。

View File

@ -29,7 +29,7 @@
- 获取游戏4.0正式版 (如果你没有4.0的客户端可以在这里找到https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md)
- 下载[最新的Cultivation版本](https://github.com/Grasscutters/Cultivation/releases/latest)(使用以“.msi”为后缀的安装包
- 以管理员身份打开Culivation按右上角的下载按钮。
- 以管理员身份打开Cultivation按右上角的下载按钮。
- 点击“下载 Grasscutter 一体化”
- 点击右上角的齿轮
- 将游戏安装路径设置为你游戏所在的位置。

View File

@ -29,7 +29,7 @@
- 下載遊戲版本 REL3.7(如果你沒有的話,可以在[這裡](https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/3.7.0.md)找到 3.7 客戶端)
- 下載 [最新的 Cultivation 版本](https://github.com/Grasscutters/Cultivation/releases/latest)。使用 `.msi` 安裝程式。
- 以管理員身分打開 Culivation按右上角的下載按鈕。
- 以管理員身分打開 Cultivation按右上角的下載按鈕。
- 點擊 `Download All-in-One`
- 點擊右上角的齒輪
- 將遊戲安裝路徑設置為你的遊戲所在的位置。

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.
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);
}
}
/**

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);
static int getIntFromEnv(String key, int defaultValue) {
var currentValue = System.getenv(key);
if (currentValue == null) {
return defaultValue;
}
} catch (Exception ignored) { }
var existing = config.version;
var latest = version();
if (existing == latest)
return;
// Create a new configuration instance.
var updated = new ConfigContainer();
// Update all configuration fields.
var fields = ConfigContainer.class.getDeclaredFields();
Arrays.stream(fields).forEach(field -> {
try {
field.set(updated, field.get(config));
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to update a configuration field.", exception);
return Integer.parseInt(currentValue, 10);
} catch (Exception e) {
return defaultValue;
}
}
}); 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);
/**
* 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 = """
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 = "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 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,10 +574,7 @@ 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;

View File

@ -302,6 +302,10 @@ public final class GameData {
private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeWorldModuleData> homeWorldModuleDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeWorldNPCData> homeWorldNPCDataMap =
new Int2ObjectOpenHashMap<>();

View File

@ -42,6 +42,7 @@ public class AbilityModifier implements Serializable {
public String stacking;
public AbilityMixinData[] modifierMixins;
public AbilityModifierProperty properties;
public ElementType elementType;
public DynamicFloat elementDurability = DynamicFloat.ZERO;
@ -327,6 +328,9 @@ public class AbilityModifier implements Serializable {
public String srcKey, dstKey;
public int skillID;
public int resistanceListID;
public int monsterID;
public int summonTag;
public AbilityModifierAction[] actions;
public AbilityModifierAction[] successActions;
@ -369,6 +373,11 @@ public class AbilityModifier implements Serializable {
}
}
public static class AbilityModifierProperty implements Serializable {
public float Actor_HpThresholdRatio;
// Add more properties here when GC needs them.
}
public enum State {
LockHP,
Invincible,

View File

@ -8,4 +8,5 @@ import lombok.experimental.FieldDefaults;
public class ConfigCombat {
// There are more values that can be added that might be useful in the json
ConfigCombatProperty property;
ConfigCombatSummon summon;
}

View File

@ -0,0 +1,14 @@
package emu.grasscutter.data.binout.config.fields;
import java.util.List;
import lombok.*;
@Data
public class ConfigCombatSummon {
List<SummonTag> summonTags;
@Getter
public final class SummonTag {
int summonTag;
}
}

View File

@ -0,0 +1,17 @@
package emu.grasscutter.data.excels;
import emu.grasscutter.data.GameResource;
import emu.grasscutter.data.ResourceType;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@ResourceType(name = "HomeworldModuleExcelConfigData.json")
@FieldDefaults(level = AccessLevel.PRIVATE)
@Getter
public class HomeWorldModuleData extends GameResource {
int Id;
boolean isFree;
int worldSceneId;
int defaultRoomSceneId;
}

View File

@ -13,6 +13,7 @@ public class TowerLevelData extends GameResource {
private int levelGroupId;
private int dungeonId;
private List<TowerLevelCond> conds;
private int monsterLevel;
public static class TowerLevelCond {
private TowerCondType towerCondType;

View File

@ -2,6 +2,7 @@ package emu.grasscutter.game.ability;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.AbilityStringOuterClass.AbilityString;
@ -24,6 +25,7 @@ public class Ability {
private static Map<String, Object2FloatMap<String>> abilitySpecialsModified = new HashMap<>();
@Getter private int hash;
@Getter private Set<Integer> avatarSkillStartIds;
public Ability(AbilityData data, GameEntity owner, Player playerOwner) {
this.data = data;
@ -44,6 +46,49 @@ public class Ability {
hash = Utils.abilityHash(data.abilityName);
data.initialize();
//
// Collect skill IDs referenced by AvatarSkillStart modifier actions
// in onAbilityStart and in every modifier's onAdded action set.
// These skill IDs will be used by AbilityManager to determine whether
// an elemental burst has fired correctly.
//
avatarSkillStartIds = new HashSet<>();
if (data.onAbilityStart != null) {
avatarSkillStartIds.addAll(
Arrays.stream(data.onAbilityStart)
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
.map(action -> action.skillID)
.toList());
}
avatarSkillStartIds.addAll(
data.modifiers.values().stream()
.map(
m ->
(List<AbilityModifierAction>)
(m.onAdded == null ? Collections.emptyList() : Arrays.asList(m.onAdded)))
.flatMap(List::stream)
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
.map(action -> action.skillID)
.toList());
if (data.onAdded != null) {
processOnAddedAbilityModifiers();
}
}
public void processOnAddedAbilityModifiers() {
for (AbilityModifierAction modifierAction : data.onAdded) {
if (modifierAction.type == null) continue;
if (modifierAction.type == AbilityModifierAction.Type.ApplyModifier) {
if (modifierAction.modifierName == null) continue;
else if (!data.modifiers.containsKey(modifierAction.modifierName)) continue;
var modifierData = data.modifiers.get(modifierAction.modifierName);
owner.onAddAbilityModifier(modifierData);
}
}
}
public static String getAbilityName(AbilityString abString) {

View File

@ -21,7 +21,7 @@ import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalar
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import emu.grasscutter.server.event.player.PlayerUseSkillEvent;
import io.netty.util.concurrent.FastThreadLocalThread;
import java.util.HashMap;
import java.util.*;
import java.util.concurrent.*;
import lombok.Getter;
@ -48,9 +48,64 @@ public final class AbilityManager extends BasePlayerManager {
}
@Getter private boolean abilityInvulnerable = false;
private int burstCasterId;
private int burstSkillId;
public AbilityManager(Player player) {
super(player);
removePendingEnergyClear();
}
public void removePendingEnergyClear() {
this.burstCasterId = 0;
this.burstSkillId = 0;
}
private void onPossibleElementalBurst(Ability ability, AbilityModifier modifier, int entityId) {
//
// Possibly clear avatar energy spent on elemental burst
// and set invulnerability.
//
// Problem: Burst can misfire occasionally, like hitting Q when
// dashing, doing E, or switching avatars. The client would
// still send EvtDoSkillSuccNotify, but the burst may not
// actually happen. We don't know when to clear avatar energy.
//
// When burst does happen, a number of AbilityInvokeEntry will
// come in. Use the Ability it references and search for any
// modifier with type=AvatarSkillStart, skillID=burst skill ID.
//
// If that is missing, search for modifier action that sets
// invulnerability as a fallback.
//
if (this.burstCasterId == 0) return;
boolean skillInvincibility = modifier.state == AbilityModifier.State.Invincible;
if (modifier.onAdded != null) {
skillInvincibility |=
Arrays.stream(modifier.onAdded)
.filter(
action ->
action.type == AbilityModifierAction.Type.AttachAbilityStateResistance
&& action.resistanceListID == 11002)
.toList()
.size()
> 0;
}
if (this.burstCasterId == entityId
&& (ability.getAvatarSkillStartIds().contains(this.burstSkillId) || skillInvincibility)) {
Grasscutter.getLogger()
.trace(
"Caster ID's {} burst successful, clearing energy and setting invulnerability",
entityId);
this.abilityInvulnerable = true;
this.player
.getEnergyManager()
.handleEvtDoSkillSuccNotify(
this.player.getSession(), this.burstSkillId, this.burstCasterId);
this.removePendingEnergyClear();
}
}
public static void registerHandlers() {
@ -280,8 +335,9 @@ public final class AbilityManager extends BasePlayerManager {
return;
}
// Set the player as invulnerable.
this.abilityInvulnerable = true;
// Track this elemental burst to possibly clear avatar energy later.
this.burstSkillId = skillId;
this.burstCasterId = casterId;
}
/**
@ -454,6 +510,8 @@ public final class AbilityManager extends BasePlayerManager {
modifierData);
}
onPossibleElementalBurst(instancedAbility, modifierData, invoke.getEntityId());
AbilityModifierController modifier =
new AbilityModifierController(instancedAbility, instancedAbilityData, modifierData);

View File

@ -0,0 +1,70 @@
package emu.grasscutter.game.ability.actions;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.Ability;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.world.*;
import emu.grasscutter.net.proto.EPKDEHOJFLIOuterClass.EPKDEHOJFLI;
import emu.grasscutter.server.packet.send.PacketMonsterSummonTagNotify;
import emu.grasscutter.utils.*;
@AbilityAction(AbilityModifierAction.Type.Summon)
public class ActionSummon extends AbilityActionHandler {
@Override
public synchronized boolean execute(
Ability ability, AbilityModifierAction action, ByteString abilityData, GameEntity target) {
EPKDEHOJFLI summonPosRot = null;
try {
// In game version 4.0, summoned entity's
// position and rotation are packed in EPKDEHOJFLI.
// This is packet AbilityActionSummon and has two fields:
// 4: Vector pos
// 13: Vector rot
summonPosRot = EPKDEHOJFLI.parseFrom(abilityData);
} catch (InvalidProtocolBufferException e) {
Grasscutter.getLogger()
.error("Failed to parse abilityData: {}", Utils.bytesToHex(abilityData.toByteArray()));
return false;
}
var pos = new Position(summonPosRot.getPos());
var rot = new Position(summonPosRot.getRot());
var monsterId = action.monsterID;
var scene = target.getScene();
var monsterData = GameData.getMonsterDataMap().get(monsterId);
if (monsterData == null) {
Grasscutter.getLogger().error("Failed to find monster by ID {}", monsterId);
return false;
}
if (target instanceof EntityMonster ownerEntity) {
var level = scene.getLevelForMonster(0, ownerEntity.getLevel());
var entity = new EntityMonster(scene, monsterData, pos, rot, level);
ownerEntity.getSummonTagMap().put(action.summonTag, entity);
entity.setSummonedTag(action.summonTag);
entity.setOwnerEntityId(target.getId());
scene.addEntity(entity);
scene.getPlayers().get(0).sendPacket(new PacketMonsterSummonTagNotify(ownerEntity));
Grasscutter.getLogger()
.trace(
"Spawned entityId {} monsterId {} pos {} rot {}, target { {} }, action { {} }",
entity.getId(),
monsterId,
pos,
rot,
target,
action);
return true;
} else {
return false;
}
}
}

View File

@ -68,6 +68,10 @@ public final class DungeonManager {
}
if (isFinishedSuccessfully()) {
// Set ended now because calling EVENT_DUNGEON_SETTLE
// during finishDungeon() may cause reentrance into
// this function, leading to double settles.
ended = true;
finishDungeon();
}
}
@ -78,9 +82,14 @@ public final class DungeonManager {
}
public int getLevelForMonster(int id) {
if (isTowerDungeon()) {
// Tower dungeons have their own level setting in TowerLevelData
return scene.getPlayers().get(0).getTowerManager().getCurrentMonsterLevel();
} else {
// TODO should use levelConfigMap? and how?
return dungeonData.getShowLevel();
}
}
public boolean activateRespawnPoint(int pointId) {
val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId);

View File

@ -1,5 +1,6 @@
package emu.grasscutter.game.dungeons;
import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult;
import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult.DungeonEndReason;
import emu.grasscutter.game.dungeons.dungeon_results.TowerResult;
import emu.grasscutter.server.packet.send.*;
@ -25,16 +26,22 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
var towerManager = scene.getPlayers().get(0).getTowerManager();
var stars = towerManager.getCurLevelStars();
if (endReason == DungeonEndReason.COMPLETED) {
// Update star record only when challenge completes successfully.
towerManager.notifyCurLevelRecordChangeWhenDone(stars);
scene.broadcastPacket(
new PacketTowerFloorRecordChangeNotify(
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor()));
}
var challenge = scene.getChallenge();
var finishedTime = challenge == null ? challenge.getFinishedTime() : 0;
var dungeonStats =
new DungeonEndStats(
scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason);
var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars);
new DungeonEndStats(scene.getKilledMonsterCount(), finishedTime, 0, endReason);
var result =
endReason == DungeonEndReason.COMPLETED
? new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars)
: new BaseDungeonResult(dungeonData, dungeonStats);
scene.broadcastPacket(new PacketDungeonSettleNotify(result));
}

View File

@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.constants.EventType;
@ -22,12 +23,13 @@ public class WorldChallenge {
private final int challengeIndex;
private final List<Integer> paramList;
private int timeLimit;
private GameEntity guardEntity;
private final List<ChallengeTrigger> challengeTriggers;
private final int goal;
private final AtomicInteger score;
private boolean progress;
private boolean success;
private long startedAt;
private int startedAt;
private int finishedTime;
/**
@ -58,6 +60,7 @@ public class WorldChallenge {
this.challengeTriggers = challengeTriggers;
this.goal = goal;
this.score = new AtomicInteger(0);
this.guardEntity = null;
}
public boolean inProgress() {
@ -143,6 +146,10 @@ public class WorldChallenge {
this.progress = false;
this.success = success;
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt));
// Despawn all leftover mobs in this challenge's SceneGroup
getScene().getScriptManager().removeMonstersInGroup(group);
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
}
@ -150,6 +157,20 @@ public class WorldChallenge {
return score.incrementAndGet();
}
public int getGuardEntityHpPercent() {
if (guardEntity == null) {
Grasscutter.getLogger()
.warn(
"getGuardEntityHpPercent: Could not find guardEntity for this challenge = {}", this);
return 100;
}
var curHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
var maxHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
int percent = (int) (curHp * 100 / maxHp);
return percent;
}
public void onMonsterDeath(EntityMonster monster) {
if (!inProgress()) {
return;

View File

@ -33,7 +33,7 @@ public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHand
realGroup,
challengeId, // Id
challengeIndex, // Index
List.of(monstersToKill, 0),
List.of(monstersToKill, gadgetCFGId),
0, // Limit
monstersToKill, // Goal
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));

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

@ -2,7 +2,6 @@ package emu.grasscutter.game.dungeons.challenge.trigger;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
public class GuardTrigger extends ChallengeTrigger {
@ -14,7 +13,12 @@ public class GuardTrigger extends ChallengeTrigger {
}
public void onBegin(WorldChallenge challenge) {
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, 100));
challenge.setGuardEntity(
challenge.getScene().getEntityByConfigId(entityToProtectCFGId, challenge.getGroup().id));
lastSendPercent = challenge.getGuardEntityHpPercent();
challenge
.getScene()
.broadcastPacket(new PacketChallengeDataNotify(challenge, 2, lastSendPercent));
}
@Override
@ -22,9 +26,7 @@ public class GuardTrigger extends ChallengeTrigger {
if (gadget.getConfigId() != entityToProtectCFGId) {
return;
}
var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
var maxHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
int percent = (int) (curHp / maxHp);
var percent = challenge.getGuardEntityHpPercent();
if (percent != lastSendPercent) {
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent));

View File

@ -18,12 +18,6 @@ public class InTimeTrigger extends ChallengeTrigger {
@Override
public void onCheckTimeout(WorldChallenge challenge) {
// In Tower challenges, time can run out without
// causing the challenge to fail. (Player just
// gets 0 stars when they ultimately finish.)
var dungeonManager = challenge.getScene().getDungeonManager();
if (dungeonManager != null && dungeonManager.isTowerDungeon()) return;
var current = challenge.getScene().getSceneTimeSeconds();
if (current - challenge.getStartedAt() > challenge.getTimeLimit()) {
challenge.fail();

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

@ -32,11 +32,12 @@ public class TowerResult extends BaseDungeonResult {
@Override
protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE;
if (challenge.isSuccess() && canJump) {
continueStatus =
hasNextLevel
? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE
: ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
if (challenge.isSuccess()) {
if (hasNextLevel) {
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE;
} else if (canJump) {
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
}
}
var towerLevelEndNotify =

View File

@ -70,6 +70,11 @@ public abstract class EntityBaseGadget extends GameEntity {
.setSourceEntityId(getId())
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
.setEventSource(getConfigId()));
var challenge = getScene().getChallenge();
if (challenge != null && this instanceof EntityGadget gadget) {
challenge.onGadgetDamage(gadget);
}
}
protected void fillFightProps(ConfigEntityGadget configGadget) {

View File

@ -5,6 +5,7 @@ import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.config.ConfigEntityGadget;
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData;
import emu.grasscutter.data.excels.GadgetData;
import emu.grasscutter.data.excels.monster.MonsterCurveData;
import emu.grasscutter.game.entity.gadget.*;
import emu.grasscutter.game.entity.gadget.platform.*;
import emu.grasscutter.game.player.Player;
@ -104,6 +105,25 @@ public class EntityGadget extends EntityBaseGadget {
this.bornRot = this.getRotation().clone();
this.fillFightProps(configGadget);
// Check if this gadget is the abyss defense objective's gadget.
// That doesn't have a level and defaults to having 5000 hp, so it dies in like 2 hits on 11-1.
// I'll forgive player skill issues and scale its hp up here.
// TODO: find out how its fight props are actually scaled
if (gadgetData.getJsonName().equals("SceneObj_Gear_Operator_Mamolu_Entity")) {
MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(11);
if (curve != null) {
FightProperty[] hpProps = {
FightProperty.FIGHT_PROP_MAX_HP,
FightProperty.FIGHT_PROP_BASE_HP,
FightProperty.FIGHT_PROP_CUR_HP
};
for (var prop : hpProps) {
setFightProperty(
prop, this.getFightProperty(prop) * curve.getMultByProp("GROW_CURVE_HP_ENVIRONMENT"));
}
}
}
if (GameData.getGadgetMappingMap().containsKey(gadgetId)) {
var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController();
this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName));

View File

@ -25,6 +25,7 @@ import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo;
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
import emu.grasscutter.net.proto.ServantInfoOuterClass.ServantInfo;
import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.*;
import emu.grasscutter.server.event.entity.EntityDamageEvent;
@ -49,6 +50,9 @@ public class EntityMonster extends GameEntity {
@Getter private final Position bornPos;
@Getter private final int level;
@Getter private EntityWeapon weaponEntity;
@Getter private Map<Integer, EntityMonster> summonTagMap;
@Getter @Setter private int summonedTag;
@Getter @Setter private int ownerEntityId;
@Getter @Setter private int poseId;
@Getter @Setter private int aiId = -1;
@ -67,6 +71,9 @@ public class EntityMonster extends GameEntity {
this.bornPos = this.getPosition().clone();
this.level = level;
this.playerOnBattle = new ArrayList<>();
this.summonTagMap = new HashMap<>();
this.summonedTag = 0;
this.ownerEntityId = 0;
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
this.configEntityMonster =
@ -76,6 +83,17 @@ public class EntityMonster extends GameEntity {
this.configEntityMonster = null;
}
if (this.configEntityMonster != null
&& this.configEntityMonster.getCombat() != null
&& this.configEntityMonster.getCombat().getSummon() != null
&& this.configEntityMonster.getCombat().getSummon().getSummonTags() != null) {
this.configEntityMonster
.getCombat()
.getSummon()
.getSummonTags()
.forEach(t -> this.summonTagMap.put(t.getSummonTag(), null));
}
// Monster weapon
if (getMonsterWeaponId() > 0) {
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
@ -204,7 +222,9 @@ public class EntityMonster extends GameEntity {
}
@Override
public void onCreate() {
public void onTick(int sceneTime) {
super.onTick(sceneTime);
// Lua event
getScene()
.getScriptManager()
@ -316,6 +336,11 @@ public class EntityMonster extends GameEntity {
this.getMonsterData().getType().getValue());
scene.triggerDungeonEvent(
DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
// If this entity spawned servants, kill those too.
summonTagMap.values().stream()
.filter(Objects::nonNull)
.forEach(entity -> scene.killEntity(entity, killerId));
}
public void recalcStats() {
@ -355,6 +380,28 @@ public class EntityMonster extends GameEntity {
+ (this.getFightProperty(c.getBase())
* (1f + this.getFightProperty(c.getPercent())))));
// If in tower, scale max hp by
// +50%: Floors 3 7
// +100%: Floors 8 11
// +150%: Floor 12
var dungeonManager = getScene().getDungeonManager();
var towerManager = getScene().getPlayers().get(0).getTowerManager();
if (dungeonManager != null && dungeonManager.isTowerDungeon() && towerManager != null) {
var floor = towerManager.getCurrentFloorNumber();
float additionalScaleFactor = 0f;
if (floor >= 12) {
additionalScaleFactor = 1.5f;
} else if (floor >= 8) {
additionalScaleFactor = 1.f;
} else if (floor >= 3) {
additionalScaleFactor = .5f;
}
this.setFightProperty(
FightProperty.FIGHT_PROP_MAX_HP,
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * (1 + additionalScaleFactor));
}
// Set current hp
this.setFightProperty(
FightProperty.FIGHT_PROP_CUR_HP,
@ -365,14 +412,17 @@ public class EntityMonster extends GameEntity {
public SceneEntityInfo toProto() {
var data = this.getMonsterData();
var aiInfo =
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(this.getBornPos().toProto());
if (ownerEntityId != 0) {
aiInfo.setServantInfo(ServantInfo.newBuilder().setMasterEntityId(ownerEntityId));
}
var authority =
EntityAuthorityInfo.newBuilder()
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
.setAiInfo(
SceneEntityAiInfo.newBuilder()
.setIsAiOpen(true)
.setBornPos(this.getBornPos().toProto()))
.setAiInfo(aiInfo)
.setBornPos(this.getBornPos().toProto())
.build();
@ -403,7 +453,10 @@ public class EntityMonster extends GameEntity {
.setAuthorityPeerId(this.getWorld().getHostPeerId())
.setPoseId(this.getPoseId())
.setBlockId(this.getScene().getId())
.setSummonedTag(this.summonedTag)
.setOwnerEntityId(this.ownerEntityId)
.setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT);
summonTagMap.forEach((k, v) -> monsterInfo.putSummonTagMap(k, v == null ? 0 : 1));
if (this.metaMonster != null) {
if (this.metaMonster.special_name_id != 0) {

View File

@ -1,6 +1,7 @@
package emu.grasscutter.game.entity;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.game.ability.*;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.*;
@ -32,6 +33,8 @@ public abstract class GameEntity {
@Getter @Setter private int lastMoveReliableSeq;
@Getter @Setter private boolean lockHP;
private boolean limbo;
private float limboHpThreshold;
@Setter(AccessLevel.PROTECTED)
@Getter
@ -110,6 +113,21 @@ public abstract class GameEntity {
});
}
protected void setLimbo(float hpThreshold) {
limbo = true;
limboHpThreshold = hpThreshold;
}
public void onAddAbilityModifier(AbilityModifier data) {
// Set limbo state (invulnerability at a certain HP threshold)
// if ability modifier calls for it
if (data.state == AbilityModifier.State.Limbo
&& data.properties != null
&& data.properties.Actor_HpThresholdRatio > .0f) {
this.setLimbo(data.properties.Actor_HpThresholdRatio);
}
}
protected MotionInfo getMotionInfo() {
return MotionInfo.newBuilder()
.setPos(this.getPosition().toProto())
@ -167,11 +185,26 @@ public abstract class GameEntity {
return; // If the event is canceled, do not damage the entity.
}
float effectiveDamage = 0;
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
if (limbo) {
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
float curRatio = curHp / maxHp;
if (curRatio > limboHpThreshold) {
// OK if this hit takes HP below threshold.
effectiveDamage = event.getDamage();
}
if (effectiveDamage >= curHp && limboHpThreshold > .0f) {
// Don't let entity die while in limbo.
effectiveDamage = curHp - 1;
}
} else if (curHp != Float.POSITIVE_INFINITY && !lockHP
|| lockHP && curHp <= event.getDamage()) {
effectiveDamage = event.getDamage();
}
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -effectiveDamage);
this.lastAttackType = attackType;
this.checkIfDead();

View File

@ -9,9 +9,10 @@ import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass;
import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo;
import emu.grasscutter.server.packet.send.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntSets;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
@ -36,6 +37,10 @@ public class GameHome {
|| sceneData.getSceneType() == SceneType.SCENE_HOME_ROOM)
.map(SceneData::getId)
.collect(Collectors.toUnmodifiableSet());
public static final Set<Integer> HOME_MODULE_IDS =
GameData.getHomeWorldModuleDataMap().isEmpty()
? IntSets.fromTo(1, 6)
: GameData.getHomeWorldModuleDataMap().keySet();
@Id String id;
@ -181,6 +186,7 @@ public class GameHome {
public void onOwnerLogin(Player player) {
this.player = player; // update player pointer. (prevent offline player from sending packet)
this.fixModuleIdIfInvalid();
player.getSession().send(new PacketHomeBasicInfoNotify(player, false));
player.getSession().send(new PacketPlayerHomeCompInfoNotify(player));
player.getSession().send(new PacketHomeComfortInfoNotify(player));
@ -194,6 +200,35 @@ public class GameHome {
player.getSession().send(new PacketHomeResourceNotify(player));
}
private void fixModuleIdIfInvalid() {
if (this.player.hasSentLoginPackets() || this.player.getRealmList() == null) {
return;
}
this.player
.getRealmList()
.removeIf(integer -> !HOME_MODULE_IDS.contains(integer)); // Delete invalid module ids.
if (this.player.getRealmList().isEmpty()) {
this.player.setRealmList(null);
this.player.save();
return;
}
if (this.player.getCurrentRealmId() <= 0 || !this.player.getCurHomeWorld().isRealmIdValid()) {
int firstRId = this.player.getRealmList().iterator().next();
this.player.setCurrentRealmId(firstRId);
this.player.save();
Grasscutter.getLogger()
.info(
"Set player {}'s current realm id to {} cuz the id is invalid.",
this.player.getUid(),
firstRId);
}
this.player.getCurHomeWorld().refreshModuleManager(); // Apply module id fix.
}
public void onPlayerChangedAvatarCostume(Avatar avatar) {
var world = this.player.getServer().getHomeWorldOrCreate(this.player);
world.broadcastPacket(
@ -239,8 +274,7 @@ public class GameHome {
return this.finishedTalkIdMap.get(avatarId);
}
public List<HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo>
toAvatarTalkFinishInfoProto() {
public List<HomeAvatarTalkFinishInfo> toAvatarTalkFinishInfoProto() {
if (this.finishedTalkIdMap == null) {
this.finishedTalkIdMap = new HashMap<>();
}
@ -248,7 +282,7 @@ public class GameHome {
return this.finishedTalkIdMap.entrySet().stream()
.map(
e -> {
return HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo.newBuilder()
return HomeAvatarTalkFinishInfo.newBuilder()
.setAvatarId(e.getKey())
.addAllFinishTalkIdList(e.getValue())
.build();

View File

@ -1,18 +1,20 @@
package emu.grasscutter.game.home;
import com.github.davidmoten.guavamini.Lists;
import emu.grasscutter.game.home.suite.HomeSuiteItem;
import emu.grasscutter.game.home.suite.event.HomeAvatarRewardEvent;
import emu.grasscutter.game.home.suite.event.HomeAvatarSummonEvent;
import emu.grasscutter.game.home.suite.event.SuiteEventType;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass;
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass;
import emu.grasscutter.net.proto.RetcodeOuterClass;
import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify;
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonAllEventNotify;
import emu.grasscutter.utils.Either;
import java.util.*;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@ -24,8 +26,8 @@ public class HomeModuleManager {
final HomeWorld homeWorld;
final GameHome home;
final int moduleId;
final HomeScene outdoor;
HomeScene indoor;
@Nullable final HomeScene outdoor;
@Nullable HomeScene indoor;
final List<HomeAvatarRewardEvent> rewardEvents;
final List<HomeAvatarSummonEvent> summonEvents;
@ -45,8 +47,14 @@ public class HomeModuleManager {
return;
}
if (this.outdoor != null) {
this.outdoor.onTick();
}
if (this.indoor != null) {
this.indoor.onTick();
}
this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
}
@ -67,6 +75,7 @@ public class HomeModuleManager {
this.rewardEvents.clear();
var allBlockItems =
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
.filter(Objects::nonNull)
.map(HomeSceneItem::getBlockItems)
.map(Map::values)
.flatMap(Collection::stream)
@ -114,6 +123,7 @@ public class HomeModuleManager {
private void cancelSummonEventsIfAvatarLeave() {
var avatars =
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
.filter(Objects::nonNull)
.map(HomeSceneItem::getBlockItems)
.map(Map::values)
.flatMap(Collection::stream)
@ -127,16 +137,16 @@ public class HomeModuleManager {
public Either<List<GameItem>, Integer> claimAvatarRewards(int eventId) {
if (this.rewardEvents.isEmpty()) {
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
return Either.right(Retcode.RET_FAIL_VALUE);
}
var event = this.rewardEvents.remove(0);
if (event.getEventId() != eventId) {
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
return Either.right(Retcode.RET_FAIL_VALUE);
}
if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
return Either.right(Retcode.RET_FAIL_VALUE);
}
return Either.left(event.giveRewards());
@ -144,32 +154,34 @@ public class HomeModuleManager {
public Either<HomeAvatarSummonEvent, Integer> fireAvatarSummonEvent(
Player owner, int avatarId, int guid, int suiteId) {
var targetSuite =
((HomeScene) owner.getScene())
.getSceneItem().getBlockItems().values().stream()
HomeSuiteItem targetSuite = null;
if (owner.getScene() instanceof HomeScene homeScene) {
targetSuite =
homeScene.getSceneItem().getBlockItems().values().stream()
.map(HomeBlockItem::getSuiteList)
.flatMap(Collection::stream)
.filter(suite -> suite.getGuid() == guid)
.findFirst()
.orElse(null);
}
if (this.isInRewardEvent(avatarId)) {
return Either.right(RetcodeOuterClass.Retcode.RET_DUPLICATE_AVATAR_VALUE);
return Either.right(Retcode.RET_DUPLICATE_AVATAR_VALUE);
}
if (this.rewardEvents.stream().anyMatch(event -> event.getGuid() == guid)) {
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
return Either.right(Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
}
this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
if (targetSuite == null) {
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
}
var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
if (eventData == null) {
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
}
var event =
@ -184,8 +196,8 @@ public class HomeModuleManager {
this.summonEvents.removeIf(event -> event.getEventId() == eventId);
}
public HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify toRewardEventProto() {
var notify = HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify.newBuilder();
public HomeAvatarRewardEventNotify toRewardEventProto() {
var notify = HomeAvatarRewardEventNotify.newBuilder();
if (!this.rewardEvents.isEmpty()) {
notify.setRewardEvent(this.rewardEvents.get(0).toProto()).setIsEventTrigger(true);
@ -198,9 +210,8 @@ public class HomeModuleManager {
return notify.build();
}
public HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify
toSummonEventProto() {
return HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify.newBuilder()
public HomeAvatarSummonAllEventNotify toSummonEventProto() {
return HomeAvatarSummonAllEventNotify.newBuilder()
.addAllSummonEventList(
this.summonEvents.stream().map(HomeAvatarSummonEvent::toProto).toList())
.build();
@ -210,12 +221,12 @@ public class HomeModuleManager {
return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
}
public HomeSceneItem getOutdoorSceneItem() {
return this.outdoor.getSceneItem();
@Nullable public HomeSceneItem getOutdoorSceneItem() {
return this.outdoor == null ? null : this.outdoor.getSceneItem();
}
public HomeSceneItem getIndoorSceneItem() {
return this.indoor.getSceneItem();
@Nullable public HomeSceneItem getIndoorSceneItem() {
return this.indoor == null ? null : this.indoor.getSceneItem();
}
public void onSetModule() {
@ -223,8 +234,14 @@ public class HomeModuleManager {
return;
}
if (this.outdoor != null) {
this.outdoor.addEntities(this.getOutdoorSceneItem().getAnimals(this.outdoor));
}
if (this.indoor != null) {
this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
}
this.fireAllAvatarRewardEvents();
}
@ -233,7 +250,12 @@ public class HomeModuleManager {
return;
}
if (this.outdoor != null) {
this.outdoor.getEntities().clear();
}
if (this.indoor != null) {
this.indoor.getEntities().clear();
}
}
}

View File

@ -3,7 +3,6 @@ package emu.grasscutter.game.home;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.entity.EntityTeam;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.game.world.World;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.ChatInfoOuterClass;
@ -13,6 +12,7 @@ import emu.grasscutter.server.packet.send.PacketPlayerChatNotify;
import emu.grasscutter.server.packet.send.PacketPlayerGameTimeNotify;
import java.util.List;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import lombok.Getter;
@Getter
@ -66,7 +66,7 @@ public class HomeWorld extends World {
}
public boolean isRealmIdValid() {
return this.getHost().getCurrentRealmId() > 0;
return this.getSceneById(this.getHost().getCurrentRealmId() + 2000) != null;
}
@Override
@ -147,11 +147,13 @@ public class HomeWorld extends World {
player.setWorld(null);
// Remove from scene
Scene scene = this.getSceneById(player.getSceneId());
var scene = this.getSceneById(player.getSceneId());
if (scene != null) {
scene.removePlayer(player);
}
// Info packet for other players
if (this.getPlayers().size() > 0) {
if (!this.getPlayers().isEmpty()) {
this.updatePlayerInfos(player);
}
@ -167,7 +169,7 @@ public class HomeWorld extends World {
}
@Override
public HomeScene getSceneById(int sceneId) {
@Nullable public HomeScene getSceneById(int sceneId) {
var scene = this.getScenes().get(sceneId);
if (scene instanceof HomeScene homeScene) {
return homeScene;

View File

@ -139,11 +139,15 @@ public class HomeWorldMPSystem extends BaseGameSystem {
int realmId = 2000 + owner.getCurrentRealmId();
var item = targetHome.getHomeSceneItem(realmId);
var scene = world.getSceneById(realmId);
targetHome.save();
var pos =
toSafe
? world.getSceneById(realmId).getScriptManager().getConfig().born_pos
: item.getBornPos();
Position pos;
if (scene != null) {
pos = toSafe ? scene.getScriptManager().getConfig().born_pos : item.getBornPos();
} else {
pos = item.getBornPos();
}
if (teleportPoint != 0) {
var target = item.getTeleportPointPos(teleportPoint);

View File

@ -259,8 +259,14 @@ public class EnergyManager extends BasePlayerManager {
return;
}
// Also reference AvatarSkillData in case the burst gets a different skill ID
// when the avatar is in a different state. For example, Wanderer's burst is
// 10755 usually but when he floats, it becomes 10753.
var skillData = GameData.getAvatarSkillDataMap().get(skillId);
// If the cast skill was a burst, consume energy.
if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) {
if ((avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill())
|| (skillData != null && skillData.getCostElemVal() > 0)) {
avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
}
}

View File

@ -100,7 +100,7 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
}
private void setOpenState(int openState, int value, boolean sendNotify) {
int previousValue = this.player.getOpenStates().getOrDefault(openState, 0);
int previousValue = this.player.getOpenStates().getOrDefault(openState, -1 /* non-existent */);
if (value != previousValue) {
this.player.getOpenStates().put(openState, value);

View File

@ -29,6 +29,11 @@ public class TowerManager extends BasePlayerManager {
return this.getTowerData().currentFloorId;
}
/** floor number: 1 - 12 * */
public int getCurrentFloorNumber() {
return GameData.getTowerFloorDataMap().get(getCurrentFloorId()).getFloorIndex();
}
public int getCurrentLevelId() {
return this.getTowerData().currentLevelId + this.getTowerData().currentLevel;
}
@ -40,7 +45,7 @@ public class TowerManager extends BasePlayerManager {
public void onTick() {
var challenge = player.getScene().getChallenge();
if (challenge == null || !challenge.inProgress()) return;
if (!inProgress || challenge == null || !challenge.inProgress()) return;
// Check star conditions and notify client if any failed.
int stars = getCurLevelStars();
@ -93,8 +98,17 @@ public class TowerManager extends BasePlayerManager {
player.getTeamManager().setupTemporaryTeam(towerTeams);
}
public TowerLevelData getCurrentTowerLevelDataMap() {
return GameData.getTowerLevelDataMap().get(getCurrentLevelId());
}
public int getCurrentMonsterLevel() {
// monsterLevel given in TowerLevelExcelConfigData.json is off by one.
return getCurrentTowerLevelDataMap().getMonsterLevel() + 1;
}
public void enterLevel(int enterPointId) {
var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
var levelData = getCurrentTowerLevelDataMap();
var dungeonId = levelData.getDungeonId();
@ -140,7 +154,7 @@ public class TowerManager extends BasePlayerManager {
return 0;
}
var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
var levelData = getCurrentTowerLevelDataMap();
// 0-based indexing. "star" = 0 means checking for 1-star conditions.
int star;
for (star = 2; star >= 0; star--) {
@ -153,8 +167,11 @@ public class TowerManager extends BasePlayerManager {
break;
}
} else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) {
// TODO: Check monolith health
var params = levelData.getHpCond(star);
var hpPercent = challenge.getGuardEntityHpPercent();
if (hpPercent >= params.getMinimumHpPercentage()) {
break;
}
} else {
Grasscutter.getLogger()
.error(

View File

@ -600,7 +600,7 @@ public class Scene {
// Should be OK to check only player 0,
// as no other players could enter Tower
var towerManager = getPlayers().get(0).getTowerManager();
if (towerManager != null) {
if (towerManager != null && towerManager.isInProgress()) {
towerManager.onTick();
}
@ -767,6 +767,19 @@ public class Scene {
return level;
}
public int getLevelForMonster(int configId, int defaultLevel) {
if (getDungeonManager() != null) {
return getDungeonManager().getLevelForMonster(configId);
} else if (getWorld().getWorldLevel() > 0) {
var worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel());
if (worldLevelData != null) {
return worldLevelData.getMonsterLevel();
}
}
return defaultLevel;
}
public void checkNpcGroup() {
Set<SceneNpcBornEntry> npcBornEntries = ConcurrentHashMap.newKeySet();
for (Player player : this.getPlayers()) {

View File

@ -5,14 +5,19 @@ import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportTy
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.dungeon.DungeonData;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.entity.EntityTeam;
import emu.grasscutter.game.entity.EntityWorld;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.player.Player.SceneLoadState;
import emu.grasscutter.game.props.*;
import emu.grasscutter.game.props.EnterReason;
import emu.grasscutter.game.props.EntityIdType;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.game.world.data.TeleportProperties;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.*;
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHint;
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHintType;
import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType;
import emu.grasscutter.scripts.data.SceneConfig;
import emu.grasscutter.server.event.player.PlayerTeleportEvent;
@ -21,10 +26,20 @@ import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.ConversionUtils;
import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import java.util.concurrent.*;
import lombok.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.val;
import org.jetbrains.annotations.NotNull;
public class World implements Iterable<Player> {
@ -124,7 +139,7 @@ public class World implements Iterable<Player> {
* @param sceneId The scene ID.
* @return The scene.
*/
public Scene getSceneById(int sceneId) {
@Nullable public Scene getSceneById(int sceneId) {
// Get scene normally
var scene = this.getScenes().get(sceneId);
if (scene != null) {
@ -152,7 +167,7 @@ public class World implements Iterable<Player> {
* @param idType The entity type.
* @return The next entity ID.
*/
public int getNextEntityId(EntityIdType idType) {
public synchronized int getNextEntityId(EntityIdType idType) {
return (idType.getId() << 24) + ++this.nextEntityId;
}

View File

@ -46,6 +46,7 @@ public class SceneScriptManager {
/** current triggers controlled by RefreshGroup */
private final Map<Integer, Set<SceneTrigger>> currentTriggers;
private final Set<SceneTrigger> ongoingTriggers;
private final Map<String, Set<SceneTrigger>> triggersByGroupScene;
private final Map<Integer, Set<Pair<String, Integer>>> activeGroupTimers;
private final Map<String, AtomicInteger> triggerInvocations;
@ -76,6 +77,7 @@ public class SceneScriptManager {
public SceneScriptManager(Scene scene) {
this.scene = scene;
this.currentTriggers = new ConcurrentHashMap<>();
this.ongoingTriggers = ConcurrentHashMap.newKeySet();
this.triggersByGroupScene = new ConcurrentHashMap<>();
this.activeGroupTimers = new ConcurrentHashMap<>();
this.triggerInvocations = new ConcurrentHashMap<>();
@ -264,6 +266,15 @@ public class SceneScriptManager {
this.addGroupSuite(groupInstance, suiteData, entitiesAdded);
// refreshGroup may be called by a trigger.
// If that trigger has been refreshed, ensure it does not get
// deregistered anyway when the trigger completes its invocation.
for (var triggerSet : currentTriggers.values()) {
var toSave = new HashSet<SceneTrigger>(triggerSet);
toSave.retainAll(ongoingTriggers);
toSave.forEach(t -> t.setPreserved(true));
}
// Refesh variables here
group.variables.forEach(
variable -> {
@ -925,6 +936,7 @@ public class SceneScriptManager {
private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time
ongoingTriggers.add(trigger);
var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params);
var invocationsCounter = triggerInvocations.get(trigger.getName());
var invocations = invocationsCounter.incrementAndGet();
@ -956,11 +968,15 @@ public class SceneScriptManager {
}
// always deregister on error, otherwise only if the count is reached
if (ret.isboolean() && !ret.checkboolean()
// or the trigger should be preserved after a RefreshGroup call
if (trigger.isPreserved()) {
trigger.setPreserved(false);
} else if (ret.isboolean() && !ret.checkboolean()
|| ret.isint() && ret.checkint() != 0
|| trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) {
deregisterTrigger(trigger);
}
ongoingTriggers.remove(trigger);
}
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
@ -1053,18 +1069,7 @@ public class SceneScriptManager {
}
// Calculate level
int level = monster.level;
if (getScene().getDungeonManager() != null) {
level = getScene().getDungeonManager().getLevelForMonster(monster.config_id);
} else if (getScene().getWorld().getWorldLevel() > 0) {
var worldLevelData =
GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel());
if (worldLevelData != null) {
level = worldLevelData.getMonsterLevel();
}
}
int level = getScene().getLevelForMonster(monster.config_id, monster.level);
// Spawn mob
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level);
@ -1104,6 +1109,19 @@ public class SceneScriptManager {
return meta.sceneBlockIndex;
}
public void removeMonstersInGroup(SceneGroup group) {
var configSet =
group.monsters.values().stream().map(m -> m.config_id).collect(Collectors.toSet());
var toRemove =
getScene().getEntities().values().stream()
.filter(e -> e instanceof EntityMonster)
.filter(e -> e.getGroupId() == group.id)
.filter(e -> configSet.contains(e.getConfigId()))
.toList();
getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
}
public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet());
var toRemove =

View File

@ -201,7 +201,7 @@ public class ScriptLib {
}
var towerManager = scene.getPlayers().get(0).getTowerManager();
if (towerManager.isInProgress()) {
if (towerManager.isInProgress() && towerManager.getCurrentTimeLimit() > 0) {
// Tower scripts call ActiveChallenge twice in mirror stages.
// The second call provides the time _taken_ in the first stage,
// not the actual time limit for the challenge.

View File

@ -17,6 +17,7 @@ public final class SceneTrigger {
private String tag;
public transient SceneGroup currentGroup;
private boolean preserved;
@Override
public boolean equals(Object obj) {

View File

@ -62,7 +62,11 @@ public final class ScriptMonsterTideService {
public SceneMonster getNextMonster() {
var nextId = this.monsterConfigOrders.poll();
if (currentGroup.monsters.containsKey(nextId)) {
if (nextId == null) {
// AutoMonsterTide has been called with fewer monster config IDs than the total tide count.
// Get last config ID from the list, then.
return currentGroup.monsters.get(monsterConfigIds.get(monsterConfigIds.size() - 1));
} else if (currentGroup.monsters.containsKey(nextId)) {
return currentGroup.monsters.get(nextId);
}
// TODO some monster config_id do not exist in groups, so temporarily set it to the first

View File

@ -21,7 +21,6 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
// Handle skill notify in other managers.
player.getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId);
player.getEnergyManager().handleEvtDoSkillSuccNotify(session, skillId, casterId);
player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_SKILL, skillId);
}
}

View File

@ -1,9 +1,11 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.HomeChangeModuleReqOuterClass;
import emu.grasscutter.net.proto.HomeChangeModuleReqOuterClass.HomeChangeModuleReq;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketHomeAvatarTalkFinishInfoNotify;
@ -16,12 +18,20 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
HomeChangeModuleReqOuterClass.HomeChangeModuleReq req =
HomeChangeModuleReqOuterClass.HomeChangeModuleReq.parseFrom(payload);
var req = HomeChangeModuleReq.parseFrom(payload);
var homeWorld = session.getPlayer().getCurHomeWorld();
if (!homeWorld.getGuests().isEmpty()) {
session.send(new PacketHomeChangeModuleRsp());
session.send(new PacketHomeChangeModuleRsp(Retcode.RET_HOME_HAS_GUEST));
return;
}
int realmId = 2000 + req.getTargetModuleId();
var scene = homeWorld.getSceneById(realmId);
if (scene == null) {
Grasscutter.getLogger().warn("scene == null! Changing module will fail.");
session.send(new PacketHomeChangeModuleRsp(Retcode.RET_INVALID_SCENE_ID));
return;
}
@ -31,8 +41,6 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer()));
session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
int realmId = 2000 + req.getTargetModuleId();
var scene = homeWorld.getSceneById(realmId);
var pos = scene.getScriptManager().getConfig().born_pos;
homeWorld.transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);

View File

@ -34,8 +34,7 @@ public class HandlerHomeSceneJumpReq extends PacketHandler {
pos = home.getSceneMap().get(realmId).getBornPos();
}
world.transferPlayerToScene(
session.getPlayer(), req.getIsEnterRoomScene() ? homeScene.getRoomSceneId() : realmId, pos);
world.transferPlayerToScene(session.getPlayer(), scene.getId(), pos);
session.send(new PacketHomeSceneJumpRsp(req.getIsEnterRoomScene()));
}

View File

@ -2,28 +2,23 @@ package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.HomeChangeModuleRspOuterClass;
import emu.grasscutter.net.proto.RetcodeOuterClass;
import emu.grasscutter.net.proto.HomeChangeModuleRspOuterClass.HomeChangeModuleRsp;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public class PacketHomeChangeModuleRsp extends BasePacket {
public PacketHomeChangeModuleRsp(int targetModuleId) {
super(PacketOpcodes.HomeChangeModuleRsp);
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp proto =
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp.newBuilder()
.setRetcode(0)
.setTargetModuleId(targetModuleId)
.build();
var proto =
HomeChangeModuleRsp.newBuilder().setRetcode(0).setTargetModuleId(targetModuleId).build();
this.setData(proto);
}
public PacketHomeChangeModuleRsp() {
public PacketHomeChangeModuleRsp(Retcode retcode) {
super(PacketOpcodes.HomeChangeModuleRsp);
this.setData(
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp.newBuilder()
.setRetcode(RetcodeOuterClass.Retcode.RET_HOME_HAS_GUEST_VALUE));
this.setData(HomeChangeModuleRsp.newBuilder().setRetcode(retcode.getNumber()));
}
}

View File

@ -1,10 +1,13 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.home.HomeBlockItem;
import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.*;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.HomeMarkPointNotifyOuterClass.HomeMarkPointNotify;
import emu.grasscutter.net.proto.HomeMarkPointSceneDataOuterClass.HomeMarkPointSceneData;
import java.util.Collection;
import java.util.Set;
@ -13,17 +16,24 @@ public class PacketHomeMarkPointNotify extends BasePacket {
public PacketHomeMarkPointNotify(Player player) {
super(PacketOpcodes.HomeMarkPointNotify);
var proto = HomeMarkPointNotifyOuterClass.HomeMarkPointNotify.newBuilder();
var proto = HomeMarkPointNotify.newBuilder();
var world = player.getCurHomeWorld();
var owner = world.getHost();
var home = world.getHome();
if (owner.getRealmList() == null) {
if (owner.getRealmList() == null || owner.getRealmList().isEmpty()) {
return;
}
// send current home mark points.
var moduleId = owner.getCurrentRealmId();
var scene = world.getSceneById(moduleId + 2000);
if (scene == null) {
Grasscutter.getLogger()
.warn(
"Current Realm id is invalid! SceneExcelConfigData.json, game resource not loaded correctly or the realm id maybe wrong?!");
return;
}
var homeScene = home.getHomeSceneItem(moduleId + 2000);
var mainHouse = home.getMainHouseItem(moduleId + 2000);
@ -31,12 +41,12 @@ public class PacketHomeMarkPointNotify extends BasePacket {
.forEach(
homeSceneItem -> {
var markPointData =
HomeMarkPointSceneDataOuterClass.HomeMarkPointSceneData.newBuilder()
HomeMarkPointSceneData.newBuilder()
.setModuleId(moduleId)
.setSceneId(homeSceneItem.getSceneId());
if (!homeSceneItem.isRoom()) {
var config = world.getSceneById(moduleId + 2000).getScriptManager().getConfig();
var config = scene.getScriptManager().getConfig();
markPointData
.setSafePointPos(
config == null

View File

@ -0,0 +1,18 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.MonsterSummonTagNotifyOuterClass.MonsterSummonTagNotify;
import java.util.*;
public class PacketMonsterSummonTagNotify extends BasePacket {
public PacketMonsterSummonTagNotify(EntityMonster monsterEntity) {
super(PacketOpcodes.MonsterSummonTagNotify);
var proto = MonsterSummonTagNotify.newBuilder().setMonsterEntityId(monsterEntity.getId());
monsterEntity.getSummonTagMap().forEach((k, v) -> proto.putSummonTagMap(k, v == null ? 0 : 1));
this.setData(proto.build());
}
}