9 Commits

130 changed files with 2806 additions and 4291 deletions

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 - 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. - Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
- After opening Cultivation (as admin), press the download button in the upper right corner. - After opening Culivation (as admin), press the download button in the upper right corner.
- Click `Download All-in-One` - Click `Download All-in-One`
- Click the gear in the upper right corner - Click the gear in the upper right corner
- Set the game Install path to where your game is located. - 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 targetCompatibility = JavaVersion.VERSION_17
group = 'io.grasscutter' group = 'io.grasscutter'
version = '1.7.4' version = '1.7.1'
java { java {
withJavadocJar() withJavadocJar()

View File

@ -3,65 +3,81 @@
<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> <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) | [简中](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) [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)
**Attention:** 私たちはプロジェクトへのコントリビュータをいつでも歓迎します。コントリビュートする前に、私たちの [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。 **:** 私たちはプロジェクトへの貢献者をいつでも歓迎します。貢献を追加する前に、我々の [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
## 現在実装されている機能 ## 現在機能している
* ログイン * ログイン
* 戦闘 * 戦闘
* フレンドリスト * フレンドリスト
* テレポート * テレポート
* 祈願 (ガチャ) * 祈願(ガチャ)
* マルチプレイ (一部) * マルチプレイは一部機能しています
* コンソールを通したモンスタースポーン * コンソールを使用してモンスタースポーンさせる
* インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど) * インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど)
## かんたんセットアップガイド ## クイックセットアップガイド
**Note:** サポートが必要な場合はGrasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加してください。 **:** サポートが必要な場合は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`)
- その他の設定には手を付けず次の段階に進む。
- Launch の隣にある小さいボタンを押す **:** サーバーを動作させるだけならjreのみで十分です。 開発をしたい場合JDKが必要になるかもしれません
- 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 Kit 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) - [Java SE Development Kits - 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download) (任意、ハンドブックの生成に必要)
##### Clone ##### Windows
```shell
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
```
##### Compile
**Note:** 環境によってはハンドブックの生成が失敗する場合があります。ハンドブックの生成をさせない場合は `gradlew jar` コマンドに `-PskipHandbook=1` を付け加えてください。
Windows:
```shell ```shell
git clone https://github.com/Grasscutters/Grasscutter.git git clone https://github.com/Grasscutters/Grasscutter.git
@ -70,7 +86,7 @@ cd Grasscutter
.\gradlew jar # コンパイル .\gradlew jar # コンパイル
``` ```
Linux: ##### Linux
```bash ```bash
git clone https://github.com/Grasscutters/Grasscutter.git git clone https://github.com/Grasscutters/Grasscutter.git
@ -79,8 +95,13 @@ chmod +x gradlew
./gradlew jar # コンパイル ./gradlew jar # コンパイル
``` ```
生成されたjarファイルはプロジェクトフォルダのルートにります。 生成されたjarファイルはプロジェクトフォルダのルートにります。
### トラブルシューティング ### コマンドリストは[wiki](https://github.com/Grasscutters/Grasscutter/wiki/Commands)へ移動しました。
よく散見されるトラブルとそれに対する解決策のまとめリストや、質問し誰かの助けを得たい場合は、Grasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加し、サポートチャンネルを参照してください。 # トラブルシューティング
* コンパイルが失敗した場合JDKがインストールされているか確認してください。(JDKのバージョンが17以降であることと、環境変数でJDKのパスが設定されている必要があります)
* クライアントが接続できない・ログインできない・エラーコード4206・またその他場合、ほとんどは、プロキシデーモンの設定が問題です。Fiddlerを使っている場合はデフォルトポートを8888以外の別のポートに変更してみてください。
Fiddlerを使用している場合はポートが8888以外に設定されていることを確認してください。
* 起動シーケンス(順番): MongoDB > Grasscutter > プロキシツール (mitmdumpかfiddler、その他) > ゲーム

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) - 获取游戏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”为后缀的安装包 - 下载[最新的Cultivation版本](https://github.com/Grasscutters/Cultivation/releases/latest)(使用以“.msi”为后缀的安装包
- 以管理员身份打开Cultivation按右上角的下载按钮。 - 以管理员身份打开Culivation按右上角的下载按钮。
- 点击“下载 Grasscutter 一体化” - 点击“下载 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 客戶端) - 下載遊戲版本 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` 安裝程式。 - 下載 [最新的 Cultivation 版本](https://github.com/Grasscutters/Cultivation/releases/latest)。使用 `.msi` 安裝程式。
- 以管理員身分打開 Cultivation按右上角的下載按鈕。 - 以管理員身分打開 Culivation按右上角的下載按鈕。
- 點擊 `Download All-in-One` - 點擊 `Download All-in-One`
- 點擊右上角的齒輪 - 點擊右上角的齒輪
- 將遊戲安裝路徑設置為你的遊戲所在的位置。 - 將遊戲安裝路徑設置為你的遊戲所在的位置。

View File

@ -1,188 +0,0 @@
# Hide and Seek!
Documentation on how the **Hide and Seek** game works.\
Externally dubbed: `Windtrace`.
# Map IDs
TODO: Document the map IDs of Windtrace.
TODO: Investigate `ServerGlobalValueChangeNotify`
# Asking Players to Play in a Co-Op Game
1. The client will send `DraftOwnerStartInviteReq`
2. The server will send `DraftOwnerInviteNotify` to all clients.
3. The server will send `DraftOwnerStartInviteRsp`
# Matching in a Co-Op Game
1. World owner talks to Gygax and begins a Windtrace game.
2. The packet `DraftOwnerInviteNotify` is sent to clients.
3. Clients will respond with `DraftGuestReplyInviteReq` (client-side)
4. The server will respond with `DraftGuestReplyInviteRsp`
5. The server will respond with `DraftInviteResultNotify`
# Starting Windtrace
1. If `DraftInviteResultNotify` is a success, the server will send a series of packets.
1. A series of `SceneEntityAppearNotify` packets.
2. `NpcTalkStateNotify`
3. `PlayerEnterSceneNotify`
4. `MultistagePlayInfoNotify`
2. The players are then teleported to the Windtrace map in their locations.
3. Server will send packets to clients. (this is server boilerplate)
4. The server sends another `MultistagePlayInfoNotify` to clients.
# Changing Avatars - Others
1. The server will send a `AvatarEquipChangeNotify` packet to clients.
2. The server will send a `SceneTeamUpdateNotify` packet to clients.
3. The server will send a `HideAndSeekPlayerSetAvatarNotify` packet to clients.
# Getting Ready
1. The client will send `HideAndSeekSetReadyReq` to the server.
2. The server will reply with `HideAndSeekPlayerReadyNotify` to clients.
3. The server will send `MultistagePlayInfoNotify` to clients.
4. The server will reply with `HideAndSeekSetReadyRsp` to the client.
5. If all players are ready, the server will move on to start Windtrace.
# Starting Windtrace
1. When all players are ready, the server will send a series of packets to players.
1. `GalleryStartNotify`
2. `SceneGalleryInfoNotify`
3. `MultistagePlayInfoNotify`
4. `MultistagePlayStageEndNotify`
5. This will only get sent at the `1.` countdown.
### Notes:
- `GuestReplyInviteRsp` is sent **after** `DraftInviteResultNotify`.
## `DraftOwnerInviteNotify`
- `invite_deadline_time` - This is the time when the invite expires.
- `draft_id` - The value is always `3001` for Windtrace.
## `DraftOwnerStartInviteReq`
- `draft_id` - The value is always `3001` for Windtrace.
## `DraftOwnerStartInviteRsp`
- `draft_id` - The value is always `3001` for Windtrace.
- `invite_fail_info_list` - A list of players who weren't invited.
- `retcode` - The response code.
- `wrong_uid` - Always `0`. (undocumented)
## `DraftGuestReplyInviteReq`
- `draft_id` - The value is always `3001` for Windtrace.
- `is_agree` - A boolean value for whether the client accepts the invite.
## `DraftGuestReplyInviteRsp`
- `draft_id` - The value is always `3001` for Windtrace.
- `retcode` - Response code for the request.
- `is_agree` - A boolean value for whether the server acknowledges the client's invite acceptation.
## `DraftInviteResultNotify`
- `draft_id` - The value is always `3001` for Windtrace.
- `is_all_agree` - A boolean value for whether all clients accepted the invite.
## `NpcTalkStateNotify`
- `is_ban` - This value is always true when entering Windtrace.
## `PlayerEnterSceneNotify`
- `pos` - This is where the player will be teleported to.
- This value depends on if the player is a hunter or a runner.
- This value is set by the server and must be hardcoded/read from a JSON file.
## `MultistagePlayStageEndNotify`
- `play_index` - Value picked by the server. (use 1)
- `group_id` - This value is always `133002121` for Windtrace.
## `MultistagePlayInfoNotify` - Initial + PostEnterSceneReq
- Image Reference: ![img.png](images/multistageplayinfo.png)
- `info` - MultistagePlayInfo data.
- `group_id` - The value is always `133002121` for Windtrace.
- `play_index` - Value picked by the server. (use 1)
- `hide_and_seek_info` - Information about Windtrace.
- `hider_uid_list` - A list of UIDs (ints) of the hiders.
- `hunter_uid` - The UID (int) of the hunter.
- `map_id` - The ID of the Windtrace map.
- `stage_type` - Windtrace state.
- This will be `HIDE_AND_SEEK_STAGE_TYPE_PREPARE`.
- `battle_info_map` - Contains a dictionary of UID -> `HideAndSeekPlayerBattleInfo` objects.
- `skill_list` - Array of 3 values of skill IDs chosen by the player.
- `avatar_id` - The ID of the avatar the player wants to use.
- `is_ready` - The player's in-game ready state.
- `costume_id` - The costume the player's avatar is wearing.
## `MultistagePlayInfoNotify` - Picking Avatars
- Image Reference: ![img.png](images/pickavatar.png)
- **Note:** This packet matches the initial structure and data.
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_PICK`.
## `MultistagePlayInfoNotify` - Starting Windtrace
- Image Reference: ![img.png](images/startwindtrace.png)
- **Note:** This packet matches the initial structure and data.
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_HIDE`.
## `MultistagePlayInfoNotify` - Seeking Time
- Image Reference: ![img.png](images/seektime.png)
- **Note:** This packet matches the initial structure and data.
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_SEEK`.
## `MultistagePlayInfoNotify` - Finish Windtrace
- Image Reference: ![img.png](images/seektime.png)
- **Note:** This packet matches the initial structure and data.
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_SETTLE`.
## `HideAndSeekPlayerSetAvatarNotify`
- `avatar_id` - The ID of the new avatar the player wants to use.
- `uid` - The UID of the player who changed their avatar.
- `costume_id` - The costume the player's avatar is wearing.
## `HideAndSeekSetReadyRsp`
- `retcode` - Response code for the request.
## `HideAndSeekPlayerReadyNotify`
- `uid_list` - A list of UIDs (ints) of the players who are ready.
## `GalleryStartNotify`
- `gallery_id` - TODO: Check if this value is always `7056` for Windtrace.
- `start_time` - This value is always `2444` for Windtrace.
- This value is `200` when displaying game end statistics.
- `owner_uid` - The UID of the player who started the Windtrace game.
- `player_count` - The number of players in the Windtrace game.
- `end_time` - This value is always the same as `start_time`.
## `SceneGalleryInfoNotify` - Starting Windtrace
- `gallery_info` - SceneGalleryInfo data.
- `end_time` - This value is always the same as `start_time`.
- `start_time` - This value is always `2444` for Windtrace.
- This value is `200` when displaying game end statistics.
- `gallery_id` - This value is always the same as `gallery_id` from `GalleryStartNotify`.
- `stage` - The current stage of the gallery.
- This will be `GALLERY_STAGE_TYPE_START`.
- `owner_uid` - The UID of the player who started the Windtrace game.
- `hide_and_seek_info` - SceneGalleryHideAndSeekInfo
- `visible_uid_list` - List of UIDs (ints) of the players who were left alive.
- `caught_uid_list` - List of UIDs (ints) of the players who have been caught.
- `player_count` - The amount of players in the Windtrace game.
- `pre_start_end_time` - This value is always `0` for Windtrace.
## `HideAndSeekSettleNotify`
- `reason` - The reason for the game ending.
- `winner_list` - A list of UIDs (ints) of the players who won the game.
- `settle_info_list` - HideAndSeekSettleInfo data.
- This is a list of players who participated in the game.
## `HideAndSeekSettleInfo`
- `card_list` - A collection of `ExhibitionDisplayInfo`
- If unknown: hardcode the specified values. ![img.png](images/defaultexhibitioninfo.png)
- These values are repeated during testing.
- `uid` - The UID of the player who participated in the game.
- `nickname` - The player's nickname.
- `head_image` - This value is always `0`.
- `online_id` - This value is always blank.
- `profile_picture` - `ProfilePicture` object.
- `play_index` - Value picked by the server. (use 1)
- `stage_type` - The stage type. (inconclusive; TODO)
- `cost_time` - The amount of time the player took to complete the game.
- `score_list` - A list of player scores.
## `ExhibitionDisplayInfo`
- `id` - The ID of the reward.
- `param` - The amount of the reward given.
- `detail_param` - This value is *mostly* 0.
- This value **matches** param when the reward is of the amount of time spent playing. (participation reward)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@ -68,7 +68,7 @@ class MlgmXyysd_Animation_Company_Proxy:
] ]
def request(self, flow: http.HTTPFlow) -> None: def request(self, flow: http.HTTPFlow) -> None:
if flow.request.pretty_host in self.LIST_DOMAINS: if flow.request.host in self.LIST_DOMAINS:
if USE_SSL: if USE_SSL:
flow.request.scheme = "https" flow.request.scheme = "https"
else: else:

View File

@ -1,8 +1,5 @@
package emu.grasscutter; package emu.grasscutter;
import static emu.grasscutter.config.Configuration.SERVER;
import static emu.grasscutter.utils.lang.Language.translate;
import ch.qos.logback.classic.*; import ch.qos.logback.classic.*;
import emu.grasscutter.auth.*; import emu.grasscutter.auth.*;
import emu.grasscutter.command.*; import emu.grasscutter.command.*;
@ -21,16 +18,20 @@ import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.lang.Language; import emu.grasscutter.utils.lang.Language;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import java.io.*;
import java.util.Calendar;
import java.util.concurrent.*;
import javax.annotation.Nullable;
import lombok.*; import lombok.*;
import org.jline.reader.*; import org.jline.reader.*;
import org.jline.terminal.*; import org.jline.terminal.*;
import org.reflections.Reflections; import org.reflections.Reflections;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.*;
import java.util.Calendar;
import java.util.concurrent.*;
import static emu.grasscutter.config.Configuration.SERVER;
import static emu.grasscutter.utils.lang.Language.translate;
public final class Grasscutter { public final class Grasscutter {
public static final File configFile = new File("./config.json"); public static final File configFile = new File("./config.json");
public static final Reflections reflector = new Reflections("emu.grasscutter"); public static final Reflections reflector = new Reflections("emu.grasscutter");
@ -158,8 +159,6 @@ public final class Grasscutter {
// Generate handbooks. // Generate handbooks.
Tools.createGmHandbooks(false); Tools.createGmHandbooks(false);
// Generate gacha mappings.
Tools.generateGachaMappings();
} }
// Start servers. // Start servers.
@ -183,12 +182,18 @@ public final class Grasscutter {
// Hook into shutdown event. // Hook into shutdown event.
Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown));
// Start database heartbeat.
Database.startSaveThread();
// Open console. // Open console.
Grasscutter.startConsole(); Grasscutter.startConsole();
} }
/** Server shutdown event. */ /** Server shutdown event. */
private static void onShutdown() { private static void onShutdown() {
// Save all data.
Database.saveAll();
// Disable all plugins. // Disable all plugins.
if (pluginManager != null) pluginManager.disablePlugins(); if (pluginManager != null) pluginManager.disablePlugins();
// Shutdown the game server. // Shutdown the game server.
@ -198,14 +203,14 @@ public final class Grasscutter {
// Wait for Grasscutter's thread pool to finish. // Wait for Grasscutter's thread pool to finish.
var executor = Grasscutter.getThreadPool(); var executor = Grasscutter.getThreadPool();
executor.shutdown(); executor.shutdown();
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
executor.shutdownNow(); executor.shutdownNow();
} }
// Wait for database operations to finish. // Wait for database operations to finish.
var dbExecutor = DatabaseHelper.getEventExecutor(); var dbExecutor = DatabaseHelper.getEventExecutor();
dbExecutor.shutdown(); dbExecutor.shutdown();
if (!dbExecutor.awaitTermination(5, TimeUnit.SECONDS)) { if (!dbExecutor.awaitTermination(2, TimeUnit.MINUTES)) {
dbExecutor.shutdownNow(); dbExecutor.shutdownNow();
} }
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {

View File

@ -1,6 +1,5 @@
package emu.grasscutter.command; package emu.grasscutter.command;
import emu.grasscutter.game.world.Position;
import java.util.*; import java.util.*;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.regex.*; import java.util.regex.*;
@ -55,78 +54,4 @@ public class CommandHelpers {
}); });
return args; return args;
} }
public static float parseRelative(String input, Float current) {
if (input.contains("~")) { // Relative
if (!input.equals("~")) { // Relative with offset
current += Float.parseFloat(input.replace("~", ""));
} // Else no offset, no modification
} else { // Absolute
current = Float.parseFloat(input);
}
return current;
}
public static Position parsePosition(
String inputX, String inputY, String inputZ, Position curPos, Position curRot) {
Position offset = new Position();
Position target = new Position(curPos);
if (inputX.contains("~")) { // Relative
if (!inputX.equals("~")) { // Relative with offset
target.addX(Float.parseFloat(inputX.replace("~", "")));
}
} else if (inputX.contains("^")) {
if (!inputX.equals("^")) {
offset.setX(Float.parseFloat(inputX.replace("^", "")));
}
} else { // Absolute
target.setX(Float.parseFloat(inputX));
}
if (inputY.contains("~")) { // Relative
if (!inputY.equals("~")) { // Relative with offset
target.addY(Float.parseFloat(inputY.replace("~", "")));
}
} else if (inputY.contains("^")) {
if (!inputY.equals("^")) {
offset.setY(Float.parseFloat(inputY.replace("^", "")));
}
} else { // Absolute
target.setY(Float.parseFloat(inputY));
}
if (inputZ.contains("~")) { // Relative
if (!inputZ.equals("~")) { // Relative with offset
target.addZ(Float.parseFloat(inputZ.replace("~", "")));
}
} else if (inputZ.contains("^")) {
if (!inputZ.equals("^")) {
offset.setZ(Float.parseFloat(inputZ.replace("^", "")));
}
} else { // Absolute
target.setZ(Float.parseFloat(inputZ));
}
if (!offset.equal3d(Position.ZERO)) {
return calculateOffset(target, curRot, offset);
} else {
return target;
}
}
public static Position calculateOffset(Position pos, Position rot, Position offset) {
// Degrees to radians
float angleZ = (float) Math.toRadians(rot.getY());
float angleX = (float) Math.toRadians(rot.getY() + 90);
// Calculate offset based on current position and rotation
return new Position(
pos.getX()
+ offset.getZ() * (float) Math.sin(angleZ)
+ offset.getX() * (float) Math.sin(angleX),
pos.getY() + offset.getY(),
pos.getZ()
+ offset.getZ() * (float) Math.cos(angleZ)
+ offset.getX() * (float) Math.cos(angleX));
}
} }

View File

@ -32,12 +32,9 @@ public final class DebugCommand implements CommandHandler {
var scene = sender.getScene(); var scene = sender.getScene();
var entityId = Integer.parseInt(args.get(0)); var entityId = Integer.parseInt(args.get(0));
// TODO Might want to allow groupId specification,
// because there can be more than one entity with
// the given config ID.
var entity = var entity =
args.size() > 1 && args.get(1).equals("config") args.size() > 1 && args.get(1).equals("config")
? scene.getFirstEntityByConfigId(entityId) ? scene.getEntityByConfigId(entityId)
: scene.getEntityById(entityId); : scene.getEntityById(entityId);
if (entity == null) { if (entity == null) {
sender.dropMessage("Entity not found."); sender.dropMessage("Entity not found.");

View File

@ -51,10 +51,7 @@ public final class EntityCommand implements CommandHandler {
} }
param.scene = targetPlayer.getScene(); param.scene = targetPlayer.getScene();
// TODO Might want to allow groupId specification, var entity = param.scene.getEntityByConfigId(param.configId);
// because there can be more than one entity with
// the given config ID.
var entity = param.scene.getFirstEntityByConfigId(param.configId);
if (entity == null) { if (entity == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.entity.not_found_error")); CommandHandler.sendMessage(sender, translate(sender, "commands.entity.not_found_error"));

View File

@ -12,7 +12,7 @@ import lombok.val;
@Command( @Command(
label = "setSceneTag", label = "setSceneTag",
aliases = {"tag"}, aliases = {"tag"},
usage = {"<add|remove|unlockall|reset> <sceneTagId>"}, usage = {"<add|remove|unlockall> <sceneTagId>"},
permission = "player.setscenetag", permission = "player.setscenetag",
permissionTargeted = "player.setscenetag.others") permissionTargeted = "player.setscenetag.others")
public final class SetSceneTagCommand implements CommandHandler { public final class SetSceneTagCommand implements CommandHandler {
@ -20,7 +20,7 @@ public final class SetSceneTagCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, Player targetPlayer, List<String> args) { public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.isEmpty()) { if (args.size() == 0) {
sendUsageMessage(sender); sendUsageMessage(sender);
return; return;
} }
@ -39,9 +39,6 @@ public final class SetSceneTagCommand implements CommandHandler {
if (actionStr.equals("unlockall")) { if (actionStr.equals("unlockall")) {
unlockAllSceneTags(targetPlayer); unlockAllSceneTags(targetPlayer);
return; return;
} else if (actionStr.equals("reset") || actionStr.equals("restore")) {
resetAllSceneTags(targetPlayer);
return;
} else { } else {
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error"); CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
return; return;
@ -52,7 +49,7 @@ public final class SetSceneTagCommand implements CommandHandler {
var sceneData = var sceneData =
sceneTagData.values().stream().filter(sceneTag -> sceneTag.getId() == userVal).findFirst(); sceneTagData.values().stream().filter(sceneTag -> sceneTag.getId() == userVal).findFirst();
if (sceneData.isEmpty()) { if (sceneData == null) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id"); CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id");
return; return;
} }
@ -83,15 +80,15 @@ public final class SetSceneTagCommand implements CommandHandler {
.toList() .toList()
.forEach( .forEach(
sceneTag -> { sceneTag -> {
targetPlayer if (targetPlayer.getSceneTags().get(sceneTag.getSceneId()) == null) {
.getSceneTags() targetPlayer.getSceneTags().put(sceneTag.getSceneId(), new HashSet<>());
.computeIfAbsent(sceneTag.getSceneId(), k -> new HashSet<>()); }
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId()); targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
}); });
// Remove default SceneTags, as most are "before" or "locked" states // Remove default SceneTags, as most are "before" or "locked" states
allData.stream() allData.stream()
.filter(SceneTagData::isDefaultValid) .filter(sceneTag -> sceneTag.isDefaultValid())
// Only remove for big world as some other scenes only have defaults // Only remove for big world as some other scenes only have defaults
.filter(sceneTag -> sceneTag.getSceneId() == 3) .filter(sceneTag -> sceneTag.getSceneId() == 3)
.forEach( .forEach(
@ -102,22 +99,6 @@ public final class SetSceneTagCommand implements CommandHandler {
this.setSceneTags(targetPlayer); this.setSceneTags(targetPlayer);
} }
private void resetAllSceneTags(Player targetPlayer) {
targetPlayer.getSceneTags().clear();
// targetPlayer.applyStartingSceneTags(); // private
GameData.getSceneTagDataMap().values().stream()
.filter(SceneTagData::isDefaultValid)
.forEach(
sceneTag -> {
targetPlayer
.getSceneTags()
.computeIfAbsent(sceneTag.getSceneId(), k -> new HashSet<>());
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
});
this.setSceneTags(targetPlayer);
}
private void setSceneTags(Player targetPlayer) { private void setSceneTags(Player targetPlayer) {
targetPlayer.sendPacket(new PacketPlayerWorldSceneInfoListNotify(targetPlayer)); targetPlayer.sendPacket(new PacketPlayerWorldSceneInfoListNotify(targetPlayer));
} }

View File

@ -21,9 +21,9 @@ import lombok.Setter;
label = "spawn", label = "spawn",
aliases = {"drop", "s"}, aliases = {"drop", "s"},
usage = { usage = {
"<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] [<x> <y> <z>] [<rotX> <rotY> <rotZ>]", "<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] [<x> <y> <z>] [<rotX> <rotY> <rotZ>]", "<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] [<x> <y> <z>] [<rotX> <rotY> <rotZ>]" "<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>"
}, },
permission = "server.spawn", permission = "server.spawn",
permissionTargeted = "server.spawn.others") permissionTargeted = "server.spawn.others")
@ -53,23 +53,14 @@ public final class SpawnCommand implements CommandHandler {
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
Position pos = new Position(targetPlayer.getPosition());
Position rot = new Position(targetPlayer.getRotation());
switch (args.size()) { switch (args.size()) {
case 7:
try {
rot.setX(CommandHelpers.parseRelative(args.get(4), rot.getX()));
rot.setY(CommandHelpers.parseRelative(args.get(5), rot.getY()));
rot.setZ(CommandHelpers.parseRelative(args.get(6), rot.getZ()));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.execution.argument_error"));
} // Fallthrough
case 4: case 4:
try { try {
pos = CommandHelpers.parsePosition(args.get(1), args.get(2), args.get(3), pos, rot); float x, y, z;
x = Float.parseFloat(args.get(1));
y = Float.parseFloat(args.get(2));
z = Float.parseFloat(args.get(3));
param.pos = new Position(x, y, z);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
CommandHandler.sendMessage( CommandHandler.sendMessage(
sender, translate(sender, "commands.execution.argument_error")); sender, translate(sender, "commands.execution.argument_error"));
@ -86,8 +77,6 @@ public final class SpawnCommand implements CommandHandler {
sendUsageMessage(sender); sendUsageMessage(sender);
return; return;
} }
param.pos = pos;
param.rot = rot;
MonsterData monsterData = GameData.getMonsterDataMap().get(param.id); MonsterData monsterData = GameData.getMonsterDataMap().get(param.id);
GadgetData gadgetData = GameData.getGadgetDataMap().get(param.id); GadgetData gadgetData = GameData.getGadgetDataMap().get(param.id);
@ -113,8 +102,12 @@ public final class SpawnCommand implements CommandHandler {
} }
double maxRadius = Math.sqrt(param.amount * 0.2 / Math.PI); double maxRadius = Math.sqrt(param.amount * 0.2 / Math.PI);
if (param.pos == null) {
param.pos = targetPlayer.getPosition();
}
for (int i = 0; i < param.amount; i++) { for (int i = 0; i < param.amount; i++) {
pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3); Position pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3);
GameEntity entity = null; GameEntity entity = null;
if (itemData != null) { if (itemData != null) {
entity = createItem(itemData, param, pos); entity = createItem(itemData, param, pos);
@ -135,12 +128,12 @@ public final class SpawnCommand implements CommandHandler {
} }
private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) { private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) {
return new EntityItem(param.scene, null, itemData, pos, param.rot, 1, true); return new EntityItem(param.scene, null, itemData, pos, 1, true);
} }
private EntityMonster createMonster( private EntityMonster createMonster(
MonsterData monsterData, SpawnParameters param, Position pos) { MonsterData monsterData, SpawnParameters param, Position pos) {
var entity = new EntityMonster(param.scene, monsterData, pos, param.rot, param.lvl); var entity = new EntityMonster(param.scene, monsterData, pos, param.lvl);
if (param.ai != -1) { if (param.ai != -1) {
entity.setAiId(param.ai); entity.setAiId(param.ai);
} }
@ -151,13 +144,16 @@ public final class SpawnCommand implements CommandHandler {
GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) { GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) {
EntityBaseGadget entity; EntityBaseGadget entity;
if (gadgetData.getType() == EntityType.Vehicle) { if (gadgetData.getType() == EntityType.Vehicle) {
entity = new EntityVehicle(param.scene, targetPlayer, param.id, 0, pos, param.rot); entity =
new EntityVehicle(
param.scene, targetPlayer, param.id, 0, pos, targetPlayer.getRotation());
} else { } else {
entity = new EntityGadget(param.scene, param.id, pos, param.rot); entity = new EntityGadget(param.scene, param.id, pos, targetPlayer.getRotation());
if (param.state != -1) { if (param.state != -1) {
((EntityGadget) entity).setState(param.state); ((EntityGadget) entity).setState(param.state);
} }
} }
return entity; return entity;
} }
@ -211,7 +207,6 @@ public final class SpawnCommand implements CommandHandler {
@Setter public int def = -1; @Setter public int def = -1;
@Setter public int ai = -1; @Setter public int ai = -1;
@Setter public Position pos = null; @Setter public Position pos = null;
@Setter public Position rot = null;
public Scene scene = null; public Scene scene = null;
} }
} }

View File

@ -16,10 +16,24 @@ import java.util.List;
permissionTargeted = "player.teleport.others") permissionTargeted = "player.teleport.others")
public final class TeleportCommand implements CommandHandler { public final class TeleportCommand implements CommandHandler {
private float parseRelative(
String input, Float current) { // TODO: Maybe this will be useful elsewhere later
if (input.contains("~")) { // Relative
if (!input.equals("~")) { // Relative with offset
current += Float.parseFloat(input.replace("~", ""));
} // Else no offset, no modification
} else { // Absolute
current = Float.parseFloat(input);
}
return current;
}
@Override @Override
public void execute(Player sender, Player targetPlayer, List<String> args) { public void execute(Player sender, Player targetPlayer, List<String> args) {
Position pos = new Position(targetPlayer.getPosition()); Position pos = targetPlayer.getPosition();
Position rot = new Position(targetPlayer.getRotation()); float x = pos.getX();
float y = pos.getY();
float z = pos.getZ();
int sceneId = targetPlayer.getSceneId(); int sceneId = targetPlayer.getSceneId();
switch (args.size()) { switch (args.size()) {
@ -32,7 +46,9 @@ public final class TeleportCommand implements CommandHandler {
} // Fallthrough } // Fallthrough
case 3: case 3:
try { try {
pos = CommandHelpers.parsePosition(args.get(0), args.get(1), args.get(2), pos, rot); x = this.parseRelative(args.get(0), x);
y = this.parseRelative(args.get(1), y);
z = this.parseRelative(args.get(2), z);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
CommandHandler.sendMessage( CommandHandler.sendMessage(
sender, translate(sender, "commands.teleport.invalid_position")); sender, translate(sender, "commands.teleport.invalid_position"));
@ -43,10 +59,11 @@ public final class TeleportCommand implements CommandHandler {
return; return;
} }
Position target_pos = new Position(x, y, z);
boolean result = boolean result =
targetPlayer targetPlayer
.getWorld() .getWorld()
.transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, pos); .transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, target_pos);
if (!result) { if (!result) {
CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.exists_error")); CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.exists_error"));
@ -54,13 +71,7 @@ public final class TeleportCommand implements CommandHandler {
CommandHandler.sendMessage( CommandHandler.sendMessage(
sender, sender,
translate( translate(
sender, sender, "commands.teleport.success", targetPlayer.getNickname(), x, y, z, sceneId));
"commands.teleport.success",
targetPlayer.getNickname(),
pos.getX(),
pos.getY(),
pos.getZ(),
sceneId));
} }
} }
} }

View File

@ -1,6 +1,8 @@
package emu.grasscutter.data; package emu.grasscutter.data;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import java.io.*; import java.io.*;
import java.nio.file.*; import java.nio.file.*;
@ -112,6 +114,8 @@ public class DataLoader {
} catch (Exception e) { } catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while trying to check the data folder.", e); Grasscutter.getLogger().error("An error occurred while trying to check the data folder.", e);
} }
generateGachaMappings();
} }
private static void checkAndCopyData(String name) { private static void checkAndCopyData(String name) {
@ -127,4 +131,16 @@ public class DataLoader {
FileUtils.copyResource("/defaults/data/" + name, filePath.toString()); FileUtils.copyResource("/defaults/data/" + name, filePath.toString());
} }
} }
private static void generateGachaMappings() {
var path = GachaHandler.getGachaMappingsPath();
if (!Files.exists(path)) {
try {
Grasscutter.getLogger().debug("Creating default '" + path + "' data");
Tools.createGachaMappings(path);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to create gacha mappings. \n" + exception);
}
}
}
} }

View File

@ -214,14 +214,6 @@ public final class GameData {
private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap = private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap =
new Int2ObjectOpenHashMap<>(); new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CoopChapterData> coopChapterDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CoopPointData> coopPointDataMap =
new Int2ObjectOpenHashMap<>();
@Getter @Getter
private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>();
@ -302,10 +294,6 @@ public final class GameData {
private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap = private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap =
new Int2ObjectOpenHashMap<>(); new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeWorldModuleData> homeWorldModuleDataMap =
new Int2ObjectOpenHashMap<>();
@Getter @Getter
private static final Int2ObjectMap<HomeWorldNPCData> homeWorldNPCDataMap = private static final Int2ObjectMap<HomeWorldNPCData> homeWorldNPCDataMap =
new Int2ObjectOpenHashMap<>(); new Int2ObjectOpenHashMap<>();

View File

@ -42,7 +42,6 @@ public class AbilityModifier implements Serializable {
public String stacking; public String stacking;
public AbilityMixinData[] modifierMixins; public AbilityMixinData[] modifierMixins;
public AbilityModifierProperty properties;
public ElementType elementType; public ElementType elementType;
public DynamicFloat elementDurability = DynamicFloat.ZERO; public DynamicFloat elementDurability = DynamicFloat.ZERO;
@ -328,9 +327,6 @@ public class AbilityModifier implements Serializable {
public String srcKey, dstKey; public String srcKey, dstKey;
public int skillID; public int skillID;
public int resistanceListID;
public int monsterID;
public int summonTag;
public AbilityModifierAction[] actions; public AbilityModifierAction[] actions;
public AbilityModifierAction[] successActions; public AbilityModifierAction[] successActions;
@ -373,11 +369,6 @@ 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 { public enum State {
LockHP, LockHP,
Invincible, Invincible,

View File

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

View File

@ -1,14 +0,0 @@
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

@ -19,7 +19,6 @@ public final class PointData {
@Getter private Position size; @Getter private Position size;
@Getter private boolean forbidSimpleUnlock; @Getter private boolean forbidSimpleUnlock;
@Getter private boolean unlocked; @Getter private boolean unlocked;
@Getter private boolean groupLimit;
@SerializedName( @SerializedName(
value = "dungeonIds", value = "dungeonIds",
@ -29,7 +28,7 @@ public final class PointData {
@SerializedName( @SerializedName(
value = "dungeonRandomList", value = "dungeonRandomList",
alternate = {"GLEKJMEEOMH"}) alternate = {"OIBKFJNBLHO"})
@Getter @Getter
private int[] dungeonRandomList; private int[] dungeonRandomList;

View File

@ -1,46 +0,0 @@
package emu.grasscutter.data.excels;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.*;
import java.util.List;
import lombok.*;
import lombok.experimental.FieldDefaults;
@ResourceType(name = "CoopChapterExcelConfigData.json")
@Getter
@Setter // TODO: remove setters next API break
@FieldDefaults(level = AccessLevel.PRIVATE)
public class CoopChapterData extends GameResource {
@Getter(onMethod_ = @Override)
int id;
int avatarId;
// int chapterNameTextMapHash;
// int coopPageTitleTextMapHash;
// int chapterSortId;
// int avatarSortId;
// String chapterIcon;
List<CoopCondition> unlockCond;
// int [] unlockCondTips;
// int openMaterialId;
// int openMaterialNum;
// String beginTimeStr;
// int confidenceValue;
// String pointGraphPath;
// Double graphXRatio;
// Double graphYRatio;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
private static class CoopCondition {
@SerializedName(
value = "_condType",
alternate = {"condType"})
String type = "COOP_COND_NONE";
@SerializedName(
value = "_args",
alternate = {"args"})
int[] args;
}
}

View File

@ -1,24 +0,0 @@
package emu.grasscutter.data.excels;
import emu.grasscutter.data.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
@ResourceType(name = "CoopPointExcelConfigData.json")
@Getter
@Setter // TODO: remove setters next API break
@FieldDefaults(level = AccessLevel.PRIVATE)
public class CoopPointData extends GameResource {
@Getter(onMethod_ = @Override)
int id;
int chapterId;
String type;
int acceptQuest;
int[] postPointList;
// int pointNameTextMapHash;
// int pointDecTextMapHash;
int pointPosId;
// long photoMaleHash;
// long photoFemaleHash;
}

View File

@ -1,17 +0,0 @@
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

@ -1,8 +1,6 @@
package emu.grasscutter.data.excels.dungeon; package emu.grasscutter.data.excels.dungeon;
import emu.grasscutter.data.*; import emu.grasscutter.data.*;
import emu.grasscutter.game.dungeons.enums.*;
import java.util.List;
import lombok.*; import lombok.*;
@ResourceType(name = "DungeonEntryExcelConfigData.json") @ResourceType(name = "DungeonEntryExcelConfigData.json")
@ -14,46 +12,4 @@ public class DungeonEntryData extends GameResource {
private int dungeonEntryId; private int dungeonEntryId;
private int sceneId; private int sceneId;
private DungunEntryType type;
private DungeonEntryCondCombType condComb;
private List<DungeonEntryCondition> satisfiedCond;
public static class DungeonEntryCondition {
private DungeonEntrySatisfiedConditionType type;
private int param1;
}
public DungunEntryType getType() {
if (type == null) {
return DungunEntryType.DUNGEN_ENTRY_TYPE_NONE;
}
return type;
}
public DungeonEntryCondCombType getCondComb() {
if (condComb == null) {
return DungeonEntryCondCombType.DUNGEON_ENTRY_COND_COMB_NONE;
}
return condComb;
}
public int getLevelCondition() {
for (var cond : satisfiedCond) {
if (cond.type != null
&& cond.type.equals(DungeonEntrySatisfiedConditionType.DUNGEON_ENTRY_CONDITION_LEVEL)) {
return cond.param1;
}
}
return 0;
}
public int getQuestCondition() {
for (var cond : satisfiedCond) {
if (cond.type != null
&& cond.type.equals(DungeonEntrySatisfiedConditionType.DUNGEON_ENTRY_CONDITION_QUEST)) {
return cond.param1;
}
}
return 0;
}
} }

View File

@ -1,83 +1,33 @@
package emu.grasscutter.data.excels.tower; package emu.grasscutter.data.excels.tower;
import emu.grasscutter.data.*; import emu.grasscutter.data.*;
import java.util.List;
import lombok.*;
@ResourceType(name = "TowerLevelExcelConfigData.json") @ResourceType(name = "TowerLevelExcelConfigData.json")
@Getter
public class TowerLevelData extends GameResource { public class TowerLevelData extends GameResource {
private int levelId; private int levelId;
private int levelIndex; private int levelIndex;
private int levelGroupId; private int levelGroupId;
private int dungeonId; private int dungeonId;
private List<TowerLevelCond> conds;
private int monsterLevel;
public static class TowerLevelCond {
private TowerCondType towerCondType;
private List<Integer> argumentList;
}
public enum TowerCondType {
TOWER_COND_NONE,
TOWER_COND_CHALLENGE_LEFT_TIME_MORE_THAN,
TOWER_COND_LEFT_HP_GREATER_THAN
}
// Not actual data in TowerLevelExcelConfigData.
// Just packaging condition parameters for convenience.
@Getter
public class TowerCondTimeParams {
private int param1;
private int minimumTimeInSeconds;
public TowerCondTimeParams(int param1, int minimumTimeInSeconds) {
this.param1 = param1;
this.minimumTimeInSeconds = minimumTimeInSeconds;
}
}
@Getter
public class TowerCondHpParams {
private int sceneId;
private int configId;
private int minimumHpPercentage;
public TowerCondHpParams(int sceneId, int configId, int minimumHpPercentage) {
this.sceneId = sceneId;
this.configId = configId;
this.minimumHpPercentage = minimumHpPercentage;
}
}
@Override @Override
public int getId() { public int getId() {
return this.getLevelId(); return this.getLevelId();
} }
public TowerCondType getCondType(int star) { public int getLevelId() {
if (star < 0 || conds == null || star >= conds.size()) { return levelId;
return TowerCondType.TOWER_COND_NONE;
}
var condType = conds.get(star).towerCondType;
return condType == null ? TowerCondType.TOWER_COND_NONE : condType;
} }
public TowerCondTimeParams getTimeCond(int star) { public int getLevelGroupId() {
if (star < 0 || conds == null || star >= conds.size()) { return levelGroupId;
return null;
}
var params = conds.get(star).argumentList;
return new TowerCondTimeParams(params.get(0), params.get(1));
} }
public TowerCondHpParams getHpCond(int star) { public int getLevelIndex() {
if (star < 0 || conds == null || star >= conds.size()) { return levelIndex;
return null; }
}
var params = conds.get(star).argumentList; public int getDungeonId() {
return new TowerCondHpParams(params.get(0), params.get(1), params.get(2)); return dungeonId;
} }
} }

View File

@ -0,0 +1,85 @@
package emu.grasscutter.database;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.objects.DatabaseObject;
import org.slf4j.*;
import java.util.*;
import java.util.concurrent.*;
/**
* Complicated manager of the MongoDB database.
* Handles caching, data operations, and more.
*/
public interface Database {
Logger logger = LoggerFactory.getLogger("Database");
List<DatabaseObject<?>> objects = new CopyOnWriteArrayList<>();
/**
* Queues an object to be saved.
*
* @param object The object to save.
*/
static void save(DatabaseObject<?> object) {
if (object.saveImmediately()) {
object.save();
} else {
objects.add(object);
}
}
/**
* Performs a bulk save of all deferred objects.
*/
static void saveAll() {
var size = objects.size();
Database.saveAll(objects);
logger.debug("Performed auto save on {} objects.", size);
}
/**
* Performs a bulk save of all deferred objects.
*
* @param objects The objects to save.
*/
static void saveAll(List<? extends DatabaseObject<?>> objects) {
// Sort all objects into their respective databases.
var gameObjects = objects.stream()
.filter(DatabaseObject::isGameObject)
.toList();
var accountObjects = objects.stream()
.filter(o -> !o.isGameObject())
.toList();
// Clear the collective list.
objects.clear();
// Save all objects.
var executor = DatabaseHelper.getEventExecutor();
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.DISPATCH_ONLY) {
executor.submit(() -> {
DatabaseManager.getGameDatastore().save(gameObjects);
});
}
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.GAME_ONLY) {
executor.submit(() -> {
DatabaseManager.getAccountDatastore().save(accountObjects);
});
}
}
/**
* Starts the auto-save thread.
* Runs every 5 minutes.
*/
static void startSaveThread() {
var timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Database.saveAll();
}
}, 0, 1000 * 60 * 5);
}
}

View File

@ -1,7 +1,5 @@
package emu.grasscutter.database; package emu.grasscutter.database;
import static com.mongodb.client.model.Filters.eq;
import dev.morphia.query.*; import dev.morphia.query.*;
import dev.morphia.query.experimental.filters.Filters; import dev.morphia.query.experimental.filters.Filters;
import emu.grasscutter.*; import emu.grasscutter.*;
@ -20,24 +18,19 @@ import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.GameMainQuest; import emu.grasscutter.game.quest.GameMainQuest;
import emu.grasscutter.game.world.SceneGroupInstance; import emu.grasscutter.game.world.SceneGroupInstance;
import emu.grasscutter.utils.objects.Returnable; import emu.grasscutter.utils.objects.Returnable;
import io.netty.util.concurrent.FastThreadLocalThread; import lombok.Getter;
import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.Nullable;
import lombok.Getter; import static com.mongodb.client.model.Filters.eq;
public final class DatabaseHelper { public final class DatabaseHelper {
@Getter @Getter
private static final ExecutorService eventExecutor = private static final ExecutorService eventExecutor =
new ThreadPoolExecutor( Executors.newFixedThreadPool(4);
6,
6,
60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
FastThreadLocalThread::new,
new ThreadPoolExecutor.AbortPolicy());
/** /**
* Saves an object on the account datastore. * Saves an object on the account datastore.

View File

@ -241,8 +241,7 @@ public interface HandbookActions {
// Create the entity. // Create the entity.
for (var i = 1; i <= request.getAmount(); i++) { for (var i = 1; i <= request.getAmount(); i++) {
var entity = var entity = new EntityMonster(scene, entityData, player.getPosition(), level);
new EntityMonster(scene, entityData, player.getPosition(), player.getRotation(), level);
scene.addEntity(entity); scene.addEntity(entity);
} }

View File

@ -2,7 +2,6 @@ package emu.grasscutter.game.ability;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityData; import emu.grasscutter.data.binout.AbilityData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.AbilityStringOuterClass.AbilityString; import emu.grasscutter.net.proto.AbilityStringOuterClass.AbilityString;
@ -25,7 +24,6 @@ public class Ability {
private static Map<String, Object2FloatMap<String>> abilitySpecialsModified = new HashMap<>(); private static Map<String, Object2FloatMap<String>> abilitySpecialsModified = new HashMap<>();
@Getter private int hash; @Getter private int hash;
@Getter private Set<Integer> avatarSkillStartIds;
public Ability(AbilityData data, GameEntity owner, Player playerOwner) { public Ability(AbilityData data, GameEntity owner, Player playerOwner) {
this.data = data; this.data = data;
@ -46,49 +44,6 @@ public class Ability {
hash = Utils.abilityHash(data.abilityName); hash = Utils.abilityHash(data.abilityName);
data.initialize(); 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) { public static String getAbilityName(AbilityString abString) {

View File

@ -7,7 +7,6 @@ import emu.grasscutter.data.binout.*;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.actions.*; import emu.grasscutter.game.ability.actions.*;
import emu.grasscutter.game.ability.mixins.*; import emu.grasscutter.game.ability.mixins.*;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.*; import emu.grasscutter.game.player.*;
import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.FightProperty;
@ -21,7 +20,7 @@ import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalar
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction; import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import emu.grasscutter.server.event.player.PlayerUseSkillEvent; import emu.grasscutter.server.event.player.PlayerUseSkillEvent;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import java.util.*; import java.util.HashMap;
import java.util.concurrent.*; import java.util.concurrent.*;
import lombok.Getter; import lombok.Getter;
@ -48,64 +47,9 @@ public final class AbilityManager extends BasePlayerManager {
} }
@Getter private boolean abilityInvulnerable = false; @Getter private boolean abilityInvulnerable = false;
private int burstCasterId;
private int burstSkillId;
public AbilityManager(Player player) { public AbilityManager(Player player) {
super(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() { public static void registerHandlers() {
@ -335,9 +279,8 @@ public final class AbilityManager extends BasePlayerManager {
return; return;
} }
// Track this elemental burst to possibly clear avatar energy later. // Set the player as invulnerable.
this.burstSkillId = skillId; this.abilityInvulnerable = true;
this.burstCasterId = casterId;
} }
/** /**
@ -510,8 +453,6 @@ public final class AbilityManager extends BasePlayerManager {
modifierData); modifierData);
} }
onPossibleElementalBurst(instancedAbility, modifierData, invoke.getEntityId());
AbilityModifierController modifier = AbilityModifierController modifier =
new AbilityModifierController(instancedAbility, instancedAbilityData, modifierData); new AbilityModifierController(instancedAbility, instancedAbilityData, modifierData);
@ -621,14 +562,6 @@ public final class AbilityManager extends BasePlayerManager {
if (killState.getKilled()) { if (killState.getKilled()) {
scene.killEntity(entity); scene.killEntity(entity);
} else if (!entity.isAlive()) { } else if (!entity.isAlive()) {
if (entity instanceof EntityAvatar) {
// TODO Should EntityAvatar act on this invocation?
// It bugs revival due to resetting HP to max when
// the avatar should just stay dead.
Grasscutter.getLogger()
.trace("Entity of ID {} is EntityAvatar. Ignoring", invoke.getEntityId());
return;
}
entity.setFightProperty( entity.setFightProperty(
FightProperty.FIGHT_PROP_CUR_HP, FightProperty.FIGHT_PROP_CUR_HP,
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP)); entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP));

View File

@ -1,70 +0,0 @@
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

@ -12,12 +12,13 @@ import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.AchievementOuterClass.Achievement.Status; import emu.grasscutter.net.proto.AchievementOuterClass.Achievement.Status;
import emu.grasscutter.server.event.player.PlayerCompleteAchievementEvent; import emu.grasscutter.server.event.player.PlayerCompleteAchievementEvent;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import lombok.*;
import org.bson.types.ObjectId;
import javax.annotation.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntSupplier; import java.util.function.IntSupplier;
import javax.annotation.Nullable;
import lombok.*;
import org.bson.types.ObjectId;
@Entity("achievements") @Entity("achievements")
@Data @Data
@ -44,15 +45,30 @@ public class Achievements {
return achievements; return achievements;
} }
/**
* Creates a blank achievements object.
*
* @return The achievements object.
*/
public static Achievements blank() {
return Achievements.of()
.achievementList(init())
.finishedAchievementNum(0)
.takenGoalRewardIdList(Lists.newArrayList())
.build();
}
/**
* Creates and saves a blank achievements object.
*
* @param uid The UID of the player.
* @return The achievements object.
*/
public static Achievements create(int uid) { public static Achievements create(int uid) {
var newAchievement = var newAchievement = blank();
Achievements.of() newAchievement.setUid(uid);
.uid(uid)
.achievementList(init())
.finishedAchievementNum(0)
.takenGoalRewardIdList(Lists.newArrayList())
.build();
newAchievement.save(); newAchievement.save();
return newAchievement; return newAchievement;
} }

View File

@ -11,15 +11,17 @@ import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass; import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify; import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import java.util.*; import emu.grasscutter.utils.objects.DatabaseObject;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import java.util.*;
@Entity("activities") @Entity("activities")
@Data @Data
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of") @Builder(builderMethodName = "of")
public class PlayerActivityData { public class PlayerActivityData implements DatabaseObject<PlayerActivityData> {
@Id String id; @Id String id;
int uid; int uid;
int activityId; int activityId;
@ -34,8 +36,25 @@ public class PlayerActivityData {
return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId); return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId);
} }
/**
* Saves this object to the database.
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
*/
public void save() { public void save() {
DatabaseHelper.savePlayerActivityData(this); this.deferSave();
}
/**
* Saves this object to the database.
*
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
*/
public void save(boolean immediate) {
if (immediate) {
DatabaseObject.super.save();
} else {
this.save();
}
} }
public synchronized void addWatcherProgress(int watcherId) { public synchronized void addWatcherProgress(int watcherId) {

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.avatar; package emu.grasscutter.game.avatar;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
@ -15,7 +13,6 @@ import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData.InherentProudSkil
import emu.grasscutter.data.excels.reliquary.*; import emu.grasscutter.data.excels.reliquary.*;
import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData; import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData;
import emu.grasscutter.data.excels.weapon.*; import emu.grasscutter.data.excels.weapon.*;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.inventory.*; import emu.grasscutter.game.inventory.*;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
@ -31,15 +28,19 @@ import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGra
import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo; import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.helpers.ProtoHelper; import emu.grasscutter.utils.helpers.ProtoHelper;
import emu.grasscutter.utils.objects.DatabaseObject;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import java.util.stream.Stream;
import javax.annotation.*;
import lombok.*; import lombok.*;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import javax.annotation.*;
import java.util.*;
import java.util.stream.Stream;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity(value = "avatars", useDiscriminator = false) @Entity(value = "avatars", useDiscriminator = false)
public class Avatar { public class Avatar implements DatabaseObject<Avatar> {
@Transient @Getter private final Int2ObjectMap<GameItem> equips; @Transient @Getter private final Int2ObjectMap<GameItem> equips;
@Transient @Getter private final Int2FloatOpenHashMap fightProperties; @Transient @Getter private final Int2FloatOpenHashMap fightProperties;
@Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides; @Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides;
@ -989,8 +990,25 @@ public class Avatar {
return entity != null ? entity.getId() : 0; return entity != null ? entity.getId() : 0;
} }
/**
* Saves this object to the database.
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
*/
public void save() { public void save() {
DatabaseHelper.saveAvatar(this); this.deferSave();
}
/**
* Saves this object to the database.
*
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
*/
public void save(boolean immediate) {
if (immediate) {
DatabaseObject.super.save();
} else {
this.save();
}
} }
public AvatarInfo toProto() { public AvatarInfo toProto() {

View File

@ -60,7 +60,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
this.avatars.put(avatar.getAvatarId(), avatar); this.avatars.put(avatar.getAvatarId(), avatar);
this.avatarsGuid.put(avatar.getGuid(), avatar); this.avatarsGuid.put(avatar.getGuid(), avatar);
avatar.save(); avatar.save(true);
return true; return true;
} }
@ -165,7 +165,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
if ((avatar.getAvatarId() == 10000007) || (avatar.getAvatarId() == 10000005)) { if ((avatar.getAvatarId() == 10000007) || (avatar.getAvatarId() == 10000005)) {
avatar.setSkillDepot(skillDepot); avatar.setSkillDepot(skillDepot);
avatar.setSkillDepotData(skillDepot); avatar.setSkillDepotData(skillDepot);
avatar.save(); avatar.save(true);
} }
} }

View File

@ -14,11 +14,12 @@ import emu.grasscutter.net.proto.BattlePassRewardTakeOptionOuterClass.BattlePass
import emu.grasscutter.net.proto.BattlePassScheduleOuterClass.BattlePassSchedule; import emu.grasscutter.net.proto.BattlePassScheduleOuterClass.BattlePassSchedule;
import emu.grasscutter.net.proto.BattlePassUnlockStatusOuterClass.BattlePassUnlockStatus; import emu.grasscutter.net.proto.BattlePassUnlockStatusOuterClass.BattlePassUnlockStatus;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import lombok.Getter;
import org.bson.types.ObjectId;
import java.time.*; import java.time.*;
import java.time.temporal.TemporalAdjusters; import java.time.temporal.TemporalAdjusters;
import java.util.*; import java.util.*;
import lombok.Getter;
import org.bson.types.ObjectId;
@Entity(value = "battlepass", useDiscriminator = false) @Entity(value = "battlepass", useDiscriminator = false)
public class BattlePassManager extends BasePlayerDataManager { public class BattlePassManager extends BasePlayerDataManager {
@ -40,7 +41,10 @@ public class BattlePassManager extends BasePlayerDataManager {
public BattlePassManager(Player player) { public BattlePassManager(Player player) {
super(player); super(player);
this.ownerUid = player.getUid(); this.ownerUid = player.getUid();
this.missions = new HashMap<>();
this.takenRewards = new HashMap<>();
} }
public void setPlayer(Player player) { public void setPlayer(Player player) {

View File

@ -38,7 +38,6 @@ public final class DungeonManager {
private boolean ended = false; private boolean ended = false;
private int newestWayPoint = 0; private int newestWayPoint = 0;
@Getter private int startSceneTime = 0; @Getter private int startSceneTime = 0;
@Setter @Getter private boolean towerDungeon = false;
DungeonTrialTeam trialTeam = null; DungeonTrialTeam trialTeam = null;
@ -68,10 +67,6 @@ public final class DungeonManager {
} }
if (isFinishedSuccessfully()) { 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(); finishDungeon();
} }
} }
@ -82,13 +77,8 @@ public final class DungeonManager {
} }
public int getLevelForMonster(int id) { public int getLevelForMonster(int id) {
if (isTowerDungeon()) { // TODO should use levelConfigMap? and how?
// Tower dungeons have their own level setting in TowerLevelData return dungeonData.getShowLevel();
return scene.getPlayers().get(0).getTowerManager().getCurrentMonsterLevel();
} else {
// TODO should use levelConfigMap? and how?
return dungeonData.getShowLevel();
}
} }
public boolean activateRespawnPoint(int pointId) { public boolean activateRespawnPoint(int pointId) {
@ -333,31 +323,15 @@ public final class DungeonManager {
p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON); p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON);
} }
}); });
var future = scene
scene .getScriptManager()
.getScriptManager() .callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0));
.callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0));
// Note: There is a possible race condition with calling
// EVENT_DUNGEON_SETTLE here asynchronously:
// 1. EVENT_DUNGEON_SETTLE triggers some Lua-side logic,
// which may happen after 2 (below) finishes.
// 2. Some DungeonSettleListener could be comparing some
// Lua variable before its setting in 1 (above) finishes.
// For safety, ensure all events have finished before returning.
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
} }
public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) { public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) {
if (scene.getDungeonSettleListeners() != null) { if (scene.getDungeonSettleListeners() != null) {
scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason)); scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason));
} }
if (isTowerDungeon()) {
scene.getPlayers().get(0).getTowerManager().onEnd();
}
ended = true; ended = true;
} }

View File

@ -131,11 +131,7 @@ public final class DungeonSystem extends BaseGameSystem {
dungeonId); dungeonId);
if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) { if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) {
var scene = player.getScene(); dungeonSettleListeners.forEach(player.getScene()::addDungeonSettleObserver);
var dungeonManager = new DungeonManager(scene, data);
dungeonManager.setTowerDungeon(true);
scene.setDungeonManager(dungeonManager);
dungeonSettleListeners.forEach(scene::addDungeonSettleObserver);
} }
return true; return true;
} }
@ -168,40 +164,11 @@ public final class DungeonSystem extends BaseGameSystem {
dungeonManager.unsetTrialTeam(player); dungeonManager.unsetTrialTeam(player);
} }
// clean temp team if it has // clean temp team if it has
if (!player.getTeamManager().cleanTemporaryTeam()) { player.getTeamManager().cleanTemporaryTeam();
// no temp team. Will use real current team, but check
// for any dead avatar to prevent switching into them.
player.getTeamManager().checkCurrentAvatarIsAlive(null);
}
player.getTowerManager().clearEntry(); player.getTowerManager().clearEntry();
dungeonManager.setTowerDungeon(false);
// Transfer player back to world after a small delay. // Transfer player back to world
// This wait is important for avoiding double teleports, player.getWorld().transferPlayerToScene(player, prevScene, prevPos);
// which specifically happen when player quits a dungeon player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp));
// by teleporting to map waypoints.
// From testing, 200ms seem reasonable.
player.getWorld().queueTransferPlayerToScene(player, prevScene, prevPos, 200);
}
public void restartDungeon(Player player) {
var scene = player.getScene();
var dungeonManager = scene.getDungeonManager();
var dungeonData = dungeonManager.getDungeonData();
var sceneId = dungeonData.getSceneId();
// Forward over previous scene and scene point
var prevScene = scene.getPrevScene();
var pointId = scene.getPrevScenePoint();
// Destroy then create scene again to reinitialize script state
scene.getPlayers().forEach(scene::removePlayer);
if (player.getWorld().transferPlayerToScene(player, sceneId, dungeonData)) {
scene = player.getScene();
scene.setPrevScene(prevScene);
scene.setPrevScenePoint(pointId);
scene.setDungeonManager(new DungeonManager(scene, dungeonData));
scene.addDungeonSettleObserver(basicDungeonSettleObserver);
}
} }
} }

View File

@ -1,6 +1,5 @@
package emu.grasscutter.game.dungeons; 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.BaseDungeonResult.DungeonEndReason;
import emu.grasscutter.game.dungeons.dungeon_results.TowerResult; import emu.grasscutter.game.dungeons.dungeon_results.TowerResult;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
@ -10,7 +9,6 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
@Override @Override
public void onDungeonSettle(DungeonManager dungeonManager, DungeonEndReason endReason) { public void onDungeonSettle(DungeonManager dungeonManager, DungeonEndReason endReason) {
var scene = dungeonManager.getScene(); var scene = dungeonManager.getScene();
var dungeonData = dungeonManager.getDungeonData(); var dungeonData = dungeonManager.getDungeonData();
if (scene.getLoadedGroups().stream() if (scene.getLoadedGroups().stream()
.anyMatch( .anyMatch(
@ -24,24 +22,17 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
} }
var towerManager = scene.getPlayers().get(0).getTowerManager(); var towerManager = scene.getPlayers().get(0).getTowerManager();
var stars = towerManager.getCurLevelStars();
if (endReason == DungeonEndReason.COMPLETED) { towerManager.notifyCurLevelRecordChangeWhenDone(3);
// Update star record only when challenge completes successfully. scene.broadcastPacket(
towerManager.notifyCurLevelRecordChangeWhenDone(stars); new PacketTowerFloorRecordChangeNotify(
scene.broadcastPacket( towerManager.getCurrentFloorId(), 3, towerManager.canEnterScheduleFloor()));
new PacketTowerFloorRecordChangeNotify(
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor()));
}
var challenge = scene.getChallenge(); var challenge = scene.getChallenge();
var finishedTime = challenge == null ? challenge.getFinishedTime() : 0;
var dungeonStats = var dungeonStats =
new DungeonEndStats(scene.getKilledMonsterCount(), finishedTime, 0, endReason); new DungeonEndStats(
var result = scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason);
endReason == DungeonEndReason.COMPLETED var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge);
? new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars)
: new BaseDungeonResult(dungeonData, dungeonStats);
scene.broadcastPacket(new PacketDungeonSettleNotify(result)); scene.broadcastPacket(new PacketDungeonSettleNotify(result));
} }

View File

@ -4,7 +4,6 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
@ -23,7 +22,6 @@ public class WorldChallenge {
private final int challengeIndex; private final int challengeIndex;
private final List<Integer> paramList; private final List<Integer> paramList;
private int timeLimit; private int timeLimit;
private GameEntity guardEntity;
private final List<ChallengeTrigger> challengeTriggers; private final List<ChallengeTrigger> challengeTriggers;
private final int goal; private final int goal;
private final AtomicInteger score; private final AtomicInteger score;
@ -60,7 +58,6 @@ public class WorldChallenge {
this.challengeTriggers = challengeTriggers; this.challengeTriggers = challengeTriggers;
this.goal = goal; this.goal = goal;
this.score = new AtomicInteger(0); this.score = new AtomicInteger(0);
this.guardEntity = null;
} }
public boolean inProgress() { public boolean inProgress() {
@ -83,16 +80,9 @@ public class WorldChallenge {
return; return;
} }
this.progress = true; this.progress = true;
this.startedAt = getScene().getSceneTimeSeconds(); this.startedAt = System.currentTimeMillis();
getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this));
challengeTriggers.forEach(t -> t.onBegin(this)); challengeTriggers.forEach(t -> t.onBegin(this));
var player = scene.getPlayers().get(0);
var dungeonManager = scene.getDungeonManager();
var towerManager = player.getTowerManager();
if (dungeonManager != null && dungeonManager.isTowerDungeon() && towerManager != null) {
towerManager.onBegin();
}
} }
public void done() { public void done() {
@ -146,10 +136,6 @@ public class WorldChallenge {
this.progress = false; this.progress = false;
this.success = success; this.success = success;
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt)); 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)); getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
} }
@ -157,20 +143,6 @@ public class WorldChallenge {
return score.incrementAndGet(); 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) { public void onMonsterDeath(EntityMonster monster) {
if (!inProgress()) { if (!inProgress()) {
return; return;

View File

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

View File

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

View File

@ -1,21 +1,8 @@
package emu.grasscutter.game.dungeons.challenge.trigger; package emu.grasscutter.game.dungeons.challenge.trigger;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
public class InTimeTrigger extends ChallengeTrigger { public class InTimeTrigger extends ChallengeTrigger {
@Override
public void onBegin(WorldChallenge challenge) {
// Show time remaining UI
var scene = challenge.getScene();
scene.broadcastPacket(
new PacketChallengeDataNotify(
challenge,
2,
// Compensate for time passed so far in scene.
challenge.getTimeLimit() + scene.getSceneTimeSeconds()));
}
@Override @Override
public void onCheckTimeout(WorldChallenge challenge) { public void onCheckTimeout(WorldChallenge challenge) {
var current = challenge.getScene().getSceneTimeSeconds(); var current = challenge.getScene().getSceneTimeSeconds();

View File

@ -13,47 +13,41 @@ public class TowerResult extends BaseDungeonResult {
boolean canJump; boolean canJump;
boolean hasNextLevel; boolean hasNextLevel;
int nextFloorId; int nextFloorId;
int currentStars;
public TowerResult( public TowerResult(
DungeonData dungeonData, DungeonData dungeonData,
DungeonEndStats dungeonStats, DungeonEndStats dungeonStats,
TowerManager towerManager, TowerManager towerManager,
WorldChallenge challenge, WorldChallenge challenge) {
int currentStars) {
super(dungeonData, dungeonStats); super(dungeonData, dungeonStats);
this.challenge = challenge; this.challenge = challenge;
this.canJump = towerManager.hasNextFloor(); this.canJump = towerManager.hasNextFloor();
this.hasNextLevel = towerManager.hasNextLevel(); this.hasNextLevel = towerManager.hasNextLevel();
this.nextFloorId = hasNextLevel ? 0 : towerManager.getNextFloorId(); this.nextFloorId = hasNextLevel ? 0 : towerManager.getNextFloorId();
this.currentStars = currentStars;
} }
@Override @Override
protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) { protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE; var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE;
if (challenge.isSuccess()) { if (challenge.isSuccess() && canJump) {
if (hasNextLevel) { continueStatus =
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE; hasNextLevel
} else if (canJump) { ? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE; : ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
}
} }
var towerLevelEndNotify = var towerLevelEndNotify =
TowerLevelEndNotify.newBuilder() TowerLevelEndNotify.newBuilder()
.setIsSuccess(challenge.isSuccess()) .setIsSuccess(challenge.isSuccess())
.setContinueState(continueStatus) .setContinueState(continueStatus)
.addFinishedStarCondList(1)
.addFinishedStarCondList(2)
.addFinishedStarCondList(3)
.addRewardItemList( .addRewardItemList(
ItemParamOuterClass.ItemParam.newBuilder().setItemId(201).setCount(1000)); ItemParamOuterClass.ItemParam.newBuilder().setItemId(201).setCount(1000).build());
for (int i = 1; i <= currentStars; i++) {
towerLevelEndNotify.addFinishedStarCondList(i);
}
if (nextFloorId > 0 && canJump) { if (nextFloorId > 0 && canJump) {
towerLevelEndNotify.setNextFloorId(nextFloorId); towerLevelEndNotify.setNextFloorId(nextFloorId);
} }
builder.setTowerLevelEndNotify(towerLevelEndNotify.build()); builder.setTowerLevelEndNotify(towerLevelEndNotify);
} }
} }

View File

@ -1,7 +0,0 @@
package emu.grasscutter.game.dungeons.enums;
public enum DungeonEntryCondCombType {
DUNGEON_ENTRY_COND_COMB_NONE,
DUNGEON_ENTRY_COND_COMB_LOGIC_OR,
DUNGEON_ENTRY_COND_COMB_LOGIC_AND
}

View File

@ -67,11 +67,6 @@ public class EntityAvatar extends GameEntity {
} }
this.initAbilities(); this.initAbilities();
// New EntityAvatar instances are created on every scene transition.
// Ensure that isDead is properly carried over between scenes.
// Otherwise avatars could have 0 HP but not considered dead.
this.checkIfDead();
} }
@Override @Override

View File

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

View File

@ -5,7 +5,6 @@ import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.config.ConfigEntityGadget; import emu.grasscutter.data.binout.config.ConfigEntityGadget;
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; import emu.grasscutter.data.binout.config.fields.ConfigAbilityData;
import emu.grasscutter.data.excels.GadgetData; 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.*;
import emu.grasscutter.game.entity.gadget.platform.*; import emu.grasscutter.game.entity.gadget.platform.*;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
@ -105,25 +104,6 @@ public class EntityGadget extends EntityBaseGadget {
this.bornRot = this.getRotation().clone(); this.bornRot = this.getRotation().clone();
this.fillFightProps(configGadget); 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)) { if (GameData.getGadgetMappingMap().containsKey(gadgetId)) {
var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController(); var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController();
this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName)); this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName));

View File

@ -18,8 +18,8 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
@Getter private final int rebirthCD; @Getter private final int rebirthCD;
private final AtomicBoolean disappeared = new AtomicBoolean(); private final AtomicBoolean disappeared = new AtomicBoolean();
public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos, Position rot) { public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos) {
super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, rot, 1); super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, 1);
this.rebornPos = pos.clone(); this.rebornPos = pos.clone();
this.rebirth = data.getIsRebirth(); this.rebirth = data.getIsRebirth();

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.entity; package emu.grasscutter.game.entity;
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.config.ConfigEntityMonster; import emu.grasscutter.data.binout.config.ConfigEntityMonster;
import emu.grasscutter.data.common.PropGrowCurve; import emu.grasscutter.data.common.PropGrowCurve;
@ -25,80 +23,61 @@ import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo; import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo;
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
import emu.grasscutter.net.proto.ServantInfoOuterClass.ServantInfo;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.*; import emu.grasscutter.scripts.data.*;
import emu.grasscutter.server.event.entity.EntityDamageEvent; import emu.grasscutter.server.event.entity.EntityDamageEvent;
import emu.grasscutter.utils.helpers.ProtoHelper; import emu.grasscutter.utils.helpers.ProtoHelper;
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
import java.util.*;
import javax.annotation.Nullable;
import lombok.*; import lombok.*;
import javax.annotation.Nullable;
import java.util.*;
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
public class EntityMonster extends GameEntity { public class EntityMonster extends GameEntity {
@Getter(onMethod_ = @Override) @Getter(onMethod_ = @Override)
private final Int2FloatOpenHashMap fightProperties; private final Int2FloatOpenHashMap fightProperties;
@Getter(onMethod_ = @Override) @Getter(onMethod_ = @Override)
private final Position position; private final Position position;
@Getter(onMethod_ = @Override) @Getter(onMethod_ = @Override)
private final Position rotation; private final Position rotation;
@Getter private final MonsterData monsterData; @Getter private final MonsterData monsterData;
@Getter private final ConfigEntityMonster configEntityMonster; @Getter private final ConfigEntityMonster configEntityMonster;
@Getter private final Position bornPos; @Getter private final Position bornPos;
@Getter private final int level; @Getter private final int level;
@Getter private EntityWeapon weaponEntity; @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 poseId;
@Getter @Setter private int aiId = -1; @Getter @Setter private int aiId = -1;
@Getter private List<Player> playerOnBattle; @Getter private List<Player> playerOnBattle;
@Nullable @Getter @Setter private SceneMonster metaMonster; @Nullable @Getter @Setter private SceneMonster metaMonster;
public EntityMonster( public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) {
Scene scene, MonsterData monsterData, Position pos, Position rot, int level) {
super(scene); super(scene);
this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER); this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER);
this.monsterData = monsterData; this.monsterData = monsterData;
this.fightProperties = new Int2FloatOpenHashMap(); this.fightProperties = new Int2FloatOpenHashMap();
this.position = new Position(pos); this.position = new Position(pos);
this.rotation = new Position(rot); this.rotation = new Position();
this.bornPos = this.getPosition().clone(); this.bornPos = this.getPosition().clone();
this.level = level; this.level = level;
this.playerOnBattle = new ArrayList<>(); this.playerOnBattle = new ArrayList<>();
this.summonTagMap = new HashMap<>();
this.summonedTag = 0;
this.ownerEntityId = 0;
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) { if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
this.configEntityMonster = this.configEntityMonster = GameData.getMonsterConfigData().get(
GameData.getMonsterConfigData() GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
.get(GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
} else { } else {
this.configEntityMonster = null; 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 // Monster weapon
if (getMonsterWeaponId() > 0) { if (getMonsterWeaponId() > 0) {
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId()); this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
scene.getWeaponEntities().put(this.weaponEntity.getId(), this.weaponEntity); scene.getWeaponEntities().put(this.weaponEntity.getId(), this.weaponEntity);
// this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON); //this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON);
} }
this.recalcStats(); this.recalcStats();
@ -108,15 +87,18 @@ public class EntityMonster extends GameEntity {
private void addConfigAbility(String name) { private void addConfigAbility(String name) {
var data = GameData.getAbilityData(name); var data = GameData.getAbilityData(name);
if (data != null) { if (data != null) {
this.getWorld().getHost().getAbilityManager().addAbilityToEntity(this, data); this.getWorld().getHost()
.getAbilityManager()
.addAbilityToEntity(this, data);
} }
} }
@Override @Override
public void initAbilities() { public void initAbilities() {
// Affix abilities // Affix abilities
var optionalGroup = var optionalGroup = this.getScene().getLoadedGroups().stream()
this.getScene().getLoadedGroups().stream().filter(g -> g.id == this.getGroupId()).findAny(); .filter(g -> g.id == this.getGroupId())
.findAny();
List<Integer> affixes = null; List<Integer> affixes = null;
if (optionalGroup.isPresent()) { if (optionalGroup.isPresent()) {
var group = optionalGroup.get(); var group = optionalGroup.get();
@ -136,7 +118,7 @@ public class EntityMonster extends GameEntity {
var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue()); var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue());
if (!affix.isPreAdd()) continue; if (!affix.isPreAdd()) continue;
// Add the ability //Add the ability
for (var name : affix.getAbilityName()) { for (var name : affix.getAbilityName()) {
this.addConfigAbility(name); this.addConfigAbility(name);
} }
@ -144,12 +126,14 @@ public class EntityMonster extends GameEntity {
} }
// TODO: Research if any monster is non humanoid // TODO: Research if any monster is non humanoid
for (var ability : for(var ability : GameData.getConfigGlobalCombat()
GameData.getConfigGlobalCombat().getDefaultAbilities().getNonHumanoidMoveAbilities()) { .getDefaultAbilities()
.getNonHumanoidMoveAbilities()) {
this.addConfigAbility(ability); this.addConfigAbility(ability);
} }
if (this.configEntityMonster != null && this.configEntityMonster.getAbilities() != null) { if (this.configEntityMonster != null &&
this.configEntityMonster.getAbilities() != null) {
for (var configAbilityData : this.configEntityMonster.getAbilities()) { for (var configAbilityData : this.configEntityMonster.getAbilities()) {
this.addConfigAbility(configAbilityData.abilityName); this.addConfigAbility(configAbilityData.abilityName);
} }
@ -159,8 +143,9 @@ public class EntityMonster extends GameEntity {
var group = optionalGroup.get(); var group = optionalGroup.get();
var monster = group.monsters.get(getConfigId()); var monster = group.monsters.get(getConfigId());
if (monster != null && monster.isElite) { if (monster != null && monster.isElite) {
this.addConfigAbility( this.addConfigAbility(GameData.getConfigGlobalCombat()
GameData.getConfigGlobalCombat().getDefaultAbilities().getMonterEliteAbilityName()); .getDefaultAbilities()
.getMonterEliteAbilityName());
} }
} }
@ -169,8 +154,8 @@ public class EntityMonster extends GameEntity {
var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue()); var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue());
if (affix.isPreAdd()) continue; if (affix.isPreAdd()) continue;
// Add the ability //Add the ability
for (var name : affix.getAbilityName()) { for(var name : affix.getAbilityName()) {
this.addConfigAbility(name); this.addConfigAbility(name);
} }
} }
@ -178,7 +163,7 @@ public class EntityMonster extends GameEntity {
var levelEntityConfig = getScene().getSceneData().getLevelEntityConfig(); var levelEntityConfig = getScene().getSceneData().getLevelEntityConfig();
var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig);
if (config == null) { if (config == null){
return; return;
} }
@ -209,8 +194,7 @@ public class EntityMonster extends GameEntity {
@Override @Override
public void onInteract(Player player, GadgetInteractReq interactReq) { public void onInteract(Player player, GadgetInteractReq interactReq) {
EnvAnimalGatherConfigData gatherData = EnvAnimalGatherConfigData gatherData = GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
if (gatherData == null) { if (gatherData == null) {
return; return;
@ -224,11 +208,7 @@ public class EntityMonster extends GameEntity {
@Override @Override
public void onCreate() { public void onCreate() {
// Lua event // Lua event
getScene() getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
.getScriptManager()
.callEvent(
new ScriptArgs(
this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
} }
@Override @Override
@ -251,17 +231,10 @@ public class EntityMonster extends GameEntity {
@Override @Override
public void runLuaCallbacks(EntityDamageEvent event) { public void runLuaCallbacks(EntityDamageEvent event) {
super.runLuaCallbacks(event); super.runLuaCallbacks(event);
getScene() getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EVENT_SPECIFIC_MONSTER_HP_CHANGE, getConfigId(), monsterData.getId())
.getScriptManager() .setSourceEntityId(getId())
.callEvent( .setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
new ScriptArgs( .setEventSource(getConfigId()));
this.getGroupId(),
EVENT_SPECIFIC_MONSTER_HP_CHANGE,
getConfigId(),
monsterData.getId())
.setSourceEntityId(getId())
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
.setEventSource(getConfigId()));
} }
@Override @Override
@ -277,68 +250,29 @@ public class EntityMonster extends GameEntity {
challenge.ifPresent(c -> c.onMonsterDeath(this)); challenge.ifPresent(c -> c.onMonsterDeath(this));
if (scriptManager.isInit() && this.getGroupId() > 0) { if (scriptManager.isInit() && this.getGroupId() > 0) {
Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()) Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()).ifPresent(s -> s.onMonsterDead(this));
.ifPresent(s -> s.onMonsterDead(this));
// Ensure each EVENT_ANY_MONSTER_DIE runs to completion. // prevent spawn monster after success
// Multiple such events firing at the same time may cause /*if (challenge.map(c -> c.inProgress()).orElse(true)) {
// the same lua trigger to fire multiple times, when it scriptManager.callEvent(new ScriptArgs(EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()).setGroupId(this.getGroupId()));
// should happen only once. } else if (getScene().getChallenge() == null) {
var future = }*/
scriptManager.callEvent( scriptManager.callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
new ScriptArgs(
this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
} }
// Battle Pass trigger // Battle Pass trigger
scene scene.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
.getPlayers()
.forEach(
p ->
p.getBattlePassManager()
.triggerMission(
WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
scene scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
.getPlayers() scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
.forEach( scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
p ->
p.getQuestManager()
.queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
scene
.getPlayers()
.forEach(
p ->
p.getQuestManager()
.queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
scene
.getPlayers()
.forEach(
p ->
p.getQuestManager()
.queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
SceneGroupInstance groupInstance = SceneGroupInstance groupInstance = scene.getScriptManager().getGroupInstanceById(this.getGroupId());
scene.getScriptManager().getGroupInstanceById(this.getGroupId()); if(groupInstance != null && metaMonster != null)
if (groupInstance != null && metaMonster != null)
groupInstance.getDeadEntities().add(metaMonster.config_id); groupInstance.getDeadEntities().add(metaMonster.config_id);
scene.triggerDungeonEvent( scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId());
DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId()); scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER, this.getMonsterData().getType().getValue());
scene.triggerDungeonEvent( scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER,
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() { public void recalcStats() {
@ -346,135 +280,86 @@ public class EntityMonster extends GameEntity {
MonsterData data = this.getMonsterData(); MonsterData data = this.getMonsterData();
// Get hp percent, set to 100% if none // Get hp percent, set to 100% if none
float hpPercent = float hpPercent = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0
? 1f
: this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)
/ this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
// Clear properties // Clear properties
this.getFightProperties().clear(); this.getFightProperties().clear();
// Base stats // Base stats
MonsterData.definedFightProperties.forEach( MonsterData.definedFightProperties.forEach(prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
// Level curve // Level curve
MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel()); MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel());
if (curve != null) { if (curve != null) {
for (PropGrowCurve growCurve : data.getPropGrowCurves()) { for (PropGrowCurve growCurve : data.getPropGrowCurves()) {
FightProperty prop = FightProperty.getPropByName(growCurve.getType()); FightProperty prop = FightProperty.getPropByName(growCurve.getType());
this.setFightProperty( this.setFightProperty(prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
} }
} }
// Set % stats // Set % stats
FightProperty.forEachCompoundProperty( FightProperty.forEachCompoundProperty(c -> this.setFightProperty(c.getResult(),
c -> this.getFightProperty(c.getFlat()) + (this.getFightProperty(c.getBase()) * (1f + this.getFightProperty(c.getPercent())))));
this.setFightProperty(
c.getResult(),
this.getFightProperty(c.getFlat())
+ (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 // Set current hp
this.setFightProperty( this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
FightProperty.FIGHT_PROP_CUR_HP,
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
} }
@Override @Override
public SceneEntityInfo toProto() { public SceneEntityInfo toProto() {
var data = this.getMonsterData(); var data = this.getMonsterData();
var aiInfo = var authority = EntityAuthorityInfo.newBuilder()
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(this.getBornPos().toProto()); .setAbilityInfo(AbilitySyncStateInfo.newBuilder())
if (ownerEntityId != 0) { .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
aiInfo.setServantInfo(ServantInfo.newBuilder().setMasterEntityId(ownerEntityId)); .setAiInfo(SceneEntityAiInfo.newBuilder()
} .setIsAiOpen(true)
.setBornPos(this.getBornPos().toProto()))
.setBornPos(this.getBornPos().toProto())
.build();
var authority = var entityInfo = SceneEntityInfo.newBuilder()
EntityAuthorityInfo.newBuilder() .setEntityId(this.getId())
.setAbilityInfo(AbilitySyncStateInfo.newBuilder()) .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER)
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) .setMotionInfo(this.getMotionInfo())
.setAiInfo(aiInfo) .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder())
.setBornPos(this.getBornPos().toProto()) .setEntityClientData(EntityClientData.newBuilder())
.build(); .setEntityAuthorityInfo(authority)
.setLifeState(this.getLifeState().getValue());
var entityInfo =
SceneEntityInfo.newBuilder()
.setEntityId(this.getId())
.setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER)
.setMotionInfo(this.getMotionInfo())
.addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder())
.setEntityClientData(EntityClientData.newBuilder())
.setEntityAuthorityInfo(authority)
.setLifeState(this.getLifeState().getValue());
this.addAllFightPropsToEntityInfo(entityInfo); this.addAllFightPropsToEntityInfo(entityInfo);
entityInfo.addPropList( entityInfo.addPropList(PropPair.newBuilder()
PropPair.newBuilder() .setType(PlayerProperty.PROP_LEVEL.getId())
.setType(PlayerProperty.PROP_LEVEL.getId()) .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel()))
.setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())) .build());
.build());
var monsterInfo = var monsterInfo = SceneMonsterInfo.newBuilder()
SceneMonsterInfo.newBuilder() .setMonsterId(getMonsterId())
.setMonsterId(getMonsterId()) .setGroupId(this.getGroupId())
.setGroupId(this.getGroupId()) .setConfigId(this.getConfigId())
.setConfigId(this.getConfigId()) .addAllAffixList(data.getAffix())
.addAllAffixList(data.getAffix()) .setAuthorityPeerId(this.getWorld().getHostPeerId())
.setAuthorityPeerId(this.getWorld().getHostPeerId()) .setPoseId(this.getPoseId())
.setPoseId(this.getPoseId()) .setBlockId(this.getScene().getId())
.setBlockId(this.getScene().getId()) .setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT);
.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 != null) {
if (this.metaMonster.special_name_id != 0) { if (this.metaMonster.special_name_id != 0) {
monsterInfo monsterInfo.setTitleId(this.metaMonster.title_id)
.setTitleId(this.metaMonster.title_id) .setSpecialNameId(this.metaMonster.special_name_id);
.setSpecialNameId(this.metaMonster.special_name_id);
} else if (data.getDescribeData() != null) { } else if (data.getDescribeData() != null) {
monsterInfo monsterInfo.setTitleId(data.getDescribeData().getTitleId())
.setTitleId(data.getDescribeData().getTitleId()) .setSpecialNameId(data.getSpecialNameId());
.setSpecialNameId(data.getSpecialNameId());
} }
} }
if (this.getMonsterWeaponId() > 0) { if (this.getMonsterWeaponId() > 0) {
SceneWeaponInfo weaponInfo = SceneWeaponInfo weaponInfo = SceneWeaponInfo.newBuilder()
SceneWeaponInfo.newBuilder() .setEntityId(this.getWeaponEntity() != null ? this.getWeaponEntity().getId() : 0)
.setEntityId(this.getWeaponEntity() != null ? this.getWeaponEntity().getId() : 0) .setGadgetId(this.getMonsterWeaponId())
.setGadgetId(this.getMonsterWeaponId()) .setAbilityInfo(AbilitySyncStateInfo.newBuilder())
.setAbilityInfo(AbilitySyncStateInfo.newBuilder()) .build();
.build();
monsterInfo.addWeaponList(weaponInfo); monsterInfo.addWeaponList(weaponInfo);
} }

View File

@ -1,7 +1,6 @@
package emu.grasscutter.game.entity; package emu.grasscutter.game.entity;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.game.ability.*; import emu.grasscutter.game.ability.*;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.*; import emu.grasscutter.game.props.*;
@ -16,9 +15,10 @@ import emu.grasscutter.scripts.data.controller.EntityController;
import emu.grasscutter.server.event.entity.*; import emu.grasscutter.server.event.entity.*;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import lombok.*; import lombok.*;
import java.util.*;
public abstract class GameEntity { public abstract class GameEntity {
@Getter private final Scene scene; @Getter private final Scene scene;
@Getter protected int id; @Getter protected int id;
@ -33,12 +33,9 @@ public abstract class GameEntity {
@Getter @Setter private int lastMoveReliableSeq; @Getter @Setter private int lastMoveReliableSeq;
@Getter @Setter private boolean lockHP; @Getter @Setter private boolean lockHP;
private boolean limbo;
private float limboHpThreshold;
@Setter(AccessLevel.PROTECTED) @Setter(AccessLevel.PROTECTED)
@Getter @Getter private boolean isDead = false;
private boolean isDead = false;
// Lua controller for specific actions // Lua controller for specific actions
@Getter @Setter private EntityController entityController; @Getter @Setter private EntityController entityController;
@ -113,21 +110,6 @@ 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() { protected MotionInfo getMotionInfo() {
return MotionInfo.newBuilder() return MotionInfo.newBuilder()
.setPos(this.getPosition().toProto()) .setPos(this.getPosition().toProto())
@ -185,29 +167,20 @@ public abstract class GameEntity {
return; // If the event is canceled, do not damage the entity. return; // If the event is canceled, do not damage the entity.
} }
float effectiveDamage = 0;
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
if (limbo) { if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); // Add negative HP to the current HP property.
float curRatio = curHp / maxHp; this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
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.lastAttackType = attackType;
this.checkIfDead();
// Check if dead
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
this.isDead = true;
}
this.runLuaCallbacks(event); this.runLuaCallbacks(event);
// Packets // Packets
@ -221,17 +194,6 @@ public abstract class GameEntity {
} }
} }
public void checkIfDead() {
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
return;
}
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
this.isDead = true;
}
}
/** /**
* Runs the Lua callbacks for {@link EntityDamageEvent}. * Runs the Lua callbacks for {@link EntityDamageEvent}.
* *
@ -371,8 +333,6 @@ public abstract class GameEntity {
if (entityController != null) { if (entityController != null) {
entityController.onDie(this, getLastAttackType()); entityController.onDie(this, getLastAttackType());
} }
this.isDead = true;
} }
/** Invoked when a global ability value is updated. */ /** Invoked when a global ability value is updated. */

View File

@ -1,6 +1,6 @@
package emu.grasscutter.game.entity.gadget; package emu.grasscutter.game.entity.gadget;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
@ -18,7 +18,7 @@ public final class GadgetRewardStatue extends GadgetContent {
public boolean onInteract(Player player, GadgetInteractReq req) { public boolean onInteract(Player player, GadgetInteractReq req) {
var dungeonManager = player.getScene().getDungeonManager(); var dungeonManager = player.getScene().getDungeonManager();
if (player.getScene().getChallenge() instanceof WorldChallenge) { if (player.getScene().getChallenge() instanceof DungeonChallenge) {
var useCondensed = var useCondensed =
req.getResinCostType() == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE; req.getResinCostType() == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE;
dungeonManager.getStatueDrops(player, useCondensed, getGadget().getGroupId()); dungeonManager.getStatueDrops(player, useCondensed, getGadget().getGroupId());

View File

@ -9,10 +9,9 @@ import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo; import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntSets;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
@ -37,10 +36,6 @@ public class GameHome {
|| sceneData.getSceneType() == SceneType.SCENE_HOME_ROOM) || sceneData.getSceneType() == SceneType.SCENE_HOME_ROOM)
.map(SceneData::getId) .map(SceneData::getId)
.collect(Collectors.toUnmodifiableSet()); .collect(Collectors.toUnmodifiableSet());
public static final Set<Integer> HOME_MODULE_IDS =
GameData.getHomeWorldModuleDataMap().isEmpty()
? IntSets.fromTo(1, 6)
: GameData.getHomeWorldModuleDataMap().keySet();
@Id String id; @Id String id;
@ -186,7 +181,6 @@ public class GameHome {
public void onOwnerLogin(Player player) { public void onOwnerLogin(Player player) {
this.player = player; // update player pointer. (prevent offline player from sending packet) 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 PacketHomeBasicInfoNotify(player, false));
player.getSession().send(new PacketPlayerHomeCompInfoNotify(player)); player.getSession().send(new PacketPlayerHomeCompInfoNotify(player));
player.getSession().send(new PacketHomeComfortInfoNotify(player)); player.getSession().send(new PacketHomeComfortInfoNotify(player));
@ -200,35 +194,6 @@ public class GameHome {
player.getSession().send(new PacketHomeResourceNotify(player)); 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) { public void onPlayerChangedAvatarCostume(Avatar avatar) {
var world = this.player.getServer().getHomeWorldOrCreate(this.player); var world = this.player.getServer().getHomeWorldOrCreate(this.player);
world.broadcastPacket( world.broadcastPacket(
@ -274,7 +239,8 @@ public class GameHome {
return this.finishedTalkIdMap.get(avatarId); return this.finishedTalkIdMap.get(avatarId);
} }
public List<HomeAvatarTalkFinishInfo> toAvatarTalkFinishInfoProto() { public List<HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo>
toAvatarTalkFinishInfoProto() {
if (this.finishedTalkIdMap == null) { if (this.finishedTalkIdMap == null) {
this.finishedTalkIdMap = new HashMap<>(); this.finishedTalkIdMap = new HashMap<>();
} }
@ -282,7 +248,7 @@ public class GameHome {
return this.finishedTalkIdMap.entrySet().stream() return this.finishedTalkIdMap.entrySet().stream()
.map( .map(
e -> { e -> {
return HomeAvatarTalkFinishInfo.newBuilder() return HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo.newBuilder()
.setAvatarId(e.getKey()) .setAvatarId(e.getKey())
.addAllFinishTalkIdList(e.getValue()) .addAllFinishTalkIdList(e.getValue())
.build(); .build();

View File

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

View File

@ -107,8 +107,7 @@ public class HomeSceneItem {
return new EntityHomeAnimal( return new EntityHomeAnimal(
scene, scene,
GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()), GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()),
homeAnimalItem.getSpawnPos(), homeAnimalItem.getSpawnPos());
homeAnimalItem.getSpawnRot());
}) })
.toList(); .toList();
} }

View File

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

View File

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

View File

@ -20,13 +20,14 @@ import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary;
import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo; import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo;
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
import emu.grasscutter.net.proto.WeaponOuterClass.Weapon; import emu.grasscutter.net.proto.WeaponOuterClass.Weapon;
import emu.grasscutter.utils.objects.WeightedList; import emu.grasscutter.utils.objects.*;
import java.util.*;
import lombok.*; import lombok.*;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import java.util.*;
@Entity(value = "items", useDiscriminator = false) @Entity(value = "items", useDiscriminator = false)
public class GameItem { public class GameItem implements DatabaseObject<GameItem> {
@Id private ObjectId id; @Id private ObjectId id;
@Indexed private int ownerId; @Indexed private int ownerId;
@Getter @Setter private int itemId; @Getter @Setter private int itemId;
@ -261,14 +262,36 @@ public class GameItem {
} }
} }
/**
* Saves this object to the database.
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
*/
public void save() { public void save() {
if (this.count > 0 && this.ownerId > 0) { if (this.count > 0 && this.ownerId > 0) {
DatabaseHelper.saveItem(this); this.deferSave();
} else if (this.getObjectId() != null) { } else {
DatabaseHelper.deleteItem(this); DatabaseHelper.deleteItem(this);
} }
} }
/**
* Saves this object to the database.
*
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
*/
public void save(boolean immediate) {
if (this.count < 0 || this.ownerId <= 0) {
DatabaseHelper.deleteItem(this);
return;
}
if (immediate) {
DatabaseObject.super.save();
} else {
this.save();
}
}
public SceneWeaponInfo createSceneWeaponInfo() { public SceneWeaponInfo createSceneWeaponInfo() {
var weaponInfo = var weaponInfo =
SceneWeaponInfo.newBuilder() SceneWeaponInfo.newBuilder()

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.inventory; package emu.grasscutter.game.inventory;
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.data.common.ItemParamData;
@ -18,10 +16,13 @@ import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.longs.*;
import java.util.*;
import javax.annotation.Nullable;
import lombok.val; import lombok.val;
import javax.annotation.Nullable;
import java.util.*;
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
public class Inventory extends BasePlayerManager implements Iterable<GameItem> { public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
private final Long2ObjectMap<GameItem> store; private final Long2ObjectMap<GameItem> store;
private final Int2ObjectMap<InventoryTab> inventoryTypes; private final Int2ObjectMap<InventoryTab> inventoryTypes;
@ -178,7 +179,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
changedItems.add(result); changedItems.add(result);
} }
} }
if (changedItems.size() == 0) { if (changedItems.isEmpty()) {
return; return;
} }
if (reason != null) { if (reason != null) {
@ -298,8 +299,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
// Add // Add
switch (type) { switch (type) {
case ITEM_WEAPON: case ITEM_WEAPON, ITEM_RELIQUARY -> {
case ITEM_RELIQUARY:
if (tab.getSize() >= tab.getMaxCapacity()) { if (tab.getSize() >= tab.getMaxCapacity()) {
return null; return null;
} }
@ -310,23 +310,23 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
// Set ownership and save to db // Set ownership and save to db
item.save(); item.save();
return item; return item;
case ITEM_VIRTUAL: }
case ITEM_VIRTUAL -> {
// Handle // Handle
this.addVirtualItem(item.getItemId(), item.getCount()); this.addVirtualItem(item.getItemId(), item.getCount());
return item; return item;
default: }
default -> {
switch (item.getItemData().getMaterialType()) { switch (item.getItemData().getMaterialType()) {
case MATERIAL_AVATAR: case MATERIAL_AVATAR, MATERIAL_FLYCLOAK, MATERIAL_COSTUME, MATERIAL_NAMECARD -> {
case MATERIAL_FLYCLOAK:
case MATERIAL_COSTUME:
case MATERIAL_NAMECARD:
Grasscutter.getLogger() Grasscutter.getLogger()
.warn( .warn(
"Attempted to add a " "Attempted to add a "
+ item.getItemData().getMaterialType().name() + item.getItemData().getMaterialType().name()
+ " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error."); + " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error.");
return null; return null;
default: }
default -> {
if (tab == null) { if (tab == null) {
return null; return null;
} }
@ -350,7 +350,9 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
existingItem.save(); existingItem.save();
return existingItem; return existingItem;
} }
}
} }
}
} }
} }

View File

@ -104,8 +104,7 @@ public final class BlossomActivity {
var monsterData = GameData.getMonsterDataMap().get((int) entry); var monsterData = GameData.getMonsterDataMap().get((int) entry);
var level = scene.getEntityLevel(1, worldLevelOverride); var level = scene.getEntityLevel(1, worldLevelOverride);
var entity = var entity = new EntityMonster(scene, monsterData, pos.nearby2d(4f), level);
new EntityMonster(scene, monsterData, pos.nearby2d(4f), Position.ZERO, level);
scene.addEntity(entity); scene.addEntity(entity);
newMonsters.add(entity); newMonsters.add(entity);
} }

View File

@ -259,14 +259,8 @@ public class EnergyManager extends BasePlayerManager {
return; 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 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); avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
} }
} }
@ -400,11 +394,10 @@ public class EnergyManager extends BasePlayerManager {
public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) { public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) {
for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) { for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) {
// giving the exact amount read off the AvatarSkillData.json // giving the exact amount read off the AvatarSkillData.json
var skillDepot = entityAvatar.getAvatar().getSkillDepot(); entityAvatar.addEnergy(
if (skillDepot != null) { entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(),
entityAvatar.addEnergy( changeReason,
skillDepot.getEnergySkillData().getCostElemVal(), changeReason, isFlat); isFlat);
}
} }
} }

View File

@ -8,7 +8,7 @@ import emu.grasscutter.data.excels.world.WeatherData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.*; import emu.grasscutter.game.*;
import emu.grasscutter.game.ability.AbilityManager; import emu.grasscutter.game.ability.AbilityManager;
import emu.grasscutter.game.achievement.Achievements; import emu.grasscutter.game.achievement.*;
import emu.grasscutter.game.activity.ActivityManager; import emu.grasscutter.game.activity.ActivityManager;
import emu.grasscutter.game.avatar.*; import emu.grasscutter.game.avatar.*;
import emu.grasscutter.game.battlepass.BattlePassManager; import emu.grasscutter.game.battlepass.BattlePassManager;
@ -55,7 +55,7 @@ import emu.grasscutter.server.game.GameSession.SessionState;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.helpers.DateHelper; import emu.grasscutter.utils.helpers.DateHelper;
import emu.grasscutter.utils.objects.FieldFetch; import emu.grasscutter.utils.objects.*;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.*; import lombok.*;
@ -66,7 +66,7 @@ import java.util.concurrent.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS; import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity(value = "players", useDiscriminator = false) @Entity(value = "players", useDiscriminator = false)
public class Player implements PlayerHook, FieldFetch { public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
@Id private int id; @Id private int id;
@Indexed(options = @IndexOptions(unique = true)) @Indexed(options = @IndexOptions(unique = true))
@Getter private String accountId; @Getter private String accountId;
@ -176,7 +176,6 @@ public class Player implements PlayerHook, FieldFetch {
@Getter @Setter private Set<Date> moonCardGetTimes; @Getter @Setter private Set<Date> moonCardGetTimes;
@Transient @Getter private boolean paused; @Transient @Getter private boolean paused;
@Transient @Getter @Setter private Future<?> queuedTeleport;
@Transient @Getter @Setter private int enterSceneToken; @Transient @Getter @Setter private int enterSceneToken;
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE; @Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
@Transient private boolean hasSentLoginPackets; @Transient private boolean hasSentLoginPackets;
@ -262,6 +261,7 @@ public class Player implements PlayerHook, FieldFetch {
this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class); this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class);
this.birthday = new PlayerBirthday(); this.birthday = new PlayerBirthday();
this.achievements = Achievements.blank();
this.rewardedLevels = new HashSet<>(); this.rewardedLevels = new HashSet<>();
this.homeRewardedLevels = new HashSet<>(); this.homeRewardedLevels = new HashSet<>();
this.seenRealmList = new HashSet<>(); this.seenRealmList = new HashSet<>();
@ -276,8 +276,10 @@ public class Player implements PlayerHook, FieldFetch {
this.energyManager = new EnergyManager(this); this.energyManager = new EnergyManager(this);
this.resinManager = new ResinManager(this); this.resinManager = new ResinManager(this);
this.forgingManager = new ForgingManager(this); this.forgingManager = new ForgingManager(this);
this.deforestationManager = new DeforestationManager(this);
this.progressManager = new PlayerProgressManager(this); this.progressManager = new PlayerProgressManager(this);
this.furnitureManager = new FurnitureManager(this); this.furnitureManager = new FurnitureManager(this);
this.battlePassManager = new BattlePassManager(this);
this.cookingManager = new CookingManager(this); this.cookingManager = new CookingManager(this);
this.cookingCompoundManager = new CookingCompoundManager(this); this.cookingCompoundManager = new CookingCompoundManager(this);
this.satiationManager = new SatiationManager(this); this.satiationManager = new SatiationManager(this);
@ -301,19 +303,6 @@ public class Player implements PlayerHook, FieldFetch {
this.applyStartingSceneTags(); this.applyStartingSceneTags();
this.getFlyCloakList().add(140001); this.getFlyCloakList().add(140001);
this.getNameCardList().add(210001); this.getNameCardList().add(210001);
this.mapMarksManager = new MapMarksManager(this);
this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this);
this.energyManager = new EnergyManager(this);
this.resinManager = new ResinManager(this);
this.deforestationManager = new DeforestationManager(this);
this.forgingManager = new ForgingManager(this);
this.progressManager = new PlayerProgressManager(this);
this.furnitureManager = new FurnitureManager(this);
this.cookingManager = new CookingManager(this);
this.cookingCompoundManager = new CookingCompoundManager(this);
this.satiationManager = new SatiationManager(this);
} }
@Override @Override
@ -1341,8 +1330,25 @@ public class Player implements PlayerHook, FieldFetch {
this.getTeamManager().setPlayer(this); this.getTeamManager().setPlayer(this);
} }
/**
* Saves this object to the database.
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
*/
public void save() { public void save() {
DatabaseHelper.savePlayer(this); this.deferSave();
}
/**
* Saves this object to the database.
*
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
*/
public void save(boolean immediate) {
if (immediate) {
DatabaseObject.super.save();
} else {
this.save();
}
} }
// Called from tokenrsp // Called from tokenrsp
@ -1379,6 +1385,14 @@ public class Player implements PlayerHook, FieldFetch {
this.getPlayerProgress().setPlayer(this); // Add reference to the player. this.getPlayerProgress().setPlayer(this); // Add reference to the player.
} }
/**
* Invoked when the player selects their avatar.
*/
public void onPlayerBorn() {
Grasscutter.getThreadPool().submit(
this.getQuestManager()::onPlayerBorn);
}
public void onLogin() { public void onLogin() {
// Quest - Commented out because a problem is caused if you log out while this quest is active // Quest - Commented out because a problem is caused if you log out while this quest is active
/* /*
@ -1505,20 +1519,19 @@ public class Player implements PlayerHook, FieldFetch {
this.getProfile().syncWithCharacter(this); this.getProfile().syncWithCharacter(this);
this.getCoopRequests().clear(); this.getCoopRequests().clear();
this.getEnterHomeRequests().values().forEach(req -> this.expireEnterHomeRequest(req, true)); this.getEnterHomeRequests().values()
.forEach(req -> this.expireEnterHomeRequest(req, true));
this.getEnterHomeRequests().clear(); this.getEnterHomeRequests().clear();
// Save to db // Save to db
this.save(); this.save(true);
this.getTeamManager().saveAvatars(); this.getTeamManager().saveAvatars();
this.getFriendsList().save(); this.getFriendsList().save();
// Call quit event. // Call quit event.
PlayerQuitEvent event = new PlayerQuitEvent(this); new PlayerQuitEvent(this).call();
event.call();
} catch (Throwable e) { } catch (Throwable e) {
e.printStackTrace(); Grasscutter.getLogger().warn("Player (UID {}) failed to save.", this.getUid(), e);
Grasscutter.getLogger().warn("Player (UID {}) save failure", getUid());
} finally { } finally {
removeFromServer(); removeFromServer();
} }
@ -1526,34 +1539,10 @@ public class Player implements PlayerHook, FieldFetch {
public void removeFromServer() { public void removeFromServer() {
// Remove from server. // Remove from server.
//Note: DON'T DELETE BY UID,BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN! // Note: DON'T DELETE BY UID, BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN!
//so I decide to delete by object rather than uid //s o I decide to delete by object rather than uid
getServer().getPlayers().values().removeIf(player1 -> player1 == this); this.getServer().getPlayers().values()
} .removeIf(player1 -> player1 == this);
public void unfreezeUnlockedScenePoints(int sceneId) {
// Unfreeze previously unlocked scene points. For example,
// the first weapon mats domain needs some script interaction
// to unlock. It needs to be unfrozen when GetScenePointReq
// comes in to be interactable again.
GameData.getScenePointEntryMap().values().stream()
.filter(scenePointEntry ->
// Note: Only DungeonEntry scene points need to be unfrozen
scenePointEntry.getPointData().getType().equals("DungeonEntry")
// groupLimit says this scene point needs to be unfrozen
&& scenePointEntry.getPointData().isGroupLimit())
.forEach(scenePointEntry -> {
// If this is a previously unlocked scene point,
// send unfreeze packet.
val pointId = scenePointEntry.getPointData().getId();
if (unlockedScenePoints.get(sceneId).contains(pointId)) {
this.sendPacket(new PacketUnfreezeGroupLimitNotify(pointId, sceneId));
}
});
}
public void unfreezeUnlockedScenePoints() {
unlockedScenePoints.keySet().forEach(sceneId -> unfreezeUnlockedScenePoints(sceneId));
} }
public int getLegendaryKey() { public int getLegendaryKey() {

View File

@ -33,9 +33,12 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
public static final Set<Integer> IGNORED_OPEN_STATES = public static final Set<Integer> IGNORED_OPEN_STATES =
Set.of( Set.of(
1404 // OPEN_STATE_MENGDE_INFUSEDCRYSTAL, causes quest 'Mine Craft' to be given to the 1404, // OPEN_STATE_MENGDE_INFUSEDCRYSTAL, causes quest 'Mine Craft' to be given to the
// player at the start of the game. // player at the start of the game.
// This should be removed when city reputation is implemented. // This should be removed when city reputation is implemented.
57 // OPEN_STATE_PERSONAL_LINE, causes the prompt for showing character hangout quests to
// be permanently shown.
// This should be removed when character story quests are implemented.
); );
// Set of open states that are set per default for all accounts. Can be overwritten by an entry in // Set of open states that are set per default for all accounts. Can be overwritten by an entry in
// `map`. // `map`.
@ -100,7 +103,7 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
} }
private void setOpenState(int openState, int value, boolean sendNotify) { private void setOpenState(int openState, int value, boolean sendNotify) {
int previousValue = this.player.getOpenStates().getOrDefault(openState, -1 /* non-existent */); int previousValue = this.player.getOpenStates().getOrDefault(openState, 0);
if (value != previousValue) { if (value != previousValue) {
this.player.getOpenStates().put(openState, value); this.player.getOpenStates().put(openState, value);

View File

@ -1,11 +1,10 @@
package emu.grasscutter.game.player; package emu.grasscutter.game.player;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.*; import emu.grasscutter.*;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
import emu.grasscutter.database.Database;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.*; import emu.grasscutter.game.props.*;
@ -23,9 +22,12 @@ import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.*;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity @Entity
public final class TeamManager extends BasePlayerDataManager { public final class TeamManager extends BasePlayerDataManager {
@ -404,7 +406,7 @@ public final class TeamManager extends BasePlayerDataManager {
// Unload removed entities // Unload removed entities
for (var entity : existingAvatars.values()) { for (var entity : existingAvatars.values()) {
this.getPlayer().getScene().removeEntity(entity); this.getPlayer().getScene().removeEntity(entity);
entity.getAvatar().save(); entity.getAvatar().save(true);
} }
// Set new selected character index // Set new selected character index
@ -425,30 +427,6 @@ public final class TeamManager extends BasePlayerDataManager {
this.getPlayer().sendPacket(responsePacket); this.getPlayer().sendPacket(responsePacket);
} }
// Ensure new selected character index is alive.
// If not, change to another alive one or revive.
checkCurrentAvatarIsAlive(currentEntity);
}
public void checkCurrentAvatarIsAlive(EntityAvatar currentEntity) {
if (currentEntity == null) {
currentEntity = this.getCurrentAvatarEntity();
}
// Ensure currently selected character is still alive
if (!this.getActiveTeam().get(this.currentCharacterIndex).isAlive()) {
// Character died in a dungeon challenge...
int replaceIndex = getDeadAvatarReplacement();
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
this.currentCharacterIndex = replaceIndex;
} else {
// Team wiped in dungeon...
// Revive and change to first avatar.
this.currentCharacterIndex = 0;
this.reviveAvatar(this.getCurrentAvatarEntity().getAvatar());
}
}
// Check if character changed // Check if character changed
var newAvatarEntity = this.getCurrentAvatarEntity(); var newAvatarEntity = this.getCurrentAvatarEntity();
if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) { if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) {
@ -724,16 +702,15 @@ public final class TeamManager extends BasePlayerDataManager {
this.updateTeamEntities(null); this.updateTeamEntities(null);
} }
public boolean cleanTemporaryTeam() { public void cleanTemporaryTeam() {
// check if using temporary team // check if using temporary team
if (useTemporarilyTeamIndex < 0) { if (useTemporarilyTeamIndex < 0) {
return false; return;
} }
this.useTemporarilyTeamIndex = -1; this.useTemporarilyTeamIndex = -1;
this.temporaryTeam = null; this.temporaryTeam = null;
this.updateTeamEntities(null); this.updateTeamEntities(null);
return true;
} }
public synchronized void setCurrentTeam(int teamId) { public synchronized void setCurrentTeam(int teamId) {
@ -835,13 +812,20 @@ public final class TeamManager extends BasePlayerDataManager {
// TODO: Perhaps find a way to get vanilla experience? // TODO: Perhaps find a way to get vanilla experience?
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
} else { } else {
// Find replacement avatar // Replacement avatar
int replaceIndex = getDeadAvatarReplacement(); EntityAvatar replacement = null;
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) { int replaceIndex = -1;
// Set index and spawn replacement member
this.setCurrentCharacterIndex(replaceIndex); for (int i = 0; i < this.getActiveTeam().size(); i++) {
this.getPlayer().getScene().addEntity(this.getActiveTeam().get(replaceIndex)); EntityAvatar entity = this.getActiveTeam().get(i);
} else { if (entity.isAlive()) {
replaceIndex = i;
replacement = entity;
break;
}
}
if (replacement == null) {
// No more living team members... // No more living team members...
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
// Invoke player team death event. // Invoke player team death event.
@ -849,6 +833,10 @@ public final class TeamManager extends BasePlayerDataManager {
new PlayerTeamDeathEvent( new PlayerTeamDeathEvent(
this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex())); this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex()));
event.call(); event.call();
} else {
// Set index and spawn replacement member
this.setCurrentCharacterIndex(replaceIndex);
this.getPlayer().getScene().addEntity(replacement);
} }
} }
@ -856,20 +844,6 @@ public final class TeamManager extends BasePlayerDataManager {
this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0)); this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0));
} }
public int getDeadAvatarReplacement() {
int replaceIndex = -1;
for (int i = 0; i < this.getActiveTeam().size(); i++) {
EntityAvatar entity = this.getActiveTeam().get(i);
if (entity.isAlive()) {
replaceIndex = i;
break;
}
}
return replaceIndex;
}
public boolean reviveAvatar(Avatar avatar) { public boolean reviveAvatar(Avatar avatar) {
for (EntityAvatar entity : this.getActiveTeam()) { for (EntityAvatar entity : this.getActiveTeam()) {
if (entity.getAvatar() == avatar) { if (entity.getAvatar() == avatar) {
@ -990,11 +964,13 @@ public final class TeamManager extends BasePlayerDataManager {
return respawnPoint.get().getPointData().getTranPos(); return respawnPoint.get().getPointData().getTranPos();
} }
/**
* Performs a bulk save operation on all avatars.
*/
public void saveAvatars() { public void saveAvatars() {
// Save all avatars from active team Database.saveAll(this.getActiveTeam().stream()
for (EntityAvatar entity : this.getActiveTeam()) { .map(EntityAvatar::getAvatar)
entity.getAvatar().save(); .toList());
}
} }
public void onPlayerLogin() { // Hack for now to fix resonances on login public void onPlayerLogin() { // Hack for now to fix resonances on login

View File

@ -1,8 +1,5 @@
package emu.grasscutter.game.quest; package emu.grasscutter.game.quest;
import static emu.grasscutter.GameConstants.DEBUG;
import static emu.grasscutter.config.Configuration.*;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*; import emu.grasscutter.data.binout.*;
@ -15,12 +12,16 @@ import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.*;
import javax.annotation.Nonnull;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import lombok.*; import static emu.grasscutter.GameConstants.DEBUG;
import static emu.grasscutter.config.Configuration.*;
public final class QuestManager extends BasePlayerManager { public final class QuestManager extends BasePlayerManager {
@Getter private final Player player; @Getter private final Player player;
@ -221,11 +222,14 @@ public final class QuestManager extends BasePlayerManager {
this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords())); this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords()));
} }
public void onLogin() { public void onPlayerBorn() {
if (this.isQuestingEnabled()) { if (this.isQuestingEnabled()) {
this.enableQuests(); this.enableQuests();
this.sendGivingRecords(); this.sendGivingRecords();
} }
}
public void onLogin() {
List<GameMainQuest> activeQuests = getActiveMainQuests(); List<GameMainQuest> activeQuests = getActiveMainQuests();
List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size()); List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size());
@ -295,17 +299,6 @@ public final class QuestManager extends BasePlayerManager {
} }
public void enableQuests() { public void enableQuests() {
GameData.getBeginCondQuestMap()
.keySet()
.forEach(
x -> {
if (x.contains("QUEST_COND_STATE_NOT_EQUAL"))
this.triggerEvent(
QuestCond.QUEST_COND_STATE_NOT_EQUAL, null, Integer.parseInt(x.substring(26)));
if (x.contains("QUEST_COND_STATE_EQUAL"))
this.triggerEvent(
QuestCond.QUEST_COND_STATE_EQUAL, null, Integer.parseInt(x.substring(22)));
});
this.triggerEvent(QuestCond.QUEST_COND_NONE, null, 0); this.triggerEvent(QuestCond.QUEST_COND_NONE, null, 0);
this.triggerEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, null, 1); this.triggerEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, null, 1);
} }
@ -577,7 +570,7 @@ public final class QuestManager extends BasePlayerManager {
* @param quest The ID of the quest. * @param quest The ID of the quest.
*/ */
public void checkQuestAlreadyFulfilled(GameQuest quest) { public void checkQuestAlreadyFulfilled(GameQuest quest) {
Grasscutter.getThreadPool() eventExecutor
.submit( .submit(
() -> { () -> {
for (var condition : quest.getQuestData().getFinishCond()) { for (var condition : quest.getQuestData().getFinishCond()) {

View File

@ -1,21 +0,0 @@
package emu.grasscutter.game.quest.conditions;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.QuestValueCond;
import emu.grasscutter.game.quest.enums.QuestCond;
@QuestValueCond(QuestCond.QUEST_COND_MAIN_COOP_START)
public class ConditionMainCoopStart extends BaseCondition {
@Override
public boolean execute(
Player owner,
QuestData questData,
QuestData.QuestAcceptCondition condition,
String paramStr,
int... params) {
return condition.getParam()[0] == params[0]
&& (condition.getParam()[1] == 0 || condition.getParam()[1] == params[1]);
}
}

View File

@ -20,9 +20,6 @@ public class ConditionStateEqual extends BaseCondition {
var questStateValue = condition.getParam()[1]; var questStateValue = condition.getParam()[1];
var checkQuest = owner.getQuestManager().getQuestById(questId); var checkQuest = owner.getQuestManager().getQuestById(questId);
if (checkQuest == null) { return checkQuest != null && checkQuest.getState().getValue() == questStateValue;
return questStateValue == 0;
}
return checkQuest.getState().getValue() == questStateValue;
} }
} }

View File

@ -20,9 +20,6 @@ public class ConditionStateNotEqual extends BaseCondition {
var questStateValue = condition.getParam()[1]; var questStateValue = condition.getParam()[1];
var checkQuest = owner.getQuestManager().getQuestById(questId); var checkQuest = owner.getQuestManager().getQuestById(questId);
if (checkQuest == null) { return checkQuest != null && checkQuest.getState().getValue() != questStateValue;
return questStateValue != 0;
}
return checkQuest.getState().getValue() != questStateValue;
} }
} }

View File

@ -54,7 +54,7 @@ public enum QuestCond implements QuestTrigger {
QUEST_COND_QUEST_GLOBAL_VAR_LESS(46), QUEST_COND_QUEST_GLOBAL_VAR_LESS(46),
QUEST_COND_PERSONAL_LINE_UNLOCK(47), QUEST_COND_PERSONAL_LINE_UNLOCK(47),
QUEST_COND_CITY_REPUTATION_REQUEST(48), // missing QUEST_COND_CITY_REPUTATION_REQUEST(48), // missing
QUEST_COND_MAIN_COOP_START(49), QUEST_COND_MAIN_COOP_START(49), // missing
QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT(50), // missing QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT(50), // missing
QUEST_COND_CITY_REPUTATION_LEVEL(51), // missing, only NPC groups QUEST_COND_CITY_REPUTATION_LEVEL(51), // missing, only NPC groups
QUEST_COND_CITY_REPUTATION_UNLOCK(52), // missing, currently unused QUEST_COND_CITY_REPUTATION_UNLOCK(52), // missing, currently unused

View File

@ -25,10 +25,6 @@ public class TowerLevelRecord {
return this; return this;
} }
public int getLevelStars(int levelId) {
return passedLevelMap.get(levelId);
}
public int getStarCount() { public int getStarCount() {
return passedLevelMap.values().stream().mapToInt(Integer::intValue).sum(); return passedLevelMap.values().stream().mapToInt(Integer::intValue).sum();
} }

View File

@ -1,22 +1,16 @@
package emu.grasscutter.game.tower; package emu.grasscutter.game.tower;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.tower.TowerLevelData; import emu.grasscutter.data.excels.tower.TowerLevelData;
import emu.grasscutter.game.dungeons.*; import emu.grasscutter.game.dungeons.*;
import emu.grasscutter.game.player.*; import emu.grasscutter.game.player.*;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import java.util.*; import java.util.*;
import lombok.*;
public class TowerManager extends BasePlayerManager { public class TowerManager extends BasePlayerManager {
private static final List<DungeonSettleListener> towerDungeonSettleListener = private static final List<DungeonSettleListener> towerDungeonSettleListener =
List.of(new TowerDungeonSettleListener()); List.of(new TowerDungeonSettleListener());
private int currentPossibleStars = 0;
@Getter private boolean inProgress;
@Getter private int currentTimeLimit;
public TowerManager(Player player) { public TowerManager(Player player) {
super(player); super(player);
} }
@ -29,11 +23,6 @@ public class TowerManager extends BasePlayerManager {
return this.getTowerData().currentFloorId; return this.getTowerData().currentFloorId;
} }
/** floor number: 1 - 12 * */
public int getCurrentFloorNumber() {
return GameData.getTowerFloorDataMap().get(getCurrentFloorId()).getFloorIndex();
}
public int getCurrentLevelId() { public int getCurrentLevelId() {
return this.getTowerData().currentLevelId + this.getTowerData().currentLevel; return this.getTowerData().currentLevelId + this.getTowerData().currentLevel;
} }
@ -43,32 +32,6 @@ public class TowerManager extends BasePlayerManager {
return this.getTowerData().currentLevel + 1; return this.getTowerData().currentLevel + 1;
} }
public void onTick() {
var challenge = player.getScene().getChallenge();
if (!inProgress || challenge == null || !challenge.inProgress()) return;
// Check star conditions and notify client if any failed.
int stars = getCurLevelStars();
while (stars < currentPossibleStars) {
player
.getSession()
.send(
new PacketTowerLevelStarCondNotify(
getTowerData().currentFloorId, getCurrentLevel(), currentPossibleStars));
currentPossibleStars--;
}
}
public void onBegin() {
var challenge = player.getScene().getChallenge();
inProgress = true;
currentTimeLimit = challenge.getTimeLimit();
}
public void onEnd() {
inProgress = false;
}
public Map<Integer, TowerLevelRecord> getRecordMap() { public Map<Integer, TowerLevelRecord> getRecordMap() {
Map<Integer, TowerLevelRecord> recordMap = getTowerData().recordMap; Map<Integer, TowerLevelRecord> recordMap = getTowerData().recordMap;
if (recordMap == null || recordMap.size() == 0) { if (recordMap == null || recordMap.size() == 0) {
@ -98,17 +61,8 @@ public class TowerManager extends BasePlayerManager {
player.getTeamManager().setupTemporaryTeam(towerTeams); 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) { public void enterLevel(int enterPointId) {
var levelData = getCurrentTowerLevelDataMap(); var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
var dungeonId = levelData.getDungeonId(); var dungeonId = levelData.getDungeonId();
@ -130,12 +84,9 @@ public class TowerManager extends BasePlayerManager {
// stop using skill // stop using skill
player.getSession().send(new PacketCanUseSkillNotify(false)); player.getSession().send(new PacketCanUseSkillNotify(false));
// notify the cond of stars // notify the cond of stars
currentPossibleStars = 3;
player player
.getSession() .getSession()
.send( .send(new PacketTowerLevelStarCondNotify(getTowerData().currentFloorId, getCurrentLevel()));
new PacketTowerLevelStarCondNotify(
getTowerData().currentFloorId, getCurrentLevel(), currentPossibleStars + 1));
} }
public void notifyCurLevelRecordChange() { public void notifyCurLevelRecordChange() {
@ -146,44 +97,6 @@ public class TowerManager extends BasePlayerManager {
getTowerData().currentFloorId, getCurrentLevel())); getTowerData().currentFloorId, getCurrentLevel()));
} }
public int getCurLevelStars() {
var scene = player.getScene();
var challenge = scene.getChallenge();
if (challenge == null) {
Grasscutter.getLogger().error("getCurLevelStars: no challenge registered!");
return 0;
}
var levelData = getCurrentTowerLevelDataMap();
// 0-based indexing. "star" = 0 means checking for 1-star conditions.
int star;
for (star = 2; star >= 0; star--) {
var cond = levelData.getCondType(star);
if (cond == TowerLevelData.TowerCondType.TOWER_COND_CHALLENGE_LEFT_TIME_MORE_THAN) {
var params = levelData.getTimeCond(star);
var timeRemaining =
challenge.getTimeLimit() - (scene.getSceneTimeSeconds() - challenge.getStartedAt());
if (timeRemaining >= params.getMinimumTimeInSeconds()) {
break;
}
} else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) {
var params = levelData.getHpCond(star);
var hpPercent = challenge.getGuardEntityHpPercent();
if (hpPercent >= params.getMinimumHpPercentage()) {
break;
}
} else {
Grasscutter.getLogger()
.error(
"getCurLevelStars: Tower level {} has no or unknown condition defined for {} stars",
getCurrentLevelId(),
star + 1);
continue;
}
}
return star + 1;
}
public void notifyCurLevelRecordChangeWhenDone(int stars) { public void notifyCurLevelRecordChangeWhenDone(int stars) {
Map<Integer, TowerLevelRecord> recordMap = this.getRecordMap(); Map<Integer, TowerLevelRecord> recordMap = this.getRecordMap();
int currentFloorId = getTowerData().currentFloorId; int currentFloorId = getTowerData().currentFloorId;
@ -192,16 +105,8 @@ public class TowerManager extends BasePlayerManager {
currentFloorId, currentFloorId,
new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(), stars)); new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
} else { } else {
// Only update record if better than previous recordMap.put(
var prevRecord = recordMap.get(currentFloorId); currentFloorId, recordMap.get(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
var passedLevelMap = prevRecord.getPassedLevelMap();
int prevStars = 0;
if (passedLevelMap.containsKey(getCurrentLevelId())) {
prevStars = prevRecord.getLevelStars(getCurrentLevelId());
}
if (stars > prevStars) {
recordMap.put(currentFloorId, prevRecord.setLevelStars(getCurrentLevelId(), stars));
}
} }
this.getTowerData().currentLevel++; this.getTowerData().currentLevel++;

View File

@ -5,13 +5,6 @@ import lombok.Data;
@Data @Data
public class GroupReplacementData { public class GroupReplacementData {
public int id; int id;
public List<Integer> replace_groups; List<Integer> replace_groups;
public GroupReplacementData() {}
public GroupReplacementData(int id, List<Integer> replace_groups) {
this.id = id;
this.replace_groups = replace_groups;
}
} }

View File

@ -158,7 +158,7 @@ public class Scene {
return entity; return entity;
} }
public GameEntity getFirstEntityByConfigId(int configId) { public GameEntity getEntityByConfigId(int configId) {
return this.entities.values().stream() return this.entities.values().stream()
.filter(x -> x.getConfigId() == configId) .filter(x -> x.getConfigId() == configId)
.findFirst() .findFirst()
@ -597,13 +597,6 @@ public class Scene {
blossomManager.onTick(); blossomManager.onTick();
// Should be OK to check only player 0,
// as no other players could enter Tower
var towerManager = getPlayers().get(0).getTowerManager();
if (towerManager != null && towerManager.isInProgress()) {
towerManager.onTick();
}
this.checkNpcGroup(); this.checkNpcGroup();
this.finishLoading(); this.finishLoading();
@ -767,19 +760,6 @@ public class Scene {
return level; 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() { public void checkNpcGroup() {
Set<SceneNpcBornEntry> npcBornEntries = ConcurrentHashMap.newKeySet(); Set<SceneNpcBornEntry> npcBornEntries = ConcurrentHashMap.newKeySet();
for (Player player : this.getPlayers()) { for (Player player : this.getPlayers()) {
@ -836,8 +816,8 @@ public class Scene {
int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride); int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride);
EntityMonster monster = EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level);
new EntityMonster(this, data, entry.getPos(), entry.getRot(), level); monster.getRotation().set(entry.getRot());
monster.setGroupId(entry.getGroup().getGroupId()); monster.setGroupId(entry.getGroup().getGroupId());
monster.setPoseId(entry.getPoseId()); monster.setPoseId(entry.getPoseId());
monster.setConfigId(entry.getConfigId()); monster.setConfigId(entry.getConfigId());
@ -1123,9 +1103,6 @@ public class Scene {
if (group.regions != null) { if (group.regions != null) {
group.regions.values().forEach(getScriptManager()::deregisterRegion); group.regions.values().forEach(getScriptManager()::deregisterRegion);
} }
if (challenge != null && group.id == challenge.getGroup().id) {
challenge.fail();
}
scriptManager.getLoadedGroupSetPerBlock().get(block.id).remove(group); scriptManager.getLoadedGroupSetPerBlock().get(block.id).remove(group);
this.loadedGroups.remove(group); this.loadedGroups.remove(group);

View File

@ -1,23 +1,15 @@
package emu.grasscutter.game.world; package emu.grasscutter.game.world;
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.dungeon.DungeonData; import emu.grasscutter.data.excels.dungeon.DungeonData;
import emu.grasscutter.game.entity.EntityTeam; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.entity.EntityWorld;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.player.Player.SceneLoadState; import emu.grasscutter.game.player.Player.SceneLoadState;
import emu.grasscutter.game.props.EnterReason; import emu.grasscutter.game.props.*;
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.quest.enums.QuestContent;
import emu.grasscutter.game.world.data.TeleportProperties; import emu.grasscutter.game.world.data.TeleportProperties;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHint; import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.*;
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHintType;
import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType;
import emu.grasscutter.scripts.data.SceneConfig; import emu.grasscutter.scripts.data.SceneConfig;
import emu.grasscutter.server.event.player.PlayerTeleportEvent; import emu.grasscutter.server.event.player.PlayerTeleportEvent;
@ -25,23 +17,14 @@ import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.ConversionUtils; import emu.grasscutter.utils.ConversionUtils;
import io.netty.util.concurrent.FastThreadLocalThread; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import lombok.*;
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; import org.jetbrains.annotations.NotNull;
import java.util.*;
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
public class World implements Iterable<Player> { public class World implements Iterable<Player> {
@Getter private final GameServer server; @Getter private final GameServer server;
@Getter private Player host; @Getter private Player host;
@ -61,16 +44,6 @@ public class World implements Iterable<Player> {
@Getter private boolean isPaused = false; @Getter private boolean isPaused = false;
@Getter private long currentWorldTime; @Getter private long currentWorldTime;
private static final ExecutorService eventExecutor =
new ThreadPoolExecutor(
4,
4,
60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(1000),
FastThreadLocalThread::new,
new ThreadPoolExecutor.AbortPolicy());
public World(Player player) { public World(Player player) {
this(player, false); this(player, false);
} }
@ -100,8 +73,6 @@ public class World implements Iterable<Player> {
this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
this.entity = new EntityWorld(this); this.entity = new EntityWorld(this);
this.lastUpdateTime = System.currentTimeMillis(); this.lastUpdateTime = System.currentTimeMillis();
server.registerWorld(this);
} }
public int getLevelEntityId() { public int getLevelEntityId() {
@ -139,7 +110,7 @@ public class World implements Iterable<Player> {
* @param sceneId The scene ID. * @param sceneId The scene ID.
* @return The scene. * @return The scene.
*/ */
@Nullable public Scene getSceneById(int sceneId) { public Scene getSceneById(int sceneId) {
// Get scene normally // Get scene normally
var scene = this.getScenes().get(sceneId); var scene = this.getScenes().get(sceneId);
if (scene != null) { if (scene != null) {
@ -167,7 +138,7 @@ public class World implements Iterable<Player> {
* @param idType The entity type. * @param idType The entity type.
* @return The next entity ID. * @return The next entity ID.
*/ */
public synchronized int getNextEntityId(EntityIdType idType) { public int getNextEntityId(EntityIdType idType) {
return (idType.getId() << 24) + ++this.nextEntityId; return (idType.getId() << 24) + ++this.nextEntityId;
} }
@ -296,7 +267,7 @@ public class World implements Iterable<Player> {
scene.removePlayer(player); scene.removePlayer(player);
// Info packet for other players // Info packet for other players
if (this.getPlayers().size() > 0) { if (!this.getPlayers().isEmpty()) {
this.updatePlayerInfos(player); this.updatePlayerInfos(player);
} }
@ -339,21 +310,6 @@ public class World implements Iterable<Player> {
this.getScenes().values().forEach(Scene::saveGroups); this.getScenes().values().forEach(Scene::saveGroups);
} }
public void queueTransferPlayerToScene(Player player, int sceneId, Position pos, int delayMs) {
player.setQueuedTeleport(
eventExecutor.submit(
() -> {
try {
Thread.sleep(delayMs);
transferPlayerToScene(player, sceneId, pos);
} catch (InterruptedException e) {
Grasscutter.getLogger()
.trace(
"queueTransferPlayerToScene: teleport to scene {} is interrupted", sceneId);
}
}));
}
public boolean transferPlayerToScene(Player player, int sceneId, Position pos) { public boolean transferPlayerToScene(Player player, int sceneId, Position pos) {
return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos); return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos);
} }
@ -424,16 +380,6 @@ public class World implements Iterable<Player> {
} }
public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) { public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) {
// If a queued teleport already exists, cancel it. This prevents the player from
// becoming stranded in a dungeon due to quitting it by teleporting to a map waypoint.
synchronized (player) {
var queuedTeleport = player.getQueuedTeleport();
if (queuedTeleport != null) {
player.setQueuedTeleport(null);
queuedTeleport.cancel(true);
}
}
// Check if the teleport properties are valid. // Check if the teleport properties are valid.
if (teleportProperties.getTeleportTo() == null) if (teleportProperties.getTeleportTo() == null)
teleportProperties.setTeleportTo(player.getPosition()); teleportProperties.setTeleportTo(player.getPosition());
@ -451,50 +397,37 @@ public class World implements Iterable<Player> {
return false; return false;
} }
Scene oldScene = player.getScene(); Scene oldScene = null;
var newScene = this.getSceneById(teleportProperties.getSceneId()); if (player.getScene() != null) {
oldScene = player.getScene();
// Move directly in the same scene.
if (newScene == oldScene && teleportProperties.getTeleportType() == TeleportType.COMMAND) {
// Set player position and rotation
if (teleportProperties.getTeleportTo() != null) {
player.getPosition().set(teleportProperties.getTeleportTo());
}
if (teleportProperties.getTeleportRot() != null) {
player.getRotation().set(teleportProperties.getTeleportRot());
}
player.sendPacket(new PacketSceneEntityAppearNotify(player));
return true;
}
if (oldScene != null) {
// Don't deregister scenes if the player is going to tp back into them // Don't deregister scenes if the player is going to tp back into them
if (oldScene == newScene) { if (oldScene.getId() == teleportProperties.getSceneId()) {
oldScene.setDontDestroyWhenEmpty(true); oldScene.setDontDestroyWhenEmpty(true);
} }
oldScene.removePlayer(player); oldScene.removePlayer(player);
} }
if (newScene != null) { var newScene = this.getSceneById(teleportProperties.getSceneId());
newScene.addPlayer(player); newScene.addPlayer(player);
player.getTeamManager().applyAbilities(newScene); player.getTeamManager().applyAbilities(newScene);
// Dungeon // Dungeon
// Dungeon system is handling this already // Dungeon system is handling this already
// if(dungeonData!=null){ // if(dungeonData!=null){
// var dungeonManager = new DungeonManager(newScene, dungeonData); // var dungeonManager = new DungeonManager(newScene, dungeonData);
// dungeonManager.startDungeon(); // dungeonManager.startDungeon();
// } // }
SceneConfig config = newScene.getScriptManager().getConfig(); SceneConfig config = newScene.getScriptManager().getConfig();
if (teleportProperties.getTeleportTo() == null && config != null) { if (teleportProperties.getTeleportTo() == null && config != null) {
if (config.born_pos != null) { if (config.born_pos != null) {
teleportProperties.setTeleportTo(config.born_pos); teleportProperties.setTeleportTo(config.born_pos);
} }
if (config.born_rot != null) { if (config.born_rot != null) {
teleportProperties.setTeleportRot(config.born_rot); teleportProperties.setTeleportRot(config.born_rot);
}
} }
} }
@ -506,7 +439,7 @@ public class World implements Iterable<Player> {
player.getRotation().set(teleportProperties.getTeleportRot()); player.getRotation().set(teleportProperties.getTeleportRot());
} }
if (oldScene != null && newScene != null && newScene != oldScene) { if (oldScene != null && newScene != oldScene) {
newScene.setPrevScenePoint(oldScene.getPrevScenePoint()); newScene.setPrevScenePoint(oldScene.getPrevScenePoint());
oldScene.setDontDestroyWhenEmpty(false); oldScene.setDontDestroyWhenEmpty(false);
} }

View File

@ -0,0 +1,22 @@
package emu.grasscutter.net;
public interface KcpChannel {
/**
* Event fired when the client connects.
*
* @param tunnel The tunnel.
*/
void onConnected(KcpTunnel tunnel);
/**
* Event fired when the client disconnects.
*/
void onDisconnected();
/**
* Event fired when data is received from the client.
*
* @param bytes The data received.
*/
void onMessage(byte[] bytes);
}

View File

@ -0,0 +1,22 @@
package emu.grasscutter.net;
import java.net.InetSocketAddress;
public interface KcpTunnel {
/**
* @return The address of the client.
*/
InetSocketAddress getAddress();
/**
* Sends bytes to the client.
*
* @param bytes The bytes to send.
*/
void writeData(byte[] bytes);
/**
* Closes the connection.
*/
void close();
}

View File

@ -17,7 +17,6 @@ import emu.grasscutter.net.proto.VisionTypeOuterClass;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.*; import emu.grasscutter.scripts.data.*;
import emu.grasscutter.scripts.service.*; import emu.grasscutter.scripts.service.*;
import emu.grasscutter.server.event.game.SceneMetaLoadEvent;
import emu.grasscutter.server.packet.send.PacketGroupSuiteNotify; import emu.grasscutter.server.packet.send.PacketGroupSuiteNotify;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
@ -39,14 +38,12 @@ public class SceneScriptManager {
private final Map<String, Integer> variables; private final Map<String, Integer> variables;
private SceneMeta meta; private SceneMeta meta;
private boolean isInit; private boolean isInit;
private boolean noCacheGroupGridsToDisk;
private final Map<String, SceneTimeAxis> timeAxis = new ConcurrentHashMap<>(); private final Map<String, SceneTimeAxis> timeAxis = new ConcurrentHashMap<>();
/** current triggers controlled by RefreshGroup */ /** current triggers controlled by RefreshGroup */
private final Map<Integer, Set<SceneTrigger>> currentTriggers; private final Map<Integer, Set<SceneTrigger>> currentTriggers;
private final Set<SceneTrigger> ongoingTriggers;
private final Map<String, Set<SceneTrigger>> triggersByGroupScene; private final Map<String, Set<SceneTrigger>> triggersByGroupScene;
private final Map<Integer, Set<Pair<String, Integer>>> activeGroupTimers; private final Map<Integer, Set<Pair<String, Integer>>> activeGroupTimers;
private final Map<String, AtomicInteger> triggerInvocations; private final Map<String, AtomicInteger> triggerInvocations;
@ -77,7 +74,6 @@ public class SceneScriptManager {
public SceneScriptManager(Scene scene) { public SceneScriptManager(Scene scene) {
this.scene = scene; this.scene = scene;
this.currentTriggers = new ConcurrentHashMap<>(); this.currentTriggers = new ConcurrentHashMap<>();
this.ongoingTriggers = ConcurrentHashMap.newKeySet();
this.triggersByGroupScene = new ConcurrentHashMap<>(); this.triggersByGroupScene = new ConcurrentHashMap<>();
this.activeGroupTimers = new ConcurrentHashMap<>(); this.activeGroupTimers = new ConcurrentHashMap<>();
this.triggerInvocations = new ConcurrentHashMap<>(); this.triggerInvocations = new ConcurrentHashMap<>();
@ -138,9 +134,7 @@ public class SceneScriptManager {
public void registerTrigger(SceneTrigger trigger) { public void registerTrigger(SceneTrigger trigger) {
this.triggerInvocations.put(trigger.getName(), new AtomicInteger(0)); this.triggerInvocations.put(trigger.getName(), new AtomicInteger(0));
this.getTriggersByEvent(trigger.getEvent()).add(trigger); this.getTriggersByEvent(trigger.getEvent()).add(trigger);
Grasscutter.getLogger() Grasscutter.getLogger().trace("Registered trigger {}", trigger.getName());
.trace(
"Registered trigger {} from group {}", trigger.getName(), trigger.getCurrentGroup().id);
} }
public void deregisterTrigger(List<SceneTrigger> triggers) { public void deregisterTrigger(List<SceneTrigger> triggers) {
@ -149,11 +143,7 @@ public class SceneScriptManager {
public void deregisterTrigger(SceneTrigger trigger) { public void deregisterTrigger(SceneTrigger trigger) {
this.getTriggersByEvent(trigger.getEvent()).remove(trigger); this.getTriggersByEvent(trigger.getEvent()).remove(trigger);
Grasscutter.getLogger() Grasscutter.getLogger().trace("deregistered trigger {}", trigger.getName());
.trace(
"deregistered trigger {} from group {}",
trigger.getName(),
trigger.getCurrentGroup().id);
} }
public void resetTriggers(int eventId) { public void resetTriggers(int eventId) {
@ -266,15 +256,6 @@ public class SceneScriptManager {
this.addGroupSuite(groupInstance, suiteData, entitiesAdded); 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 // Refesh variables here
group.variables.forEach( group.variables.forEach(
variable -> { variable -> {
@ -284,10 +265,6 @@ public class SceneScriptManager {
groupInstance.setActiveSuiteId(suiteIndex); groupInstance.setActiveSuiteId(suiteIndex);
groupInstance.setLastTimeRefreshed(getScene().getWorld().getGameTime()); groupInstance.setLastTimeRefreshed(getScene().getWorld().getGameTime());
// Call EVENT_GROUP_REFRESH for any action trigger waiting for it
callEvent(new ScriptArgs(groupInstance.getGroupId(), EventType.EVENT_GROUP_REFRESH));
return suiteIndex; return suiteIndex;
} }
@ -346,7 +323,7 @@ public class SceneScriptManager {
group.monsters.values().stream() group.monsters.values().stream()
.filter( .filter(
m -> { m -> {
var entity = scene.getEntityByConfigId(m.config_id, groupId); var entity = scene.getEntityByConfigId(m.config_id);
return (entity == null return (entity == null
|| entity.getGroupId() || entity.getGroupId()
!= group != group
@ -457,17 +434,6 @@ public class SceneScriptManager {
} }
private void init() { private void init() {
var event = new SceneMetaLoadEvent(getScene());
event.call();
if (event.isOverride()) {
// Group grids should not be cached to disk when a scene
// group override is in effect. Otherwise, when the server
// next runs without that override, the cached content
// will not make sense.
noCacheGroupGridsToDisk = true;
}
var meta = ScriptLoader.getSceneMeta(getScene().getId()); var meta = ScriptLoader.getSceneMeta(getScene().getId());
if (meta == null) { if (meta == null) {
return; return;
@ -485,9 +451,7 @@ public class SceneScriptManager {
return groupGridsCache.get(sceneId); return groupGridsCache.get(sceneId);
} else { } else {
var path = FileUtils.getCachePath("scene" + sceneId + "_grid.json"); var path = FileUtils.getCachePath("scene" + sceneId + "_grid.json");
if (path.toFile().isFile() if (path.toFile().isFile() && !Grasscutter.config.server.game.cacheSceneEntitiesEveryRun) {
&& !Grasscutter.config.server.game.cacheSceneEntitiesEveryRun
&& !noCacheGroupGridsToDisk) {
try { try {
var groupGrids = JsonUtils.loadToList(path, Grid.class); var groupGrids = JsonUtils.loadToList(path, Grid.class);
groupGridsCache.put(sceneId, groupGrids); groupGridsCache.put(sceneId, groupGrids);
@ -617,18 +581,15 @@ public class SceneScriptManager {
} }
groupGridsCache.put(scene.getId(), groupGrids); groupGridsCache.put(scene.getId(), groupGrids);
if (!noCacheGroupGridsToDisk) { try {
try { Files.createDirectories(path.getParent());
Files.createDirectories(path.getParent()); } catch (IOException ignored) {
} catch (IOException ignored) { }
} try (var file = new FileWriter(path.toFile())) {
try (var file = new FileWriter(path.toFile())) { file.write(JsonUtils.encode(groupGrids));
file.write(JsonUtils.encode(groupGrids)); Grasscutter.getLogger().info("Scene {} saved grid file.", getScene().getId());
Grasscutter.getLogger().info("Scene {} saved grid file.", getScene().getId()); } catch (Exception e) {
} catch (Exception e) { Grasscutter.getLogger().error("Scene {} unable to save grid file.", getScene().getId(), e);
Grasscutter.getLogger()
.error("Scene {} unable to save grid file.", getScene().getId(), e);
}
} }
return groupGrids; return groupGrids;
} }
@ -733,7 +694,7 @@ public class SceneScriptManager {
return suite.sceneGadgets.stream() return suite.sceneGadgets.stream()
.filter( .filter(
m -> { m -> {
var entity = scene.getEntityByConfigId(m.config_id, group.id); var entity = scene.getEntityByConfigId(m.config_id);
return (entity == null || entity.getGroupId() != group.id) return (entity == null || entity.getGroupId() != group.id)
&& (!m.isOneoff && (!m.isOneoff
|| !m.persistent || !m.persistent
@ -751,7 +712,7 @@ public class SceneScriptManager {
return suite.sceneMonsters.stream() return suite.sceneMonsters.stream()
.filter( .filter(
m -> { m -> {
var entity = scene.getEntityByConfigId(m.config_id, group.id); var entity = scene.getEntityByConfigId(m.config_id);
return (entity == null return (entity == null
|| entity.getGroupId() || entity.getGroupId()
!= group != group
@ -813,9 +774,9 @@ public class SceneScriptManager {
} }
public void startMonsterTideInGroup( public void startMonsterTideInGroup(
String source, SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) { SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
this.scriptMonsterTideService = this.scriptMonsterTideService =
new ScriptMonsterTideService(this, source, group, tideCount, sceneLimit, ordersConfigId); new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
} }
public void unloadCurrentMonsterTide() { public void unloadCurrentMonsterTide() {
@ -827,7 +788,7 @@ public class SceneScriptManager {
public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) { public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) {
// TODO delay // TODO delay
var entity = scene.getEntityByConfigId(configId, group.id); var entity = scene.getEntityByConfigId(configId);
if (entity != null && entity.getGroupId() == group.id) { if (entity != null && entity.getGroupId() == group.id) {
Grasscutter.getLogger() Grasscutter.getLogger()
.debug("entity already exists failed in group {} with config {}", group.id, configId); .debug("entity already exists failed in group {} with config {}", group.id, configId);
@ -842,11 +803,11 @@ public class SceneScriptManager {
} }
} }
// Events // Events
public Future<?> callEvent(int groupId, int eventType) { public void callEvent(int groupId, int eventType) {
return callEvent(new ScriptArgs(groupId, eventType)); callEvent(new ScriptArgs(groupId, eventType));
} }
public Future<?> callEvent(@Nonnull ScriptArgs params) { public void callEvent(@Nonnull ScriptArgs params) {
/** /**
* We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for * We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for
* every groups' trigger in every scene instances. But when callEvent is called in a ScriptLib * every groups' trigger in every scene instances. But when callEvent is called in a ScriptLib
@ -854,7 +815,7 @@ public class SceneScriptManager {
* not get it. e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE -> * not get it. e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE ->
* (remove) So we use thread pool to clean the stack to avoid this new issue. * (remove) So we use thread pool to clean the stack to avoid this new issue.
*/ */
return eventExecutor.submit(() -> this.realCallEvent(params)); eventExecutor.submit(() -> this.realCallEvent(params));
} }
private void realCallEvent(@Nonnull ScriptArgs params) { private void realCallEvent(@Nonnull ScriptArgs params) {
@ -923,11 +884,9 @@ public class SceneScriptManager {
private boolean evaluateTriggerCondition(SceneTrigger trigger, ScriptArgs params) { private boolean evaluateTriggerCondition(SceneTrigger trigger, ScriptArgs params) {
Grasscutter.getLogger() Grasscutter.getLogger()
.trace( .trace(
"Call Condition Trigger {}, [{},{},{}], source_eid {}, target_eid {}", "Call Condition Trigger {}, [{},{},{}]",
trigger.getCondition(), trigger.getCondition(),
params.param1, params.param1,
params.param2,
params.param3,
params.source_eid, params.source_eid,
params.target_eid); params.target_eid);
LuaValue ret = this.callScriptFunc(trigger.getCondition(), trigger.currentGroup, params); LuaValue ret = this.callScriptFunc(trigger.getCondition(), trigger.currentGroup, params);
@ -936,7 +895,6 @@ public class SceneScriptManager {
private void callTrigger(SceneTrigger trigger, ScriptArgs params) { private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time // 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 ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params);
var invocationsCounter = triggerInvocations.get(trigger.getName()); var invocationsCounter = triggerInvocations.get(trigger.getName());
var invocations = invocationsCounter.incrementAndGet(); var invocations = invocationsCounter.incrementAndGet();
@ -968,15 +926,11 @@ public class SceneScriptManager {
} }
// always deregister on error, otherwise only if the count is reached // always deregister on error, otherwise only if the count is reached
// or the trigger should be preserved after a RefreshGroup call if (ret.isboolean() && !ret.checkboolean()
if (trigger.isPreserved()) {
trigger.setPreserved(false);
} else if (ret.isboolean() && !ret.checkboolean()
|| ret.isint() && ret.checkint() != 0 || ret.isint() && ret.checkint() != 0
|| trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) { || trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) {
deregisterTrigger(trigger); deregisterTrigger(trigger);
} }
ongoingTriggers.remove(trigger);
} }
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) { private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
@ -1069,10 +1023,22 @@ public class SceneScriptManager {
} }
// Calculate level // Calculate level
int level = getScene().getLevelForMonster(monster.config_id, monster.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();
}
}
// Spawn mob // Spawn mob
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level); EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level);
entity.getRotation().set(monster.rot);
entity.setGroupId(groupId); entity.setGroupId(groupId);
entity.setBlockId(blockId); entity.setBlockId(blockId);
entity.setConfigId(monster.config_id); entity.setConfigId(monster.config_id);
@ -1109,19 +1075,6 @@ public class SceneScriptManager {
return meta.sceneBlockIndex; 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) { public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet()); var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet());
var toRemove = var toRemove =
@ -1242,7 +1195,7 @@ public class SceneScriptManager {
return monsters.values().stream() return monsters.values().stream()
.noneMatch( .noneMatch(
m -> { m -> {
val entity = scene.getEntityByConfigId(m.config_id, groupId); val entity = scene.getEntityByConfigId(m.config_id);
return entity != null && entity.getGroupId() == groupId; return entity != null && entity.getGroupId() == groupId;
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ import emu.grasscutter.scripts.serializer.*;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.nio.file.*; import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -172,17 +172,6 @@ public class ScriptLoader {
* @return The sources of the script. * @return The sources of the script.
*/ */
public static String readScript(String path) { public static String readScript(String path) {
return readScript(path, false);
}
/**
* Loads the sources of a script.
*
* @param path The path of the script.
* @param useAbsPath Use path as-is; don't look under Scripts resources.
* @return The sources of the script.
*/
public static String readScript(String path, boolean useAbsPath) {
// Check if the path is cached. // Check if the path is cached.
var cached = ScriptLoader.tryGet(ScriptLoader.scriptSources.get(path)); var cached = ScriptLoader.tryGet(ScriptLoader.scriptSources.get(path));
if (cached.isPresent()) { if (cached.isPresent()) {
@ -190,11 +179,8 @@ public class ScriptLoader {
} }
// Attempt to load the script. // Attempt to load the script.
var scriptPath = useAbsPath ? Paths.get(path) : FileUtils.getScriptPath(path); var scriptPath = FileUtils.getScriptPath(path);
if (!Files.exists(scriptPath)) { if (!Files.exists(scriptPath)) return null;
Grasscutter.getLogger().error("Could not find script at path {}", path);
return null;
}
try { try {
var source = Files.readString(scriptPath); var source = Files.readString(scriptPath);
@ -215,17 +201,6 @@ public class ScriptLoader {
* @return The compiled script. * @return The compiled script.
*/ */
public static CompiledScript getScript(String path) { public static CompiledScript getScript(String path) {
return getScript(path, false);
}
/**
* Fetches a script and compiles it, or uses the cached varient.
*
* @param path The path of the script.
* @param useAbsPath Use path as-is; don't look under Scripts resources.
* @return The compiled script.
*/
public static CompiledScript getScript(String path, boolean useAbsPath) {
// Check if the script is cached. // Check if the script is cached.
var sc = ScriptLoader.tryGet(ScriptLoader.scriptsCache.get(path)); var sc = ScriptLoader.tryGet(ScriptLoader.scriptsCache.get(path));
if (sc.isPresent()) { if (sc.isPresent()) {
@ -236,18 +211,15 @@ public class ScriptLoader {
CompiledScript script; CompiledScript script;
if (Configuration.FAST_REQUIRE) { if (Configuration.FAST_REQUIRE) {
// Attempt to load the script. // Attempt to load the script.
var scriptPath = useAbsPath ? Paths.get(path) : FileUtils.getScriptPath(path); var scriptPath = FileUtils.getScriptPath(path);
if (!Files.exists(scriptPath)) { if (!Files.exists(scriptPath)) return null;
Grasscutter.getLogger().error("Could not find script at path {}", path);
return null;
}
// Compile the script from the file. // Compile the script from the file.
var source = Files.newBufferedReader(scriptPath); var source = Files.newBufferedReader(scriptPath);
script = ScriptLoader.getEngine().compile(source); script = ScriptLoader.getEngine().compile(source);
} else { } else {
// Load the script sources. // Load the script sources.
var sources = ScriptLoader.readScript(path, useAbsPath); var sources = ScriptLoader.readScript(path);
if (sources == null) return null; if (sources == null) return null;
// Check to see if the script references other scripts. // Check to see if the script references other scripts.
@ -265,7 +237,7 @@ public class ScriptLoader {
var scriptName = line.substring(9, line.length() - 1); var scriptName = line.substring(9, line.length() - 1);
// Resolve the script path. // Resolve the script path.
var scriptPath = "Common/" + scriptName + ".lua"; var scriptPath = "Common/" + scriptName + ".lua";
var scriptSource = ScriptLoader.readScript(scriptPath, useAbsPath); var scriptSource = ScriptLoader.readScript(scriptPath);
if (scriptSource == null) continue; if (scriptSource == null) continue;
// Append the script source. // Append the script source.

View File

@ -5,7 +5,6 @@ import com.github.davidmoten.rtreemulti.geometry.*;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Position;
import emu.grasscutter.scripts.*; import emu.grasscutter.scripts.*;
import emu.grasscutter.server.event.game.SceneBlockLoadedEvent;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.script.*; import javax.script.*;
@ -65,10 +64,6 @@ public class SceneBlock {
.collect(Collectors.toMap(x -> x.id, y -> y, (a, b) -> a)); .collect(Collectors.toMap(x -> x.id, y -> y, (a, b) -> a));
this.groups.values().forEach(g -> g.block_id = this.id); this.groups.values().forEach(g -> g.block_id = this.id);
var event = new SceneBlockLoadedEvent(this);
event.call();
this.sceneGroupIndex = this.sceneGroupIndex =
SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint()); SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint());
} catch (ScriptException exception) { } catch (ScriptException exception) {

View File

@ -3,7 +3,6 @@ package emu.grasscutter.scripts.data;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Position;
import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.scripts.ScriptLoader;
import java.io.*;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.script.*; import javax.script.*;
@ -40,7 +39,6 @@ public final class SceneGroup {
private transient boolean loaded; private transient boolean loaded;
private transient CompiledScript script; private transient CompiledScript script;
private transient Bindings bindings; private transient Bindings bindings;
public String overrideScriptPath;
public static SceneGroup of(int groupId) { public static SceneGroup of(int groupId) {
var group = new SceneGroup(); var group = new SceneGroup();
@ -88,14 +86,8 @@ public final class SceneGroup {
// Create the bindings. // Create the bindings.
this.bindings = ScriptLoader.getEngine().createBindings(); this.bindings = ScriptLoader.getEngine().createBindings();
CompiledScript cs; var cs =
if (overrideScriptPath != null && !overrideScriptPath.equals("")) { ScriptLoader.getScript("Scene/%s/scene%s_group%s.lua".formatted(sceneId, sceneId, this.id));
cs = ScriptLoader.getScript(overrideScriptPath, true);
} else {
cs =
ScriptLoader.getScript(
"Scene/%s/scene%s_group%s.lua".formatted(sceneId, sceneId, this.id));
}
if (cs == null) { if (cs == null) {
return this; return this;

View File

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

View File

@ -20,11 +20,9 @@ public final class ScriptMonsterTideService {
private final List<Integer> monsterConfigIds; private final List<Integer> monsterConfigIds;
private final OnMonsterCreated onMonsterCreated = new OnMonsterCreated(); private final OnMonsterCreated onMonsterCreated = new OnMonsterCreated();
private final OnMonsterDead onMonsterDead = new OnMonsterDead(); private final OnMonsterDead onMonsterDead = new OnMonsterDead();
private final String source;
public ScriptMonsterTideService( public ScriptMonsterTideService(
SceneScriptManager sceneScriptManager, SceneScriptManager sceneScriptManager,
String source,
SceneGroup group, SceneGroup group,
int tideCount, int tideCount,
int monsterSceneLimit, int monsterSceneLimit,
@ -37,7 +35,6 @@ public final class ScriptMonsterTideService {
this.monsterAlive = new AtomicInteger(0); this.monsterAlive = new AtomicInteger(0);
this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId));
this.monsterConfigIds = List.of(ordersConfigId); this.monsterConfigIds = List.of(ordersConfigId);
this.source = source;
this.sceneScriptManager this.sceneScriptManager
.getScriptMonsterSpawnService() .getScriptMonsterSpawnService()
@ -62,11 +59,7 @@ public final class ScriptMonsterTideService {
public SceneMonster getNextMonster() { public SceneMonster getNextMonster() {
var nextId = this.monsterConfigOrders.poll(); var nextId = this.monsterConfigOrders.poll();
if (nextId == null) { if (currentGroup.monsters.containsKey(nextId)) {
// 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); return currentGroup.monsters.get(nextId);
} }
// TODO some monster config_id do not exist in groups, so temporarily set it to the first // TODO some monster config_id do not exist in groups, so temporarily set it to the first
@ -90,11 +83,11 @@ public final class ScriptMonsterTideService {
sceneScriptManager.createMonster( sceneScriptManager.createMonster(
currentGroup.id, currentGroup.block_id, getNextMonster())); currentGroup.id, currentGroup.block_id, getNextMonster()));
} }
// call registered events that may spawn in more monsters // spawn the last turn of monsters
var scriptArgs = // fix the 5-2
new ScriptArgs(currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get()); sceneScriptManager.callEvent(
scriptArgs.setEventSource(source); new ScriptArgs(
sceneScriptManager.callEvent(scriptArgs); currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get()));
} }
} }

View File

@ -1,16 +0,0 @@
package emu.grasscutter.server.event.game;
import emu.grasscutter.scripts.data.SceneBlock;
import emu.grasscutter.server.event.types.ServerEvent;
import lombok.*;
@Getter
public final class SceneBlockLoadedEvent extends ServerEvent {
private SceneBlock block;
public SceneBlockLoadedEvent(SceneBlock block) {
super(Type.GAME);
this.block = block;
}
}

View File

@ -1,17 +0,0 @@
package emu.grasscutter.server.event.game;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.server.event.types.ServerEvent;
import lombok.*;
@Getter
public final class SceneMetaLoadEvent extends ServerEvent {
private Scene scene;
@Setter private boolean override;
public SceneMetaLoadEvent(Scene scene) {
super(Type.GAME);
this.scene = scene;
}
}

View File

@ -1,55 +1,49 @@
package emu.grasscutter.server.game; package emu.grasscutter.server.game;
import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.*; import emu.grasscutter.*;
import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.game.battlepass.BattlePassSystem; import emu.grasscutter.game.battlepass.BattlePassSystem;
import emu.grasscutter.game.chat.ChatSystem; import emu.grasscutter.game.chat.*;
import emu.grasscutter.game.chat.ChatSystemHandler;
import emu.grasscutter.game.combine.CombineManger; import emu.grasscutter.game.combine.CombineManger;
import emu.grasscutter.game.drop.DropSystem; import emu.grasscutter.game.drop.*;
import emu.grasscutter.game.drop.DropSystemLegacy;
import emu.grasscutter.game.dungeons.DungeonSystem; import emu.grasscutter.game.dungeons.DungeonSystem;
import emu.grasscutter.game.expedition.ExpeditionSystem; import emu.grasscutter.game.expedition.ExpeditionSystem;
import emu.grasscutter.game.gacha.GachaSystem; import emu.grasscutter.game.gacha.GachaSystem;
import emu.grasscutter.game.home.HomeWorld; import emu.grasscutter.game.home.*;
import emu.grasscutter.game.home.HomeWorldMPSystem; import emu.grasscutter.game.managers.cooking.*;
import emu.grasscutter.game.managers.cooking.CookingCompoundManager;
import emu.grasscutter.game.managers.cooking.CookingManager;
import emu.grasscutter.game.managers.energy.EnergyManager; import emu.grasscutter.game.managers.energy.EnergyManager;
import emu.grasscutter.game.managers.stamina.StaminaManager; import emu.grasscutter.game.managers.stamina.StaminaManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.QuestSystem; import emu.grasscutter.game.quest.QuestSystem;
import emu.grasscutter.game.shop.ShopSystem; import emu.grasscutter.game.shop.ShopSystem;
import emu.grasscutter.game.systems.AnnouncementSystem; import emu.grasscutter.game.systems.*;
import emu.grasscutter.game.systems.InventorySystem;
import emu.grasscutter.game.systems.MultiplayerSystem;
import emu.grasscutter.game.talk.TalkSystem; import emu.grasscutter.game.talk.TalkSystem;
import emu.grasscutter.game.tower.TowerSystem; import emu.grasscutter.game.tower.TowerSystem;
import emu.grasscutter.game.world.World; import emu.grasscutter.game.world.*;
import emu.grasscutter.game.world.WorldDataSystem;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.server.dispatch.DispatchClient; import emu.grasscutter.server.dispatch.DispatchClient;
import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.game.ServerTickEvent;
import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.*;
import emu.grasscutter.server.event.internal.ServerStopEvent;
import emu.grasscutter.server.event.types.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import emu.grasscutter.server.scheduler.ServerTaskScheduler; import emu.grasscutter.server.scheduler.ServerTaskScheduler;
import emu.grasscutter.task.TaskMap; import emu.grasscutter.task.TaskMap;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import kcp.highway.*;
import lombok.*;
import org.jetbrains.annotations.*;
import emu.grasscutter.server.game.session.GameSessionManager;
import java.net.*; import java.net.*;
import java.time.*; import java.time.*;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import kcp.highway.*;
import lombok.*; import static emu.grasscutter.config.Configuration.*;
import org.jetbrains.annotations.*; import static emu.grasscutter.utils.lang.Language.translate;
@Getter @Getter
public final class GameServer extends KcpServer implements Iterable<Player> { public final class GameServer extends KcpServer implements Iterable<Player> {
@ -60,6 +54,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
private final Set<World> worlds; private final Set<World> worlds;
private final Int2ObjectMap<HomeWorld> homeWorlds; private final Int2ObjectMap<HomeWorld> homeWorlds;
@Getter private boolean started = false;
@Setter private DispatchClient dispatchClient; @Setter private DispatchClient dispatchClient;
// Server systems // Server systems
@ -140,7 +135,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
channelConfig.setUseConvChannel(true); channelConfig.setUseConvChannel(true);
channelConfig.setAckNoDelay(false); channelConfig.setAckNoDelay(false);
this.init(GameSessionManager.getListener(), channelConfig, address); this.init(GameSessionManager.getInstance(), channelConfig, address);
EnergyManager.initialize(); EnergyManager.initialize();
StaminaManager.initialize(); StaminaManager.initialize();
@ -311,6 +306,11 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
world.save(); // Save the player's world world.save(); // Save the player's world
} }
public void registerHomeWorld(HomeWorld homeWorld) {
this.getHomeWorlds().put(homeWorld.getOwnerUid(), homeWorld);
this.registerWorld(homeWorld);
}
public HomeWorld getHomeWorldOrCreate(Player owner) { public HomeWorld getHomeWorldOrCreate(Player owner) {
return this.getHomeWorlds() return this.getHomeWorlds()
.computeIfAbsent(owner.getUid(), (uid) -> new HomeWorld(this, owner)); .computeIfAbsent(owner.getUid(), (uid) -> new HomeWorld(this, owner));
@ -342,6 +342,8 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
.info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort())); .info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort()));
ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now());
event.call(); event.call();
this.started = true;
} }
public void onServerShutdown() { public void onServerShutdown() {
@ -356,10 +358,10 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
this.stop(); // Stop the server. this.stop(); // Stop the server.
try { try {
var threadPool = GameSessionManager.getLogicThread(); var threadPool = GameSessionManager.getExecutor();
// Shutdown network thread. // Shutdown network thread.
threadPool.shutdownGracefully(); threadPool.shutdown();
// Wait for the network thread to finish. // Wait for the network thread to finish.
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
Grasscutter.getLogger().error("Logic thread did not terminate!"); Grasscutter.getLogger().error("Logic thread did not terminate!");

View File

@ -1,7 +1,5 @@
package emu.grasscutter.server.game; package emu.grasscutter.server.game;
import static emu.grasscutter.config.Configuration.GAME_INFO;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.net.packet.*; import emu.grasscutter.net.packet.*;
@ -9,6 +7,8 @@ import emu.grasscutter.server.event.game.ReceivePacketEvent;
import emu.grasscutter.server.game.GameSession.SessionState; import emu.grasscutter.server.game.GameSession.SessionState;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import static emu.grasscutter.config.Configuration.GAME_INFO;
public final class GameServerPacketHandler { public final class GameServerPacketHandler {
private final Int2ObjectMap<PacketHandler> handlers; private final Int2ObjectMap<PacketHandler> handlers;
@ -76,13 +76,11 @@ public final class GameServerPacketHandler {
} }
// Invoke event. // Invoke event.
ReceivePacketEvent event = new ReceivePacketEvent(session, opcode, payload); var event = new ReceivePacketEvent(session, opcode, payload);
event.call(); if (event.call()) // If event is not canceled, continue.
if (!event.isCanceled()) // If event is not canceled, continue. handler.handle(session, header, event.getPacketData());
handler.handle(session, header, event.getPacketData());
} catch (Exception ex) { } catch (Exception ex) {
// TODO Remove this when no more needed Grasscutter.getLogger().warn("Unable to handle packet.", ex);
ex.printStackTrace();
} }
return; // Packet successfully handled return; // Packet successfully handled
} }

View File

@ -1,24 +1,26 @@
package emu.grasscutter.server.game; package emu.grasscutter.server.game;
import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.*;
import emu.grasscutter.net.packet.*; import emu.grasscutter.net.packet.*;
import emu.grasscutter.server.event.game.SendPacketEvent; import emu.grasscutter.server.event.game.SendPacketEvent;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import io.netty.buffer.*; import io.netty.buffer.*;
import lombok.*;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.file.Path; import java.nio.file.Path;
import lombok.*;
public class GameSession implements GameSessionManager.KcpChannel { import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.lang.Language.translate;
public class GameSession implements KcpChannel {
private final GameServer server; private final GameServer server;
private GameSessionManager.KcpTunnel tunnel; private KcpTunnel tunnel;
@Getter @Setter private Account account; @Getter @Setter private Account account;
@Getter private Player player; @Getter private Player player;
@ -146,7 +148,7 @@ public class GameSession implements GameSessionManager.KcpChannel {
if (packet.shouldEncrypt) { if (packet.shouldEncrypt) {
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey); Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
} }
tunnel.writeData(bytes); this.tunnel.writeData(bytes);
} catch (Exception ignored) { } catch (Exception ignored) {
Grasscutter.getLogger().debug("Unable to send packet to client."); Grasscutter.getLogger().debug("Unable to send packet to client.");
} }
@ -154,13 +156,13 @@ public class GameSession implements GameSessionManager.KcpChannel {
} }
@Override @Override
public void onConnected(GameSessionManager.KcpTunnel tunnel) { public void onConnected(KcpTunnel tunnel) {
this.tunnel = tunnel; this.tunnel = tunnel;
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString())); Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
} }
@Override @Override
public void handleReceive(byte[] bytes) { public void onMessage(byte[] bytes) {
// Decrypt and turn back into a packet // Decrypt and turn back into a packet
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY); Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
ByteBuf packet = Unpooled.wrappedBuffer(bytes); ByteBuf packet = Unpooled.wrappedBuffer(bytes);
@ -226,8 +228,8 @@ public class GameSession implements GameSessionManager.KcpChannel {
// Handle // Handle
getServer().getPacketHandler().handle(this, opcode, header, payload); getServer().getPacketHandler().handle(this, opcode, header, payload);
} }
} catch (Exception e) { } catch (Exception exception) {
e.printStackTrace(); Grasscutter.getLogger().warn("Unable to handle packet.", exception);
} finally { } finally {
// byteBuf.release(); //Needn't // byteBuf.release(); //Needn't
packet.release(); packet.release();
@ -235,8 +237,9 @@ public class GameSession implements GameSessionManager.KcpChannel {
} }
@Override @Override
public void handleClose() { public void onDisconnected() {
setState(SessionState.INACTIVE); setState(SessionState.INACTIVE);
// send disconnection pack in case of reconnection // send disconnection pack in case of reconnection
Grasscutter.getLogger() Grasscutter.getLogger()
.info(translate("messages.game.disconnect", this.getAddress().toString())); .info(translate("messages.game.disconnect", this.getAddress().toString()));

View File

@ -1,114 +0,0 @@
package emu.grasscutter.server.game;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.Utils;
import io.netty.buffer.*;
import io.netty.channel.DefaultEventLoop;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import kcp.highway.*;
import lombok.Getter;
public class GameSessionManager {
@Getter private static final DefaultEventLoop logicThread = new DefaultEventLoop();
private static final ConcurrentHashMap<Ukcp, GameSession> sessions = new ConcurrentHashMap<>();
private static final KcpListener listener =
new KcpListener() {
@Override
public void onConnected(Ukcp ukcp) {
int times = 0;
GameServer server = Grasscutter.getGameServer();
while (server == null) { // Waiting server to establish
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
ukcp.close();
return;
}
if (times++ > 5) {
Grasscutter.getLogger().error("Service is not available!");
ukcp.close();
return;
}
server = Grasscutter.getGameServer();
}
GameSession conversation = new GameSession(server);
conversation.onConnected(
new KcpTunnel() {
@Override
public InetSocketAddress getAddress() {
return ukcp.user().getRemoteAddress();
}
@Override
public void writeData(byte[] bytes) {
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
ukcp.write(buf);
buf.release();
}
@Override
public void close() {
ukcp.close();
}
@Override
public int getSrtt() {
return ukcp.srtt();
}
});
sessions.put(ukcp, conversation);
}
@Override
public void handleReceive(ByteBuf buf, Ukcp kcp) {
var byteData = Utils.byteBufToArray(buf);
logicThread.execute(
() -> {
try {
var conversation = sessions.get(kcp);
if (conversation != null) {
conversation.handleReceive(byteData);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
public void handleException(Throwable ex, Ukcp ukcp) {}
@Override
public void handleClose(Ukcp ukcp) {
GameSession conversation = sessions.get(ukcp);
if (conversation != null) {
conversation.handleClose();
sessions.remove(ukcp);
}
}
};
public static KcpListener getListener() {
return listener;
}
public interface KcpTunnel {
InetSocketAddress getAddress();
void writeData(byte[] bytes);
void close();
int getSrtt();
}
interface KcpChannel {
void onConnected(KcpTunnel tunnel);
void handleClose();
void handleReceive(byte[] bytes);
}
}

View File

@ -0,0 +1,31 @@
package emu.grasscutter.server.game.session;
import emu.grasscutter.net.KcpTunnel;
import io.netty.buffer.Unpooled;
import kcp.highway.Ukcp;
import lombok.*;
import java.net.InetSocketAddress;
@RequiredArgsConstructor
public final class GameSessionHandler implements KcpTunnel {
@Getter private final Ukcp handle;
@Override
public InetSocketAddress getAddress() {
return this.getHandle().user().getRemoteAddress();
}
@Override
public void writeData(byte[] bytes) {
var buffer = Unpooled.wrappedBuffer(bytes);
this.getHandle().write(buffer);
buffer.release();
}
@Override
public void close() {
this.getHandle().close();
}
}

View File

@ -0,0 +1,89 @@
package emu.grasscutter.server.game.session;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.game.*;
import emu.grasscutter.utils.Utils;
import io.netty.buffer.ByteBuf;
import kcp.highway.*;
import lombok.Getter;
import java.util.Map;
import java.util.concurrent.*;
public final class GameSessionManager implements KcpListener {
@Getter private static final GameSessionManager instance
= new GameSessionManager();
@Getter private static final ExecutorService executor
= Executors.newWorkStealingPool();
@Getter private static final Map<Ukcp, GameSession> sessions
= new ConcurrentHashMap<>();
/**
* Waits for the game server to be ready.
*
* @return The game server.
*/
private GameServer waitForServer() {
var server = Grasscutter.getGameServer();
var times = 0; while (server == null || !server.isStarted()) {
Utils.sleep(1000); // Wait 1s for the server to start.
if (times++ > 5) {
Grasscutter.getLogger().error("Game server has not started in a reasonable time.");
return null;
}
server = Grasscutter.getGameServer();
}
return server;
}
@Override
public void onConnected(Ukcp ukcp) {
// Fetch the game server.
var server = this.waitForServer();
if (server == null) {
ukcp.close();
return;
}
// Create a new session.
var session = sessions.compute(ukcp, (k, existing) -> {
// Close an existing session.
if (existing != null) {
existing.close();
}
return new GameSession(server);
});
// Connect the session.
session.onConnected(new GameSessionHandler(ukcp));
}
@Override
public void handleReceive(ByteBuf byteBuf, Ukcp ukcp) {
// Get the session.
var session = sessions.get(ukcp);
if (session == null) {
ukcp.close(); return;
}
// Handle the message in a separate thread.
var bytes = Utils.byteBufToArray(byteBuf);
executor.submit(() -> session.onMessage(bytes));
}
@Override
public void handleException(Throwable throwable, Ukcp ukcp) {
Grasscutter.getLogger().error("Exception in game session.", throwable);
}
@Override
public void handleClose(Ukcp ukcp) {
var session = sessions.remove(ukcp);
if (session != null) {
session.close();
}
}
}

View File

@ -1,21 +0,0 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.CancelCoopTaskReqOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketCancelCoopTaskRsp;
@Opcodes(PacketOpcodes.CancelCoopTaskReq)
public class HandlerCancelCoopTaskReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
CancelCoopTaskReqOuterClass.CancelCoopTaskReq req =
CancelCoopTaskReqOuterClass.CancelCoopTaskReq.parseFrom(payload);
var chapterId = req.getChapterId();
Grasscutter.getLogger().warn("Call to unimplemented packet CancelCoopTaskReq");
// TODO: Actually cancel the quests.
session.send(new PacketCancelCoopTaskRsp(chapterId));
}
}

View File

@ -1,20 +0,0 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.DungeonDieOptionReqOuterClass.DungeonDieOptionReq;
import emu.grasscutter.server.game.GameSession;
@Opcodes(PacketOpcodes.DungeonDieOptionReq)
public class HandlerDungeonDieOptionReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
DungeonDieOptionReq req = DungeonDieOptionReq.parseFrom(payload);
var dieOption = req.getDieOption();
// TODO Handle other die options
if (req.getIsQuitImmediately()) {
session.getPlayer().getServer().getDungeonSystem().exitDungeon(session.getPlayer());
}
session.getPlayer().sendPacket(new BasePacket(PacketOpcodes.DungeonDieOptionRsp));
}
}

View File

@ -1,13 +0,0 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.server.game.GameSession;
@Opcodes(PacketOpcodes.DungeonRestartReq)
public class HandlerDungeonRestartReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
session.getPlayer().getServer().getDungeonSystem().restartDungeon(session.getPlayer());
session.getPlayer().sendPacket(new BasePacket(PacketOpcodes.DungeonRestartRsp));
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More