mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-04 05:53:42 +00:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
5ebad71e9d | |||
564b609028 | |||
cdb0dc560a | |||
d8c3da8fcd | |||
13c40b53a7 | |||
f1c1a84683 | |||
2bcbd41026 | |||
adf8031684 | |||
0bbeaf254b | |||
1fac319eb2 | |||
d224178a64 | |||
d461ee2eb3 | |||
24874e7fba | |||
205b79dc02 | |||
0e033e3f77 | |||
583a41ab2c | |||
cf6fb275be | |||
269f7b4fbf | |||
9b4ce34f4a | |||
f86259a430 | |||
837e30e04b | |||
f5703e5964 | |||
bc8e7c21ce | |||
b7a9d28f02 | |||
770cd62370 | |||
6745d1126e | |||
0803618bf5 | |||
cfc8a4866f | |||
fd75ba7b9b | |||
d32a75e980 | |||
9a198bd231 | |||
453dc9717d | |||
582d7af9c4 | |||
cab3bfb5a7 | |||
cf574e99cb | |||
3094facb88 | |||
6e309b6fee | |||
b5e35f5409 | |||
a3fd10c3be |
@ -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 Culivation (as admin), press the download button in the upper right corner.
|
- After opening Cultivation (as admin), press the download button in the upper right corner.
|
||||||
- Click `Download All-in-One`
|
- Click `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.
|
||||||
|
@ -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.1'
|
version = '1.7.4'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
|
@ -3,81 +3,65 @@
|
|||||||
|
|
||||||
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
|
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
|
||||||
|
|
||||||
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) | [हिंदी](README_hn-IN.md)
|
[EN](README.md) | [简中](docs/README_zh-CN.md) | [繁中](docs/README_zh-TW.md) | [FR](docs/README_fr-FR.md) | [ES](docs/README_es-ES.md) | [HE](docs/README_HE.md) | [RU](docs/README_ru-RU.md) | [PL](docs/README_pl-PL.md) | [ID](docs/README_id-ID.md) | [KR](docs/README_ko-KR.md) | [FIL/PH](docs/README_fil-PH.md) | [NL](docs/README_NL.md) | [JP](docs/README_ja-JP.md) | [IT](docs/README_it-IT.md) | [VI](docs/README_vi-VN.md)
|
||||||
|
|
||||||
|
|
||||||
***:** 私たちはプロジェクトへの貢献者をいつでも歓迎します。貢献を追加する前に、我々の [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
|
**Attention:** 私たちはプロジェクトへのコントリビュータをいつでも歓迎します。コントリビュートする前に、私たちの [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
|
||||||
|
|
||||||
## 現在機能している物
|
## 現在実装されている機能
|
||||||
|
|
||||||
* ログイン
|
* ログイン
|
||||||
* 戦闘
|
* 戦闘
|
||||||
* フレンドリスト
|
* フレンドリスト
|
||||||
* テレポート
|
* テレポート
|
||||||
* 祈願 (ガチャ)
|
* 祈願 (ガチャ)
|
||||||
* マルチプレイは一部機能しています
|
* マルチプレイ (一部)
|
||||||
* コンソールを使用してモンスターをスポーンさせる
|
* コンソールを通したモンスターのスポーン
|
||||||
* インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど)
|
* インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど)
|
||||||
|
|
||||||
## クイックセットアップガイド
|
## かんたんセットアップガイド
|
||||||
|
|
||||||
***:** サポートが必要な場合はGrasscutterの[Discord](https://discord.gg/T5vZU6UyeG)に参加してください。
|
**Note:** サポートが必要な場合はGrasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加してください。
|
||||||
|
|
||||||
### 動作環境
|
### パパっとスタートアップ
|
||||||
|
|
||||||
* [JAVAのバージョン17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
- [Java (バージョン17以降)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) を用意する
|
||||||
|
- [MongoDB Community Server](https://www.mongodb.com/try/download/community) を用意する
|
||||||
|
- ゲームバージョンがREL4.0.Xのものを用意する (4.0.Xのクライアントを持っていない場合は右のリンクからダウンロード): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
|
||||||
|
- [最新の Cultivation](https://github.com/Grasscutters/Cultivation/releases/latest)をダウンロードする。`.msi`インストーラを使ってください。
|
||||||
|
- 管理者権限を付与して Cultivation を実行した後、右上端にあるダウンロードアイコンのボタンを押す。
|
||||||
|
- `Download All-in-One` をクリックする
|
||||||
|
- 右上端にある歯車アイコンのボタンをクリックする。
|
||||||
|
- `Game Install Path` にゲームファイルのパスを指定する。
|
||||||
|
- `Custom Java Path` に、自分が用意したJavaのパスを指定する。 (例: `C:\Program Files\Java\jdk-17\bin\java.exe`)
|
||||||
|
- その他の設定には手を付けず次の段階に進む。
|
||||||
|
|
||||||
***:** サーバーを動作させるだけならjreのみで十分です。 開発をしたい場合JDKが必要になるかもしれません。
|
- Launch の隣にある小さいボタンを押す。
|
||||||
|
- Launchボタンを押す
|
||||||
|
- 好きなユーザ名でログインする。パスワードは特段気にすることはない。
|
||||||
|
|
||||||
* [MongoDB](https://www.mongodb.com/try/download/community) (バージョン4.0以降を推奨)
|
|
||||||
|
|
||||||
* プロキシツール: [mitmproxy](https://mitmproxy.org/) (mitmdump, 推奨)、[Fiddler Classic](https://telerik-fiddler.s3.amazonaws.com/fiddler/FiddlerSetup.exe)、その他。
|
|
||||||
|
|
||||||
### 起動方法
|
|
||||||
|
|
||||||
***:** もしサーバーをアップデートしたい場合は`config.json`を削除してから再生成してください。
|
|
||||||
|
|
||||||
1. `grasscutter.jar`を入手する
|
|
||||||
- [releases](https://github.com/Grasscutters/Grasscutter/releases/latest) か [action](https://github.com/Grasscutters/Grasscutter/actions) からダウンロードするか、[自分でビルド](#ビルド)してください。
|
|
||||||
2. `grasscutter.jar` があるディレクトリに `resources` フォルダーを作成し、そこに `BinOutput, ExcelBinOutput, Readables, Scripts, Subtitle, TextMap` を移動してください *(`resources` フォルダの中身の入手方法については [wiki](https://github.com/Grasscutters/Grasscutter/wiki) を参照してください.)*
|
|
||||||
3. コマンドプロンプトに`java -jar grasscutter.jar`を入力しGrasscutterを起動してください。**このときMongoDBも実行する必要があります。**
|
|
||||||
|
|
||||||
### クライアントとの接続
|
|
||||||
|
|
||||||
½. [このコマンド](https://github.com/Grasscutters/Grasscutter/wiki/Commands#commands-for-server-admins)をサーバーコンソールから使用してアカウントを作成してください。
|
|
||||||
|
|
||||||
1. 通信内容をリダイレクトする: (どちらか一つを選択してください)
|
|
||||||
- mitmdump: `mitmdump -s proxy.py -k`
|
|
||||||
|
|
||||||
- CA証明書を信頼する:
|
|
||||||
|
|
||||||
- ***:** CA証明書は`%USERPROFILE%\.mitmproxy`に保存されています。ダブルクリックして[インストール](https://docs.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate#installing-a-trusted-root-certificate)するか...
|
|
||||||
|
|
||||||
- コマンドライン経由でインストールします
|
|
||||||
|
|
||||||
```shell
|
|
||||||
certutil -addstore root %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer
|
|
||||||
```
|
|
||||||
|
|
||||||
- Fiddler Classic: Fiddler Classicを起動し(Tools -> Options -> HTTPS)から`Decrypt https traffic`をオンにしてください。 (Tools -> Options -> Connections) に有るポート番号の設定を`8888`以外に設定してください。その後この[スクリプト](https://github.com/Grasscutters/Grasscutter/wiki/Resources#fiddler-classic-jscript)をFiddlerScriptタブにコピペしてロードします。
|
|
||||||
|
|
||||||
- [ホストファイル](https://github.com/Grasscutters/Grasscutter/wiki/Resources#hosts-file)
|
|
||||||
|
|
||||||
2. ネットワークプロキシを `127.0.0.1:(自分で設定したポート番号)` に設定してください。
|
|
||||||
- mitmproxyを使用した場合:プロキシの設定と証明書のインストールが終わった後、http://mitm.it/ でトラフィックがmitmproxyを通過しているか確認しましょう。
|
|
||||||
|
|
||||||
**`start.cmd`でmitmdumpとサーバーをまとめて起動することが出来ます。ただ、事前に`start_config.cmd`でJAVAのパスを指定している必要があります。**
|
|
||||||
|
|
||||||
### ビルド
|
### ビルド
|
||||||
|
|
||||||
GrasscutterはGradleを使用して依存関係とビルドを処理しています。
|
Grasscutterは依存関係とビルドの処理にGradleを使用しています。
|
||||||
|
|
||||||
**要件:**
|
**必要要件:**
|
||||||
|
|
||||||
- [Java SE Development Kits - 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
- [Java SE Development Kit 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [NodeJS](https://nodejs.org/en/download) (任意、ハンドブックの生成に必要)
|
||||||
|
|
||||||
##### Windows
|
##### Clone
|
||||||
|
```shell
|
||||||
|
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
|
||||||
|
cd Grasscutter
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Compile
|
||||||
|
|
||||||
|
**Note:** 環境によってはハンドブックの生成が失敗する場合があります。ハンドブックの生成をさせない場合は `gradlew jar` コマンドに `-PskipHandbook=1` を付け加えてください。
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/Grasscutters/Grasscutter.git
|
git clone https://github.com/Grasscutters/Grasscutter.git
|
||||||
@ -86,7 +70,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
|
||||||
@ -95,13 +79,8 @@ 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、その他) > ゲーム
|
|
||||||
|
@ -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”为后缀的安装包)。
|
||||||
- 以管理员身份打开Culivation,按右上角的下载按钮。
|
- 以管理员身份打开Cultivation,按右上角的下载按钮。
|
||||||
- 点击“下载 Grasscutter 一体化”
|
- 点击“下载 Grasscutter 一体化”
|
||||||
- 点击右上角的齿轮
|
- 点击右上角的齿轮
|
||||||
- 将游戏安装路径设置为你游戏所在的位置。
|
- 将游戏安装路径设置为你游戏所在的位置。
|
||||||
|
@ -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` 安裝程式。
|
||||||
- 以管理員身分打開 Culivation,按右上角的下載按鈕。
|
- 以管理員身分打開 Cultivation,按右上角的下載按鈕。
|
||||||
- 點擊 `Download All-in-One`
|
- 點擊 `Download All-in-One`
|
||||||
- 點擊右上角的齒輪
|
- 點擊右上角的齒輪
|
||||||
- 將遊戲安裝路徑設置為你的遊戲所在的位置。
|
- 將遊戲安裝路徑設置為你的遊戲所在的位置。
|
||||||
|
188
docs/events/windtrace/README.md
Normal file
188
docs/events/windtrace/README.md
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# 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: 
|
||||||
|
- `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: 
|
||||||
|
- **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: 
|
||||||
|
- **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: 
|
||||||
|
- **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: 
|
||||||
|
- **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. 
|
||||||
|
- 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)
|
BIN
docs/events/windtrace/images/defaultexhibitioninfo.png
Normal file
BIN
docs/events/windtrace/images/defaultexhibitioninfo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
docs/events/windtrace/images/multistageplayinfo.png
Normal file
BIN
docs/events/windtrace/images/multistageplayinfo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
docs/events/windtrace/images/pickavatar.png
Normal file
BIN
docs/events/windtrace/images/pickavatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
docs/events/windtrace/images/seektime.png
Normal file
BIN
docs/events/windtrace/images/seektime.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
docs/events/windtrace/images/startwindtrace.png
Normal file
BIN
docs/events/windtrace/images/startwindtrace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
@ -32,9 +32,12 @@ 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.getEntityByConfigId(entityId)
|
? scene.getFirstEntityByConfigId(entityId)
|
||||||
: scene.getEntityById(entityId);
|
: scene.getEntityById(entityId);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
sender.dropMessage("Entity not found.");
|
sender.dropMessage("Entity not found.");
|
||||||
|
@ -51,7 +51,10 @@ public final class EntityCommand implements CommandHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
param.scene = targetPlayer.getScene();
|
param.scene = targetPlayer.getScene();
|
||||||
var entity = param.scene.getEntityByConfigId(param.configId);
|
// TODO Might want to allow groupId specification,
|
||||||
|
// 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"));
|
||||||
|
@ -12,7 +12,7 @@ import lombok.val;
|
|||||||
@Command(
|
@Command(
|
||||||
label = "setSceneTag",
|
label = "setSceneTag",
|
||||||
aliases = {"tag"},
|
aliases = {"tag"},
|
||||||
usage = {"<add|remove|unlockall> <sceneTagId>"},
|
usage = {"<add|remove|unlockall|reset> <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.size() == 0) {
|
if (args.isEmpty()) {
|
||||||
sendUsageMessage(sender);
|
sendUsageMessage(sender);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -39,6 +39,9 @@ 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;
|
||||||
@ -49,7 +52,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 == null) {
|
if (sceneData.isEmpty()) {
|
||||||
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id");
|
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -80,15 +83,15 @@ public final class SetSceneTagCommand implements CommandHandler {
|
|||||||
.toList()
|
.toList()
|
||||||
.forEach(
|
.forEach(
|
||||||
sceneTag -> {
|
sceneTag -> {
|
||||||
if (targetPlayer.getSceneTags().get(sceneTag.getSceneId()) == null) {
|
targetPlayer
|
||||||
targetPlayer.getSceneTags().put(sceneTag.getSceneId(), new HashSet<>());
|
.getSceneTags()
|
||||||
}
|
.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(sceneTag -> sceneTag.isDefaultValid())
|
.filter(SceneTagData::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(
|
||||||
@ -99,6 +102,22 @@ 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));
|
||||||
}
|
}
|
||||||
|
@ -302,6 +302,10 @@ 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<>();
|
||||||
|
@ -42,6 +42,7 @@ 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;
|
||||||
@ -327,6 +328,9 @@ 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;
|
||||||
@ -369,6 +373,11 @@ public class AbilityModifier implements Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AbilityModifierProperty implements Serializable {
|
||||||
|
public float Actor_HpThresholdRatio;
|
||||||
|
// Add more properties here when GC needs them.
|
||||||
|
}
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
LockHP,
|
LockHP,
|
||||||
Invincible,
|
Invincible,
|
||||||
|
@ -8,4 +8,5 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package emu.grasscutter.data.binout.config.fields;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ConfigCombatSummon {
|
||||||
|
List<SummonTag> summonTags;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class SummonTag {
|
||||||
|
int summonTag;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ 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",
|
||||||
@ -28,7 +29,7 @@ public final class PointData {
|
|||||||
|
|
||||||
@SerializedName(
|
@SerializedName(
|
||||||
value = "dungeonRandomList",
|
value = "dungeonRandomList",
|
||||||
alternate = {"OIBKFJNBLHO"})
|
alternate = {"GLEKJMEEOMH"})
|
||||||
@Getter
|
@Getter
|
||||||
private int[] dungeonRandomList;
|
private int[] dungeonRandomList;
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package emu.grasscutter.data.excels;
|
||||||
|
|
||||||
|
import emu.grasscutter.data.GameResource;
|
||||||
|
import emu.grasscutter.data.ResourceType;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@ResourceType(name = "HomeworldModuleExcelConfigData.json")
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
@Getter
|
||||||
|
public class HomeWorldModuleData extends GameResource {
|
||||||
|
int Id;
|
||||||
|
boolean isFree;
|
||||||
|
int worldSceneId;
|
||||||
|
int defaultRoomSceneId;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
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")
|
||||||
@ -12,4 +14,46 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,83 @@
|
|||||||
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 int getLevelId() {
|
public TowerCondType getCondType(int star) {
|
||||||
return levelId;
|
if (star < 0 || conds == null || star >= conds.size()) {
|
||||||
|
return TowerCondType.TOWER_COND_NONE;
|
||||||
|
}
|
||||||
|
var condType = conds.get(star).towerCondType;
|
||||||
|
return condType == null ? TowerCondType.TOWER_COND_NONE : condType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLevelGroupId() {
|
public TowerCondTimeParams getTimeCond(int star) {
|
||||||
return levelGroupId;
|
if (star < 0 || conds == null || star >= conds.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var params = conds.get(star).argumentList;
|
||||||
|
return new TowerCondTimeParams(params.get(0), params.get(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLevelIndex() {
|
public TowerCondHpParams getHpCond(int star) {
|
||||||
return levelIndex;
|
if (star < 0 || conds == null || star >= conds.size()) {
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ 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;
|
||||||
@ -24,6 +25,7 @@ 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;
|
||||||
@ -44,6 +46,49 @@ 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) {
|
||||||
|
@ -7,6 +7,7 @@ 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;
|
||||||
@ -20,7 +21,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.HashMap;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@ -47,9 +48,64 @@ 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() {
|
||||||
@ -279,8 +335,9 @@ public final class AbilityManager extends BasePlayerManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the player as invulnerable.
|
// Track this elemental burst to possibly clear avatar energy later.
|
||||||
this.abilityInvulnerable = true;
|
this.burstSkillId = skillId;
|
||||||
|
this.burstCasterId = casterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -453,6 +510,8 @@ 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);
|
||||||
|
|
||||||
@ -562,6 +621,14 @@ 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));
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package emu.grasscutter.game.ability.actions;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
|
import emu.grasscutter.data.GameData;
|
||||||
|
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
|
||||||
|
import emu.grasscutter.game.ability.Ability;
|
||||||
|
import emu.grasscutter.game.entity.*;
|
||||||
|
import emu.grasscutter.game.world.*;
|
||||||
|
import emu.grasscutter.net.proto.EPKDEHOJFLIOuterClass.EPKDEHOJFLI;
|
||||||
|
import emu.grasscutter.server.packet.send.PacketMonsterSummonTagNotify;
|
||||||
|
import emu.grasscutter.utils.*;
|
||||||
|
|
||||||
|
@AbilityAction(AbilityModifierAction.Type.Summon)
|
||||||
|
public class ActionSummon extends AbilityActionHandler {
|
||||||
|
@Override
|
||||||
|
public synchronized boolean execute(
|
||||||
|
Ability ability, AbilityModifierAction action, ByteString abilityData, GameEntity target) {
|
||||||
|
EPKDEHOJFLI summonPosRot = null;
|
||||||
|
try {
|
||||||
|
// In game version 4.0, summoned entity's
|
||||||
|
// position and rotation are packed in EPKDEHOJFLI.
|
||||||
|
// This is packet AbilityActionSummon and has two fields:
|
||||||
|
// 4: Vector pos
|
||||||
|
// 13: Vector rot
|
||||||
|
summonPosRot = EPKDEHOJFLI.parseFrom(abilityData);
|
||||||
|
} catch (InvalidProtocolBufferException e) {
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.error("Failed to parse abilityData: {}", Utils.bytesToHex(abilityData.toByteArray()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = new Position(summonPosRot.getPos());
|
||||||
|
var rot = new Position(summonPosRot.getRot());
|
||||||
|
var monsterId = action.monsterID;
|
||||||
|
|
||||||
|
var scene = target.getScene();
|
||||||
|
|
||||||
|
var monsterData = GameData.getMonsterDataMap().get(monsterId);
|
||||||
|
if (monsterData == null) {
|
||||||
|
Grasscutter.getLogger().error("Failed to find monster by ID {}", monsterId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof EntityMonster ownerEntity) {
|
||||||
|
var level = scene.getLevelForMonster(0, ownerEntity.getLevel());
|
||||||
|
var entity = new EntityMonster(scene, monsterData, pos, rot, level);
|
||||||
|
ownerEntity.getSummonTagMap().put(action.summonTag, entity);
|
||||||
|
entity.setSummonedTag(action.summonTag);
|
||||||
|
entity.setOwnerEntityId(target.getId());
|
||||||
|
scene.addEntity(entity);
|
||||||
|
scene.getPlayers().get(0).sendPacket(new PacketMonsterSummonTagNotify(ownerEntity));
|
||||||
|
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.trace(
|
||||||
|
"Spawned entityId {} monsterId {} pos {} rot {}, target { {} }, action { {} }",
|
||||||
|
entity.getId(),
|
||||||
|
monsterId,
|
||||||
|
pos,
|
||||||
|
rot,
|
||||||
|
target,
|
||||||
|
action);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ 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;
|
||||||
|
|
||||||
@ -67,6 +68,10 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,9 +82,14 @@ public final class DungeonManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getLevelForMonster(int id) {
|
public int getLevelForMonster(int id) {
|
||||||
|
if (isTowerDungeon()) {
|
||||||
|
// Tower dungeons have their own level setting in TowerLevelData
|
||||||
|
return scene.getPlayers().get(0).getTowerManager().getCurrentMonsterLevel();
|
||||||
|
} else {
|
||||||
// TODO should use levelConfigMap? and how?
|
// TODO should use levelConfigMap? and how?
|
||||||
return dungeonData.getShowLevel();
|
return dungeonData.getShowLevel();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean activateRespawnPoint(int pointId) {
|
public boolean activateRespawnPoint(int pointId) {
|
||||||
val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId);
|
val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId);
|
||||||
@ -323,15 +333,31 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +131,11 @@ public final class DungeonSystem extends BaseGameSystem {
|
|||||||
dungeonId);
|
dungeonId);
|
||||||
|
|
||||||
if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) {
|
if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) {
|
||||||
dungeonSettleListeners.forEach(player.getScene()::addDungeonSettleObserver);
|
var scene = player.getScene();
|
||||||
|
var dungeonManager = new DungeonManager(scene, data);
|
||||||
|
dungeonManager.setTowerDungeon(true);
|
||||||
|
scene.setDungeonManager(dungeonManager);
|
||||||
|
dungeonSettleListeners.forEach(scene::addDungeonSettleObserver);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -164,11 +168,40 @@ public final class DungeonSystem extends BaseGameSystem {
|
|||||||
dungeonManager.unsetTrialTeam(player);
|
dungeonManager.unsetTrialTeam(player);
|
||||||
}
|
}
|
||||||
// clean temp team if it has
|
// clean temp team if it has
|
||||||
player.getTeamManager().cleanTemporaryTeam();
|
if (!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
|
// Transfer player back to world after a small delay.
|
||||||
player.getWorld().transferPlayerToScene(player, prevScene, prevPos);
|
// This wait is important for avoiding double teleports,
|
||||||
player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp));
|
// which specifically happen when player quits a dungeon
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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.*;
|
||||||
@ -9,6 +10,7 @@ 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(
|
||||||
@ -22,17 +24,24 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
||||||
|
var stars = towerManager.getCurLevelStars();
|
||||||
|
|
||||||
towerManager.notifyCurLevelRecordChangeWhenDone(3);
|
if (endReason == DungeonEndReason.COMPLETED) {
|
||||||
|
// Update star record only when challenge completes successfully.
|
||||||
|
towerManager.notifyCurLevelRecordChangeWhenDone(stars);
|
||||||
scene.broadcastPacket(
|
scene.broadcastPacket(
|
||||||
new PacketTowerFloorRecordChangeNotify(
|
new PacketTowerFloorRecordChangeNotify(
|
||||||
towerManager.getCurrentFloorId(), 3, towerManager.canEnterScheduleFloor()));
|
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(
|
new DungeonEndStats(scene.getKilledMonsterCount(), finishedTime, 0, endReason);
|
||||||
scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason);
|
var result =
|
||||||
var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge);
|
endReason == DungeonEndReason.COMPLETED
|
||||||
|
? new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars)
|
||||||
|
: new BaseDungeonResult(dungeonData, dungeonStats);
|
||||||
|
|
||||||
scene.broadcastPacket(new PacketDungeonSettleNotify(result));
|
scene.broadcastPacket(new PacketDungeonSettleNotify(result));
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ 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;
|
||||||
@ -22,6 +23,7 @@ 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;
|
||||||
@ -58,6 +60,7 @@ 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() {
|
||||||
@ -80,9 +83,16 @@ public class WorldChallenge {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.progress = true;
|
this.progress = true;
|
||||||
this.startedAt = System.currentTimeMillis();
|
this.startedAt = getScene().getSceneTimeSeconds();
|
||||||
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() {
|
||||||
@ -136,6 +146,10 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +157,20 @@ 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;
|
||||||
|
@ -33,7 +33,7 @@ public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHand
|
|||||||
realGroup,
|
realGroup,
|
||||||
challengeId, // Id
|
challengeId, // Id
|
||||||
challengeIndex, // Index
|
challengeIndex, // Index
|
||||||
List.of(monstersToKill, 0),
|
List.of(monstersToKill, gadgetCFGId),
|
||||||
0, // Limit
|
0, // Limit
|
||||||
monstersToKill, // Goal
|
monstersToKill, // Goal
|
||||||
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));
|
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));
|
||||||
|
@ -2,7 +2,6 @@ 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 {
|
||||||
@ -14,7 +13,12 @@ public class GuardTrigger extends ChallengeTrigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onBegin(WorldChallenge challenge) {
|
public void onBegin(WorldChallenge challenge) {
|
||||||
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, 100));
|
challenge.setGuardEntity(
|
||||||
|
challenge.getScene().getEntityByConfigId(entityToProtectCFGId, challenge.getGroup().id));
|
||||||
|
lastSendPercent = challenge.getGuardEntityHpPercent();
|
||||||
|
challenge
|
||||||
|
.getScene()
|
||||||
|
.broadcastPacket(new PacketChallengeDataNotify(challenge, 2, lastSendPercent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -22,9 +26,7 @@ public class GuardTrigger extends ChallengeTrigger {
|
|||||||
if (gadget.getConfigId() != entityToProtectCFGId) {
|
if (gadget.getConfigId() != entityToProtectCFGId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
|
var percent = challenge.getGuardEntityHpPercent();
|
||||||
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));
|
||||||
|
@ -1,8 +1,21 @@
|
|||||||
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();
|
||||||
|
@ -13,41 +13,47 @@ 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() && canJump) {
|
if (challenge.isSuccess()) {
|
||||||
continueStatus =
|
if (hasNextLevel) {
|
||||||
hasNextLevel
|
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE;
|
||||||
? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE
|
} else if (canJump) {
|
||||||
: ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
|
continueStatus = 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).build());
|
ItemParamOuterClass.ItemParam.newBuilder().setItemId(201).setCount(1000));
|
||||||
|
|
||||||
|
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);
|
builder.setTowerLevelEndNotify(towerLevelEndNotify.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -67,6 +67,11 @@ 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
|
||||||
|
@ -70,6 +70,11 @@ 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) {
|
||||||
|
@ -5,6 +5,7 @@ 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;
|
||||||
@ -104,6 +105,25 @@ 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));
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
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;
|
||||||
@ -23,17 +25,15 @@ 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 lombok.*;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
|
import lombok.*;
|
||||||
|
|
||||||
public class EntityMonster extends GameEntity {
|
public class EntityMonster extends GameEntity {
|
||||||
@Getter(onMethod_ = @Override)
|
@Getter(onMethod_ = @Override)
|
||||||
@ -41,20 +41,26 @@ public class EntityMonster extends GameEntity {
|
|||||||
|
|
||||||
@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(Scene scene, MonsterData monsterData, Position pos, Position rot, int level) {
|
public EntityMonster(
|
||||||
|
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);
|
||||||
@ -65,14 +71,29 @@ public class EntityMonster extends GameEntity {
|
|||||||
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 = GameData.getMonsterConfigData().get(
|
this.configEntityMonster =
|
||||||
GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
|
GameData.getMonsterConfigData()
|
||||||
|
.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());
|
||||||
@ -87,18 +108,15 @@ 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()
|
this.getWorld().getHost().getAbilityManager().addAbilityToEntity(this, data);
|
||||||
.getAbilityManager()
|
|
||||||
.addAbilityToEntity(this, data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initAbilities() {
|
public void initAbilities() {
|
||||||
// Affix abilities
|
// Affix abilities
|
||||||
var optionalGroup = this.getScene().getLoadedGroups().stream()
|
var optionalGroup =
|
||||||
.filter(g -> g.id == this.getGroupId())
|
this.getScene().getLoadedGroups().stream().filter(g -> g.id == this.getGroupId()).findAny();
|
||||||
.findAny();
|
|
||||||
List<Integer> affixes = null;
|
List<Integer> affixes = null;
|
||||||
if (optionalGroup.isPresent()) {
|
if (optionalGroup.isPresent()) {
|
||||||
var group = optionalGroup.get();
|
var group = optionalGroup.get();
|
||||||
@ -126,14 +144,12 @@ public class EntityMonster extends GameEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Research if any monster is non humanoid
|
// TODO: Research if any monster is non humanoid
|
||||||
for(var ability : GameData.getConfigGlobalCombat()
|
for (var ability :
|
||||||
.getDefaultAbilities()
|
GameData.getConfigGlobalCombat().getDefaultAbilities().getNonHumanoidMoveAbilities()) {
|
||||||
.getNonHumanoidMoveAbilities()) {
|
|
||||||
this.addConfigAbility(ability);
|
this.addConfigAbility(ability);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.configEntityMonster != null &&
|
if (this.configEntityMonster != null && this.configEntityMonster.getAbilities() != 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);
|
||||||
}
|
}
|
||||||
@ -143,9 +159,8 @@ 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(GameData.getConfigGlobalCombat()
|
this.addConfigAbility(
|
||||||
.getDefaultAbilities()
|
GameData.getConfigGlobalCombat().getDefaultAbilities().getMonterEliteAbilityName());
|
||||||
.getMonterEliteAbilityName());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +209,8 @@ public class EntityMonster extends GameEntity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInteract(Player player, GadgetInteractReq interactReq) {
|
public void onInteract(Player player, GadgetInteractReq interactReq) {
|
||||||
EnvAnimalGatherConfigData gatherData = GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
|
EnvAnimalGatherConfigData gatherData =
|
||||||
|
GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
|
||||||
|
|
||||||
if (gatherData == null) {
|
if (gatherData == null) {
|
||||||
return;
|
return;
|
||||||
@ -208,7 +224,11 @@ public class EntityMonster extends GameEntity {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
// Lua event
|
// Lua event
|
||||||
getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
|
getScene()
|
||||||
|
.getScriptManager()
|
||||||
|
.callEvent(
|
||||||
|
new ScriptArgs(
|
||||||
|
this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -231,7 +251,14 @@ public class EntityMonster extends GameEntity {
|
|||||||
@Override
|
@Override
|
||||||
public void runLuaCallbacks(EntityDamageEvent event) {
|
public void runLuaCallbacks(EntityDamageEvent event) {
|
||||||
super.runLuaCallbacks(event);
|
super.runLuaCallbacks(event);
|
||||||
getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EVENT_SPECIFIC_MONSTER_HP_CHANGE, getConfigId(), monsterData.getId())
|
getScene()
|
||||||
|
.getScriptManager()
|
||||||
|
.callEvent(
|
||||||
|
new ScriptArgs(
|
||||||
|
this.getGroupId(),
|
||||||
|
EVENT_SPECIFIC_MONSTER_HP_CHANGE,
|
||||||
|
getConfigId(),
|
||||||
|
monsterData.getId())
|
||||||
.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()));
|
||||||
@ -250,29 +277,68 @@ 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()).ifPresent(s -> s.onMonsterDead(this));
|
Optional.ofNullable(scriptManager.getScriptMonsterSpawnService())
|
||||||
|
.ifPresent(s -> s.onMonsterDead(this));
|
||||||
|
|
||||||
// prevent spawn monster after success
|
// Ensure each EVENT_ANY_MONSTER_DIE runs to completion.
|
||||||
/*if (challenge.map(c -> c.inProgress()).orElse(true)) {
|
// Multiple such events firing at the same time may cause
|
||||||
scriptManager.callEvent(new ScriptArgs(EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()).setGroupId(this.getGroupId()));
|
// the same lua trigger to fire multiple times, when it
|
||||||
} else if (getScene().getChallenge() == null) {
|
// should happen only once.
|
||||||
}*/
|
var future =
|
||||||
scriptManager.callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
|
scriptManager.callEvent(
|
||||||
|
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.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
|
scene
|
||||||
|
.getPlayers()
|
||||||
|
.forEach(
|
||||||
|
p ->
|
||||||
|
p.getBattlePassManager()
|
||||||
|
.triggerMission(
|
||||||
|
WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
|
||||||
|
|
||||||
scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
|
scene
|
||||||
scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
|
.getPlayers()
|
||||||
scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
|
.forEach(
|
||||||
|
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 = scene.getScriptManager().getGroupInstanceById(this.getGroupId());
|
SceneGroupInstance groupInstance =
|
||||||
|
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(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId());
|
scene.triggerDungeonEvent(
|
||||||
scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER, this.getMonsterData().getType().getValue());
|
DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId());
|
||||||
scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
|
scene.triggerDungeonEvent(
|
||||||
|
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() {
|
||||||
@ -280,45 +346,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 = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
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);
|
||||||
|
|
||||||
// Clear properties
|
// Clear properties
|
||||||
this.getFightProperties().clear();
|
this.getFightProperties().clear();
|
||||||
|
|
||||||
// Base stats
|
// Base stats
|
||||||
MonsterData.definedFightProperties.forEach(prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
|
MonsterData.definedFightProperties.forEach(
|
||||||
|
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(prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
|
this.setFightProperty(
|
||||||
|
prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set % stats
|
// Set % stats
|
||||||
FightProperty.forEachCompoundProperty(c -> this.setFightProperty(c.getResult(),
|
FightProperty.forEachCompoundProperty(
|
||||||
this.getFightProperty(c.getFlat()) + (this.getFightProperty(c.getBase()) * (1f + this.getFightProperty(c.getPercent())))));
|
c ->
|
||||||
|
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(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
|
this.setFightProperty(
|
||||||
|
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 authority = EntityAuthorityInfo.newBuilder()
|
var aiInfo =
|
||||||
|
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(this.getBornPos().toProto());
|
||||||
|
if (ownerEntityId != 0) {
|
||||||
|
aiInfo.setServantInfo(ServantInfo.newBuilder().setMasterEntityId(ownerEntityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var authority =
|
||||||
|
EntityAuthorityInfo.newBuilder()
|
||||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||||
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
||||||
.setAiInfo(SceneEntityAiInfo.newBuilder()
|
.setAiInfo(aiInfo)
|
||||||
.setIsAiOpen(true)
|
|
||||||
.setBornPos(this.getBornPos().toProto()))
|
|
||||||
.setBornPos(this.getBornPos().toProto())
|
.setBornPos(this.getBornPos().toProto())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var entityInfo = SceneEntityInfo.newBuilder()
|
var entityInfo =
|
||||||
|
SceneEntityInfo.newBuilder()
|
||||||
.setEntityId(this.getId())
|
.setEntityId(this.getId())
|
||||||
.setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER)
|
.setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER)
|
||||||
.setMotionInfo(this.getMotionInfo())
|
.setMotionInfo(this.getMotionInfo())
|
||||||
@ -329,12 +436,14 @@ public class EntityMonster extends GameEntity {
|
|||||||
|
|
||||||
this.addAllFightPropsToEntityInfo(entityInfo);
|
this.addAllFightPropsToEntityInfo(entityInfo);
|
||||||
|
|
||||||
entityInfo.addPropList(PropPair.newBuilder()
|
entityInfo.addPropList(
|
||||||
|
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 = SceneMonsterInfo.newBuilder()
|
var monsterInfo =
|
||||||
|
SceneMonsterInfo.newBuilder()
|
||||||
.setMonsterId(getMonsterId())
|
.setMonsterId(getMonsterId())
|
||||||
.setGroupId(this.getGroupId())
|
.setGroupId(this.getGroupId())
|
||||||
.setConfigId(this.getConfigId())
|
.setConfigId(this.getConfigId())
|
||||||
@ -342,20 +451,26 @@ public class EntityMonster extends GameEntity {
|
|||||||
.setAuthorityPeerId(this.getWorld().getHostPeerId())
|
.setAuthorityPeerId(this.getWorld().getHostPeerId())
|
||||||
.setPoseId(this.getPoseId())
|
.setPoseId(this.getPoseId())
|
||||||
.setBlockId(this.getScene().getId())
|
.setBlockId(this.getScene().getId())
|
||||||
|
.setSummonedTag(this.summonedTag)
|
||||||
|
.setOwnerEntityId(this.ownerEntityId)
|
||||||
.setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT);
|
.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.setTitleId(this.metaMonster.title_id)
|
monsterInfo
|
||||||
|
.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.setTitleId(data.getDescribeData().getTitleId())
|
monsterInfo
|
||||||
|
.setTitleId(data.getDescribeData().getTitleId())
|
||||||
.setSpecialNameId(data.getSpecialNameId());
|
.setSpecialNameId(data.getSpecialNameId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.getMonsterWeaponId() > 0) {
|
if (this.getMonsterWeaponId() > 0) {
|
||||||
SceneWeaponInfo weaponInfo = SceneWeaponInfo.newBuilder()
|
SceneWeaponInfo weaponInfo =
|
||||||
|
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())
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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.*;
|
||||||
@ -32,6 +33,8 @@ 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
|
||||||
@ -110,6 +113,21 @@ public abstract class GameEntity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setLimbo(float hpThreshold) {
|
||||||
|
limbo = true;
|
||||||
|
limboHpThreshold = hpThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onAddAbilityModifier(AbilityModifier data) {
|
||||||
|
// Set limbo state (invulnerability at a certain HP threshold)
|
||||||
|
// if ability modifier calls for it
|
||||||
|
if (data.state == AbilityModifier.State.Limbo
|
||||||
|
&& data.properties != null
|
||||||
|
&& data.properties.Actor_HpThresholdRatio > .0f) {
|
||||||
|
this.setLimbo(data.properties.Actor_HpThresholdRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected MotionInfo getMotionInfo() {
|
protected MotionInfo getMotionInfo() {
|
||||||
return MotionInfo.newBuilder()
|
return MotionInfo.newBuilder()
|
||||||
.setPos(this.getPosition().toProto())
|
.setPos(this.getPosition().toProto())
|
||||||
@ -167,20 +185,29 @@ 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 (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
|
if (limbo) {
|
||||||
// Add negative HP to the current HP property.
|
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||||
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
|
float curRatio = curHp / maxHp;
|
||||||
|
if (curRatio > limboHpThreshold) {
|
||||||
|
// OK if this hit takes HP below threshold.
|
||||||
|
effectiveDamage = event.getDamage();
|
||||||
}
|
}
|
||||||
|
if (effectiveDamage >= curHp && limboHpThreshold > .0f) {
|
||||||
|
// Don't let entity die while in limbo.
|
||||||
|
effectiveDamage = curHp - 1;
|
||||||
|
}
|
||||||
|
} else if (curHp != Float.POSITIVE_INFINITY && !lockHP
|
||||||
|
|| lockHP && curHp <= event.getDamage()) {
|
||||||
|
effectiveDamage = event.getDamage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add negative HP to the current HP property.
|
||||||
|
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -effectiveDamage);
|
||||||
|
|
||||||
this.lastAttackType = attackType;
|
this.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
|
||||||
@ -194,6 +221,17 @@ 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}.
|
||||||
*
|
*
|
||||||
@ -333,6 +371,8 @@ 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. */
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package emu.grasscutter.game.entity.gadget;
|
package emu.grasscutter.game.entity.gadget;
|
||||||
|
|
||||||
import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
|
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
|
||||||
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 DungeonChallenge) {
|
if (player.getScene().getChallenge() instanceof WorldChallenge) {
|
||||||
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());
|
||||||
|
@ -9,9 +9,10 @@ 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;
|
import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo;
|
||||||
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.*;
|
||||||
@ -36,6 +37,10 @@ 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;
|
||||||
|
|
||||||
@ -181,6 +186,7 @@ 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));
|
||||||
@ -194,6 +200,35 @@ 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(
|
||||||
@ -239,8 +274,7 @@ public class GameHome {
|
|||||||
return this.finishedTalkIdMap.get(avatarId);
|
return this.finishedTalkIdMap.get(avatarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo>
|
public List<HomeAvatarTalkFinishInfo> toAvatarTalkFinishInfoProto() {
|
||||||
toAvatarTalkFinishInfoProto() {
|
|
||||||
if (this.finishedTalkIdMap == null) {
|
if (this.finishedTalkIdMap == null) {
|
||||||
this.finishedTalkIdMap = new HashMap<>();
|
this.finishedTalkIdMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
@ -248,7 +282,7 @@ public class GameHome {
|
|||||||
return this.finishedTalkIdMap.entrySet().stream()
|
return this.finishedTalkIdMap.entrySet().stream()
|
||||||
.map(
|
.map(
|
||||||
e -> {
|
e -> {
|
||||||
return HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo.newBuilder()
|
return HomeAvatarTalkFinishInfo.newBuilder()
|
||||||
.setAvatarId(e.getKey())
|
.setAvatarId(e.getKey())
|
||||||
.addAllFinishTalkIdList(e.getValue())
|
.addAllFinishTalkIdList(e.getValue())
|
||||||
.build();
|
.build();
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
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;
|
import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify;
|
||||||
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass;
|
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify;
|
||||||
import emu.grasscutter.net.proto.RetcodeOuterClass;
|
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||||
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;
|
||||||
@ -24,8 +26,8 @@ public class HomeModuleManager {
|
|||||||
final HomeWorld homeWorld;
|
final HomeWorld homeWorld;
|
||||||
final GameHome home;
|
final GameHome home;
|
||||||
final int moduleId;
|
final int moduleId;
|
||||||
final HomeScene outdoor;
|
@Nullable final HomeScene outdoor;
|
||||||
HomeScene indoor;
|
@Nullable HomeScene indoor;
|
||||||
final List<HomeAvatarRewardEvent> rewardEvents;
|
final List<HomeAvatarRewardEvent> rewardEvents;
|
||||||
final List<HomeAvatarSummonEvent> summonEvents;
|
final List<HomeAvatarSummonEvent> summonEvents;
|
||||||
|
|
||||||
@ -45,8 +47,14 @@ public class HomeModuleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.outdoor != null) {
|
||||||
this.outdoor.onTick();
|
this.outdoor.onTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indoor != null) {
|
||||||
this.indoor.onTick();
|
this.indoor.onTick();
|
||||||
|
}
|
||||||
|
|
||||||
this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
|
this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +75,7 @@ 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)
|
||||||
@ -114,6 +123,7 @@ 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)
|
||||||
@ -127,16 +137,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(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
|
return Either.right(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(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
|
return Either.right(Retcode.RET_FAIL_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
|
if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
|
return Either.right(Retcode.RET_FAIL_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Either.left(event.giveRewards());
|
return Either.left(event.giveRewards());
|
||||||
@ -144,32 +154,34 @@ 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) {
|
||||||
var targetSuite =
|
HomeSuiteItem targetSuite = null;
|
||||||
((HomeScene) owner.getScene())
|
if (owner.getScene() instanceof HomeScene homeScene) {
|
||||||
.getSceneItem().getBlockItems().values().stream()
|
targetSuite =
|
||||||
|
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(RetcodeOuterClass.Retcode.RET_DUPLICATE_AVATAR_VALUE);
|
return Either.right(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(RetcodeOuterClass.Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
|
return Either.right(Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
|
this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
|
||||||
|
|
||||||
if (targetSuite == null) {
|
if (targetSuite == null) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
|
var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
|
||||||
if (eventData == null) {
|
if (eventData == null) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
var event =
|
var event =
|
||||||
@ -184,8 +196,8 @@ public class HomeModuleManager {
|
|||||||
this.summonEvents.removeIf(event -> event.getEventId() == eventId);
|
this.summonEvents.removeIf(event -> event.getEventId() == eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify toRewardEventProto() {
|
public HomeAvatarRewardEventNotify toRewardEventProto() {
|
||||||
var notify = HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify.newBuilder();
|
var notify = 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);
|
||||||
|
|
||||||
@ -198,9 +210,8 @@ public class HomeModuleManager {
|
|||||||
return notify.build();
|
return notify.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify
|
public HomeAvatarSummonAllEventNotify toSummonEventProto() {
|
||||||
toSummonEventProto() {
|
return HomeAvatarSummonAllEventNotify.newBuilder()
|
||||||
return HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify.newBuilder()
|
|
||||||
.addAllSummonEventList(
|
.addAllSummonEventList(
|
||||||
this.summonEvents.stream().map(HomeAvatarSummonEvent::toProto).toList())
|
this.summonEvents.stream().map(HomeAvatarSummonEvent::toProto).toList())
|
||||||
.build();
|
.build();
|
||||||
@ -210,12 +221,12 @@ public class HomeModuleManager {
|
|||||||
return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
|
return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeSceneItem getOutdoorSceneItem() {
|
@Nullable public HomeSceneItem getOutdoorSceneItem() {
|
||||||
return this.outdoor.getSceneItem();
|
return this.outdoor == null ? null : this.outdoor.getSceneItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeSceneItem getIndoorSceneItem() {
|
@Nullable public HomeSceneItem getIndoorSceneItem() {
|
||||||
return this.indoor.getSceneItem();
|
return this.indoor == null ? null : this.indoor.getSceneItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSetModule() {
|
public void onSetModule() {
|
||||||
@ -223,8 +234,14 @@ 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indoor != null) {
|
||||||
this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
|
this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
|
||||||
|
}
|
||||||
|
|
||||||
this.fireAllAvatarRewardEvents();
|
this.fireAllAvatarRewardEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +250,12 @@ public class HomeModuleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.outdoor != null) {
|
||||||
this.outdoor.getEntities().clear();
|
this.outdoor.getEntities().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indoor != null) {
|
||||||
this.indoor.getEntities().clear();
|
this.indoor.getEntities().clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@ 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;
|
||||||
@ -13,6 +12,7 @@ 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
|
||||||
@ -66,7 +66,7 @@ public class HomeWorld extends World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRealmIdValid() {
|
public boolean isRealmIdValid() {
|
||||||
return this.getHost().getCurrentRealmId() > 0;
|
return this.getSceneById(this.getHost().getCurrentRealmId() + 2000) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -147,11 +147,13 @@ public class HomeWorld extends World {
|
|||||||
player.setWorld(null);
|
player.setWorld(null);
|
||||||
|
|
||||||
// Remove from scene
|
// Remove from scene
|
||||||
Scene scene = this.getSceneById(player.getSceneId());
|
var 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().size() > 0) {
|
if (!this.getPlayers().isEmpty()) {
|
||||||
this.updatePlayerInfos(player);
|
this.updatePlayerInfos(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +169,7 @@ public class HomeWorld extends World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HomeScene getSceneById(int sceneId) {
|
@Nullable 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;
|
||||||
|
@ -139,11 +139,15 @@ 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 =
|
|
||||||
toSafe
|
Position pos;
|
||||||
? world.getSceneById(realmId).getScriptManager().getConfig().born_pos
|
if (scene != null) {
|
||||||
: item.getBornPos();
|
pos = toSafe ? scene.getScriptManager().getConfig().born_pos : item.getBornPos();
|
||||||
|
} else {
|
||||||
|
pos = item.getBornPos();
|
||||||
|
}
|
||||||
|
|
||||||
if (teleportPoint != 0) {
|
if (teleportPoint != 0) {
|
||||||
var target = item.getTeleportPointPos(teleportPoint);
|
var target = item.getTeleportPointPos(teleportPoint);
|
||||||
|
@ -259,8 +259,14 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -394,10 +400,11 @@ 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();
|
||||||
|
if (skillDepot != null) {
|
||||||
entityAvatar.addEnergy(
|
entityAvatar.addEnergy(
|
||||||
entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(),
|
skillDepot.getEnergySkillData().getCostElemVal(), changeReason, isFlat);
|
||||||
changeReason,
|
}
|
||||||
isFlat);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +176,7 @@ 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;
|
||||||
@ -1530,6 +1531,31 @@ public class Player implements PlayerHook, FieldFetch {
|
|||||||
getServer().getPlayers().values().removeIf(player1 -> player1 == 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() {
|
||||||
return this.getProperty(PlayerProperty.PROP_PLAYER_LEGENDARY_KEY);
|
return this.getProperty(PlayerProperty.PROP_PLAYER_LEGENDARY_KEY);
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,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, 0);
|
int previousValue = this.player.getOpenStates().getOrDefault(openState, -1 /* non-existent */);
|
||||||
|
|
||||||
if (value != previousValue) {
|
if (value != previousValue) {
|
||||||
this.player.getOpenStates().put(openState, value);
|
this.player.getOpenStates().put(openState, value);
|
||||||
|
@ -425,6 +425,30 @@ 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) {
|
||||||
@ -700,15 +724,16 @@ public final class TeamManager extends BasePlayerDataManager {
|
|||||||
this.updateTeamEntities(null);
|
this.updateTeamEntities(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanTemporaryTeam() {
|
public boolean cleanTemporaryTeam() {
|
||||||
// check if using temporary team
|
// check if using temporary team
|
||||||
if (useTemporarilyTeamIndex < 0) {
|
if (useTemporarilyTeamIndex < 0) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -810,20 +835,13 @@ 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 {
|
||||||
// Replacement avatar
|
// Find replacement avatar
|
||||||
EntityAvatar replacement = null;
|
int replaceIndex = getDeadAvatarReplacement();
|
||||||
int replaceIndex = -1;
|
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
|
||||||
|
// Set index and spawn replacement member
|
||||||
for (int i = 0; i < this.getActiveTeam().size(); i++) {
|
this.setCurrentCharacterIndex(replaceIndex);
|
||||||
EntityAvatar entity = this.getActiveTeam().get(i);
|
this.getPlayer().getScene().addEntity(this.getActiveTeam().get(replaceIndex));
|
||||||
if (entity.isAlive()) {
|
} else {
|
||||||
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.
|
||||||
@ -831,10 +849,6 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,6 +856,20 @@ 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) {
|
||||||
|
@ -295,6 +295,17 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,9 @@ 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);
|
||||||
|
|
||||||
return checkQuest != null && checkQuest.getState().getValue() == questStateValue;
|
if (checkQuest == null) {
|
||||||
|
return questStateValue == 0;
|
||||||
|
}
|
||||||
|
return checkQuest.getState().getValue() == questStateValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,9 @@ 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);
|
||||||
|
|
||||||
return checkQuest != null && checkQuest.getState().getValue() != questStateValue;
|
if (checkQuest == null) {
|
||||||
|
return questStateValue != 0;
|
||||||
|
}
|
||||||
|
return checkQuest.getState().getValue() != questStateValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@ -23,6 +29,11 @@ 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;
|
||||||
}
|
}
|
||||||
@ -32,6 +43,32 @@ 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) {
|
||||||
@ -61,8 +98,17 @@ 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 = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
|
var levelData = getCurrentTowerLevelDataMap();
|
||||||
|
|
||||||
var dungeonId = levelData.getDungeonId();
|
var dungeonId = levelData.getDungeonId();
|
||||||
|
|
||||||
@ -84,9 +130,12 @@ 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(new PacketTowerLevelStarCondNotify(getTowerData().currentFloorId, getCurrentLevel()));
|
.send(
|
||||||
|
new PacketTowerLevelStarCondNotify(
|
||||||
|
getTowerData().currentFloorId, getCurrentLevel(), currentPossibleStars + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyCurLevelRecordChange() {
|
public void notifyCurLevelRecordChange() {
|
||||||
@ -97,6 +146,44 @@ 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;
|
||||||
@ -105,8 +192,16 @@ public class TowerManager extends BasePlayerManager {
|
|||||||
currentFloorId,
|
currentFloorId,
|
||||||
new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
|
new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
|
||||||
} else {
|
} else {
|
||||||
recordMap.put(
|
// Only update record if better than previous
|
||||||
currentFloorId, recordMap.get(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
|
var prevRecord = recordMap.get(currentFloorId);
|
||||||
|
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++;
|
||||||
|
@ -5,6 +5,13 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class GroupReplacementData {
|
public class GroupReplacementData {
|
||||||
int id;
|
public int id;
|
||||||
List<Integer> replace_groups;
|
public List<Integer> replace_groups;
|
||||||
|
|
||||||
|
public GroupReplacementData() {}
|
||||||
|
|
||||||
|
public GroupReplacementData(int id, List<Integer> replace_groups) {
|
||||||
|
this.id = id;
|
||||||
|
this.replace_groups = replace_groups;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ public class Scene {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameEntity getEntityByConfigId(int configId) {
|
public GameEntity getFirstEntityByConfigId(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,6 +597,13 @@ 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();
|
||||||
@ -760,6 +767,19 @@ 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()) {
|
||||||
@ -1103,6 +1123,9 @@ 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);
|
||||||
|
@ -2,16 +2,22 @@ package emu.grasscutter.game.world;
|
|||||||
|
|
||||||
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
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.*;
|
import emu.grasscutter.game.entity.EntityTeam;
|
||||||
|
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.*;
|
import emu.grasscutter.game.props.EnterReason;
|
||||||
|
import emu.grasscutter.game.props.EntityIdType;
|
||||||
|
import emu.grasscutter.game.props.PlayerProperty;
|
||||||
|
import emu.grasscutter.game.props.SceneType;
|
||||||
import emu.grasscutter.game.quest.enums.QuestContent;
|
import emu.grasscutter.game.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.*;
|
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHint;
|
||||||
|
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHintType;
|
||||||
import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType;
|
import emu.grasscutter.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;
|
||||||
@ -19,9 +25,21 @@ 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 it.unimi.dsi.fastutil.ints.*;
|
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||||
import java.util.*;
|
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;
|
||||||
|
|
||||||
public class World implements Iterable<Player> {
|
public class World implements Iterable<Player> {
|
||||||
@ -43,6 +61,16 @@ 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);
|
||||||
}
|
}
|
||||||
@ -111,7 +139,7 @@ public class World implements Iterable<Player> {
|
|||||||
* @param sceneId The scene ID.
|
* @param sceneId The scene ID.
|
||||||
* @return The scene.
|
* @return The scene.
|
||||||
*/
|
*/
|
||||||
public Scene getSceneById(int sceneId) {
|
@Nullable 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) {
|
||||||
@ -139,7 +167,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 int getNextEntityId(EntityIdType idType) {
|
public synchronized int getNextEntityId(EntityIdType idType) {
|
||||||
return (idType.getId() << 24) + ++this.nextEntityId;
|
return (idType.getId() << 24) + ++this.nextEntityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,6 +339,21 @@ 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);
|
||||||
}
|
}
|
||||||
@ -381,6 +424,16 @@ 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());
|
||||||
|
@ -17,6 +17,7 @@ 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;
|
||||||
@ -38,12 +39,14 @@ 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;
|
||||||
@ -74,6 +77,7 @@ 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<>();
|
||||||
@ -134,7 +138,9 @@ 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().trace("Registered trigger {}", trigger.getName());
|
Grasscutter.getLogger()
|
||||||
|
.trace(
|
||||||
|
"Registered trigger {} from group {}", trigger.getName(), trigger.getCurrentGroup().id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deregisterTrigger(List<SceneTrigger> triggers) {
|
public void deregisterTrigger(List<SceneTrigger> triggers) {
|
||||||
@ -143,7 +149,11 @@ 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().trace("deregistered trigger {}", trigger.getName());
|
Grasscutter.getLogger()
|
||||||
|
.trace(
|
||||||
|
"deregistered trigger {} from group {}",
|
||||||
|
trigger.getName(),
|
||||||
|
trigger.getCurrentGroup().id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetTriggers(int eventId) {
|
public void resetTriggers(int eventId) {
|
||||||
@ -256,6 +266,15 @@ 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 -> {
|
||||||
@ -265,6 +284,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +346,7 @@ public class SceneScriptManager {
|
|||||||
group.monsters.values().stream()
|
group.monsters.values().stream()
|
||||||
.filter(
|
.filter(
|
||||||
m -> {
|
m -> {
|
||||||
var entity = scene.getEntityByConfigId(m.config_id);
|
var entity = scene.getEntityByConfigId(m.config_id, groupId);
|
||||||
return (entity == null
|
return (entity == null
|
||||||
|| entity.getGroupId()
|
|| entity.getGroupId()
|
||||||
!= group
|
!= group
|
||||||
@ -434,6 +457,17 @@ 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;
|
||||||
@ -451,7 +485,9 @@ 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() && !Grasscutter.config.server.game.cacheSceneEntitiesEveryRun) {
|
if (path.toFile().isFile()
|
||||||
|
&& !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);
|
||||||
@ -581,6 +617,7 @@ 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) {
|
||||||
@ -589,7 +626,9 @@ public class SceneScriptManager {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@ -694,7 +733,7 @@ public class SceneScriptManager {
|
|||||||
return suite.sceneGadgets.stream()
|
return suite.sceneGadgets.stream()
|
||||||
.filter(
|
.filter(
|
||||||
m -> {
|
m -> {
|
||||||
var entity = scene.getEntityByConfigId(m.config_id);
|
var entity = scene.getEntityByConfigId(m.config_id, group.id);
|
||||||
return (entity == null || entity.getGroupId() != group.id)
|
return (entity == null || entity.getGroupId() != group.id)
|
||||||
&& (!m.isOneoff
|
&& (!m.isOneoff
|
||||||
|| !m.persistent
|
|| !m.persistent
|
||||||
@ -712,7 +751,7 @@ public class SceneScriptManager {
|
|||||||
return suite.sceneMonsters.stream()
|
return suite.sceneMonsters.stream()
|
||||||
.filter(
|
.filter(
|
||||||
m -> {
|
m -> {
|
||||||
var entity = scene.getEntityByConfigId(m.config_id);
|
var entity = scene.getEntityByConfigId(m.config_id, group.id);
|
||||||
return (entity == null
|
return (entity == null
|
||||||
|| entity.getGroupId()
|
|| entity.getGroupId()
|
||||||
!= group
|
!= group
|
||||||
@ -774,9 +813,9 @@ public class SceneScriptManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void startMonsterTideInGroup(
|
public void startMonsterTideInGroup(
|
||||||
SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
|
String source, SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
|
||||||
this.scriptMonsterTideService =
|
this.scriptMonsterTideService =
|
||||||
new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
|
new ScriptMonsterTideService(this, source, group, tideCount, sceneLimit, ordersConfigId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unloadCurrentMonsterTide() {
|
public void unloadCurrentMonsterTide() {
|
||||||
@ -788,7 +827,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);
|
var entity = scene.getEntityByConfigId(configId, group.id);
|
||||||
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);
|
||||||
@ -803,11 +842,11 @@ public class SceneScriptManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Events
|
// Events
|
||||||
public void callEvent(int groupId, int eventType) {
|
public Future<?> callEvent(int groupId, int eventType) {
|
||||||
callEvent(new ScriptArgs(groupId, eventType));
|
return callEvent(new ScriptArgs(groupId, eventType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void callEvent(@Nonnull ScriptArgs params) {
|
public Future<?> 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
|
||||||
@ -815,7 +854,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.
|
||||||
*/
|
*/
|
||||||
eventExecutor.submit(() -> this.realCallEvent(params));
|
return eventExecutor.submit(() -> this.realCallEvent(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void realCallEvent(@Nonnull ScriptArgs params) {
|
private void realCallEvent(@Nonnull ScriptArgs params) {
|
||||||
@ -884,9 +923,11 @@ 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 {}, [{},{},{}]",
|
"Call Condition Trigger {}, [{},{},{}], source_eid {}, target_eid {}",
|
||||||
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);
|
||||||
@ -895,6 +936,7 @@ 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();
|
||||||
@ -926,11 +968,15 @@ 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
|
||||||
if (ret.isboolean() && !ret.checkboolean()
|
// or the trigger should be preserved after a RefreshGroup call
|
||||||
|
if (trigger.isPreserved()) {
|
||||||
|
trigger.setPreserved(false);
|
||||||
|
} else if (ret.isboolean() && !ret.checkboolean()
|
||||||
|| ret.isint() && ret.checkint() != 0
|
|| 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) {
|
||||||
@ -1023,18 +1069,7 @@ public class SceneScriptManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate level
|
// Calculate level
|
||||||
int level = monster.level;
|
int level = getScene().getLevelForMonster(monster.config_id, 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, monster.rot, level);
|
||||||
@ -1074,6 +1109,19 @@ 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 =
|
||||||
@ -1194,7 +1242,7 @@ public class SceneScriptManager {
|
|||||||
return monsters.values().stream()
|
return monsters.values().stream()
|
||||||
.noneMatch(
|
.noneMatch(
|
||||||
m -> {
|
m -> {
|
||||||
val entity = scene.getEntityByConfigId(m.config_id);
|
val entity = scene.getEntityByConfigId(m.config_id, groupId);
|
||||||
return entity != null && entity.getGroupId() == groupId;
|
return entity != null && entity.getGroupId() == groupId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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.Files;
|
import java.nio.file.*;
|
||||||
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,6 +172,17 @@ 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()) {
|
||||||
@ -179,8 +190,11 @@ public class ScriptLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to load the script.
|
// Attempt to load the script.
|
||||||
var scriptPath = FileUtils.getScriptPath(path);
|
var scriptPath = useAbsPath ? Paths.get(path) : FileUtils.getScriptPath(path);
|
||||||
if (!Files.exists(scriptPath)) return null;
|
if (!Files.exists(scriptPath)) {
|
||||||
|
Grasscutter.getLogger().error("Could not find script at path {}", path);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var source = Files.readString(scriptPath);
|
var source = Files.readString(scriptPath);
|
||||||
@ -201,6 +215,17 @@ 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()) {
|
||||||
@ -211,15 +236,18 @@ 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 = FileUtils.getScriptPath(path);
|
var scriptPath = useAbsPath ? Paths.get(path) : FileUtils.getScriptPath(path);
|
||||||
if (!Files.exists(scriptPath)) return null;
|
if (!Files.exists(scriptPath)) {
|
||||||
|
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);
|
var sources = ScriptLoader.readScript(path, useAbsPath);
|
||||||
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.
|
||||||
@ -237,7 +265,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);
|
var scriptSource = ScriptLoader.readScript(scriptPath, useAbsPath);
|
||||||
if (scriptSource == null) continue;
|
if (scriptSource == null) continue;
|
||||||
|
|
||||||
// Append the script source.
|
// Append the script source.
|
||||||
|
@ -5,6 +5,7 @@ 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.*;
|
||||||
@ -64,6 +65,10 @@ 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) {
|
||||||
|
@ -3,6 +3,7 @@ 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.*;
|
||||||
@ -39,6 +40,7 @@ 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();
|
||||||
@ -86,8 +88,14 @@ public final class SceneGroup {
|
|||||||
// Create the bindings.
|
// Create the bindings.
|
||||||
this.bindings = ScriptLoader.getEngine().createBindings();
|
this.bindings = ScriptLoader.getEngine().createBindings();
|
||||||
|
|
||||||
var cs =
|
CompiledScript cs;
|
||||||
ScriptLoader.getScript("Scene/%s/scene%s_group%s.lua".formatted(sceneId, sceneId, this.id));
|
if (overrideScriptPath != null && !overrideScriptPath.equals("")) {
|
||||||
|
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;
|
||||||
|
@ -17,6 +17,7 @@ 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) {
|
||||||
|
@ -20,9 +20,11 @@ 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,
|
||||||
@ -35,6 +37,7 @@ 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()
|
||||||
@ -59,7 +62,11 @@ public final class ScriptMonsterTideService {
|
|||||||
|
|
||||||
public SceneMonster getNextMonster() {
|
public SceneMonster getNextMonster() {
|
||||||
var nextId = this.monsterConfigOrders.poll();
|
var nextId = this.monsterConfigOrders.poll();
|
||||||
if (currentGroup.monsters.containsKey(nextId)) {
|
if (nextId == null) {
|
||||||
|
// AutoMonsterTide has been called with fewer monster config IDs than the total tide count.
|
||||||
|
// Get last config ID from the list, then.
|
||||||
|
return currentGroup.monsters.get(monsterConfigIds.get(monsterConfigIds.size() - 1));
|
||||||
|
} else if (currentGroup.monsters.containsKey(nextId)) {
|
||||||
return currentGroup.monsters.get(nextId);
|
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
|
||||||
@ -83,11 +90,11 @@ public final class ScriptMonsterTideService {
|
|||||||
sceneScriptManager.createMonster(
|
sceneScriptManager.createMonster(
|
||||||
currentGroup.id, currentGroup.block_id, getNextMonster()));
|
currentGroup.id, currentGroup.block_id, getNextMonster()));
|
||||||
}
|
}
|
||||||
// spawn the last turn of monsters
|
// call registered events that may spawn in more monsters
|
||||||
// fix the 5-2
|
var scriptArgs =
|
||||||
sceneScriptManager.callEvent(
|
new ScriptArgs(currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get());
|
||||||
new ScriptArgs(
|
scriptArgs.setEventSource(source);
|
||||||
currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get()));
|
sceneScriptManager.callEvent(scriptArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,6 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package emu.grasscutter.server.packet.recv;
|
||||||
|
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.GetDungeonEntryExploreConditionReqOuterClass.GetDungeonEntryExploreConditionReq;
|
||||||
|
import emu.grasscutter.server.game.GameSession;
|
||||||
|
import emu.grasscutter.server.packet.send.PacketDungeonEntryToBeExploreNotify;
|
||||||
|
|
||||||
|
@Opcodes(PacketOpcodes.GetDungeonEntryExploreConditionReq)
|
||||||
|
public class HandlerGetDungeonEntryExploreConditionReq extends PacketHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||||
|
var req = GetDungeonEntryExploreConditionReq.parseFrom(payload);
|
||||||
|
|
||||||
|
// TODO Send GetDungeonEntryExploreConditionRsp if condition
|
||||||
|
// (adventurer rank or quest completion) is not met. Parse
|
||||||
|
// dungeon entry conditions from DungeonEntryExcelConfigData.json.
|
||||||
|
// session.send(new PacketGetDungeonEntryExploreConditionRsp(req.getDungeonEntryConfigId()));
|
||||||
|
|
||||||
|
// For now, just unlock any domain the player touches.
|
||||||
|
session.send(
|
||||||
|
new PacketDungeonEntryToBeExploreNotify(
|
||||||
|
req.getDungeonEntryScenePointId(), req.getSceneId(), req.getDungeonEntryConfigId()));
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package emu.grasscutter.server.packet.recv;
|
package emu.grasscutter.server.packet.recv;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.net.packet.Opcodes;
|
import emu.grasscutter.net.packet.Opcodes;
|
||||||
import emu.grasscutter.net.packet.PacketHandler;
|
import emu.grasscutter.net.packet.PacketHandler;
|
||||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||||
import emu.grasscutter.net.proto.HomeChangeModuleReqOuterClass;
|
import emu.grasscutter.net.proto.HomeChangeModuleReqOuterClass.HomeChangeModuleReq;
|
||||||
|
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||||
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
|
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
|
||||||
import emu.grasscutter.server.game.GameSession;
|
import emu.grasscutter.server.game.GameSession;
|
||||||
import emu.grasscutter.server.packet.send.PacketHomeAvatarTalkFinishInfoNotify;
|
import emu.grasscutter.server.packet.send.PacketHomeAvatarTalkFinishInfoNotify;
|
||||||
@ -16,12 +18,20 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||||
HomeChangeModuleReqOuterClass.HomeChangeModuleReq req =
|
var req = HomeChangeModuleReq.parseFrom(payload);
|
||||||
HomeChangeModuleReqOuterClass.HomeChangeModuleReq.parseFrom(payload);
|
|
||||||
|
|
||||||
var homeWorld = session.getPlayer().getCurHomeWorld();
|
var homeWorld = session.getPlayer().getCurHomeWorld();
|
||||||
if (!homeWorld.getGuests().isEmpty()) {
|
if (!homeWorld.getGuests().isEmpty()) {
|
||||||
session.send(new PacketHomeChangeModuleRsp());
|
session.send(new PacketHomeChangeModuleRsp(Retcode.RET_HOME_HAS_GUEST));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int realmId = 2000 + req.getTargetModuleId();
|
||||||
|
var scene = homeWorld.getSceneById(realmId);
|
||||||
|
|
||||||
|
if (scene == null) {
|
||||||
|
Grasscutter.getLogger().warn("scene == null! Changing module will fail.");
|
||||||
|
session.send(new PacketHomeChangeModuleRsp(Retcode.RET_INVALID_SCENE_ID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,8 +41,6 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
|
|||||||
session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer()));
|
session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer()));
|
||||||
session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
|
session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
|
||||||
|
|
||||||
int realmId = 2000 + req.getTargetModuleId();
|
|
||||||
var scene = homeWorld.getSceneById(realmId);
|
|
||||||
var pos = scene.getScriptManager().getConfig().born_pos;
|
var pos = scene.getScriptManager().getConfig().born_pos;
|
||||||
|
|
||||||
homeWorld.transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);
|
homeWorld.transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);
|
||||||
|
@ -34,8 +34,7 @@ public class HandlerHomeSceneJumpReq extends PacketHandler {
|
|||||||
pos = home.getSceneMap().get(realmId).getBornPos();
|
pos = home.getSceneMap().get(realmId).getBornPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
world.transferPlayerToScene(
|
world.transferPlayerToScene(session.getPlayer(), scene.getId(), pos);
|
||||||
session.getPlayer(), req.getIsEnterRoomScene() ? homeScene.getRoomSceneId() : realmId, pos);
|
|
||||||
|
|
||||||
session.send(new PacketHomeSceneJumpRsp(req.getIsEnterRoomScene()));
|
session.send(new PacketHomeSceneJumpRsp(req.getIsEnterRoomScene()));
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,11 @@ public class HandlerPersonalSceneJumpReq extends PacketHandler {
|
|||||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||||
PersonalSceneJumpReq req = PersonalSceneJumpReq.parseFrom(payload);
|
PersonalSceneJumpReq req = PersonalSceneJumpReq.parseFrom(payload);
|
||||||
var player = session.getPlayer();
|
var player = session.getPlayer();
|
||||||
|
var prevSceneId = player.getSceneId();
|
||||||
|
|
||||||
// get the scene point
|
// get the scene point
|
||||||
ScenePointEntry scenePointEntry =
|
ScenePointEntry scenePointEntry =
|
||||||
GameData.getScenePointEntryById(player.getSceneId(), req.getPointId());
|
GameData.getScenePointEntryById(prevSceneId, req.getPointId());
|
||||||
|
|
||||||
if (scenePointEntry != null) {
|
if (scenePointEntry != null) {
|
||||||
Position pos =
|
Position pos =
|
||||||
@ -26,6 +27,7 @@ public class HandlerPersonalSceneJumpReq extends PacketHandler {
|
|||||||
int sceneId = scenePointEntry.getPointData().getTranSceneId();
|
int sceneId = scenePointEntry.getPointData().getTranSceneId();
|
||||||
|
|
||||||
player.getWorld().transferPlayerToScene(player, sceneId, pos);
|
player.getWorld().transferPlayerToScene(player, sceneId, pos);
|
||||||
|
player.getScene().setPrevScene(prevSceneId);
|
||||||
session.send(new PacketPersonalSceneJumpRsp(sceneId, pos));
|
session.send(new PacketPersonalSceneJumpRsp(sceneId, pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.DungeonEntryToBeExploreNotifyOuterClass.DungeonEntryToBeExploreNotify;
|
||||||
|
|
||||||
|
public class PacketDungeonEntryToBeExploreNotify extends BasePacket {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the dungeon as pending exploration. This creates the "Unknown" text bubble above the
|
||||||
|
* dungeon entry in the world map.
|
||||||
|
*/
|
||||||
|
public PacketDungeonEntryToBeExploreNotify(
|
||||||
|
int dungeonEntryScenePointId, int sceneId, int dungeonEntryConfigId) {
|
||||||
|
super(PacketOpcodes.DungeonEntryToBeExploreNotify);
|
||||||
|
this.setData(
|
||||||
|
DungeonEntryToBeExploreNotify.newBuilder()
|
||||||
|
.setDungeonEntryScenePointId(dungeonEntryScenePointId)
|
||||||
|
.setSceneId(sceneId)
|
||||||
|
.setDungeonEntryConfigId(dungeonEntryConfigId));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.data.GameData;
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.DungeonEntryBlockReasonOuterClass.DungeonEntryBlockReason;
|
||||||
|
import emu.grasscutter.net.proto.DungeonEntryCondOuterClass.DungeonEntryCond;
|
||||||
|
import emu.grasscutter.net.proto.GetDungeonEntryExploreConditionRspOuterClass.GetDungeonEntryExploreConditionRsp;
|
||||||
|
|
||||||
|
public class PacketGetDungeonEntryExploreConditionRsp extends BasePacket {
|
||||||
|
public PacketGetDungeonEntryExploreConditionRsp(int dungeonId) {
|
||||||
|
super(PacketOpcodes.GetDungeonEntryExploreConditionRsp);
|
||||||
|
|
||||||
|
var data =
|
||||||
|
GameData.getDungeonEntryDataMap().values().stream()
|
||||||
|
.filter(d -> d.getId() == dungeonId)
|
||||||
|
.toList()
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
var level = data.getLevelCondition();
|
||||||
|
var quest = data.getQuestCondition();
|
||||||
|
var proto =
|
||||||
|
GetDungeonEntryExploreConditionRsp.newBuilder()
|
||||||
|
.setRetcode(0)
|
||||||
|
.setDungeonEntryCond(
|
||||||
|
DungeonEntryCond.newBuilder()
|
||||||
|
// There is also a DUNGEON_ENTRY_REASON_MULIPLE but only one param1
|
||||||
|
// field to put values in. Only report the required level for now, then.
|
||||||
|
.setCondReason(DungeonEntryBlockReason.DUNGEON_ENTRY_REASON_LEVEL)
|
||||||
|
.setParam1(level))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.setData(proto);
|
||||||
|
}
|
||||||
|
}
|
@ -2,28 +2,23 @@ package emu.grasscutter.server.packet.send;
|
|||||||
|
|
||||||
import emu.grasscutter.net.packet.BasePacket;
|
import emu.grasscutter.net.packet.BasePacket;
|
||||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||||
import emu.grasscutter.net.proto.HomeChangeModuleRspOuterClass;
|
import emu.grasscutter.net.proto.HomeChangeModuleRspOuterClass.HomeChangeModuleRsp;
|
||||||
import emu.grasscutter.net.proto.RetcodeOuterClass;
|
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||||
|
|
||||||
public class PacketHomeChangeModuleRsp extends BasePacket {
|
public class PacketHomeChangeModuleRsp extends BasePacket {
|
||||||
|
|
||||||
public PacketHomeChangeModuleRsp(int targetModuleId) {
|
public PacketHomeChangeModuleRsp(int targetModuleId) {
|
||||||
super(PacketOpcodes.HomeChangeModuleRsp);
|
super(PacketOpcodes.HomeChangeModuleRsp);
|
||||||
|
|
||||||
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp proto =
|
var proto =
|
||||||
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp.newBuilder()
|
HomeChangeModuleRsp.newBuilder().setRetcode(0).setTargetModuleId(targetModuleId).build();
|
||||||
.setRetcode(0)
|
|
||||||
.setTargetModuleId(targetModuleId)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
this.setData(proto);
|
this.setData(proto);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PacketHomeChangeModuleRsp() {
|
public PacketHomeChangeModuleRsp(Retcode retcode) {
|
||||||
super(PacketOpcodes.HomeChangeModuleRsp);
|
super(PacketOpcodes.HomeChangeModuleRsp);
|
||||||
|
|
||||||
this.setData(
|
this.setData(HomeChangeModuleRsp.newBuilder().setRetcode(retcode.getNumber()));
|
||||||
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp.newBuilder()
|
|
||||||
.setRetcode(RetcodeOuterClass.Retcode.RET_HOME_HAS_GUEST_VALUE));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package emu.grasscutter.server.packet.send;
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.game.home.HomeBlockItem;
|
import emu.grasscutter.game.home.HomeBlockItem;
|
||||||
import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
|
import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.net.packet.*;
|
import emu.grasscutter.net.packet.BasePacket;
|
||||||
import emu.grasscutter.net.proto.*;
|
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||||
|
import emu.grasscutter.net.proto.HomeMarkPointNotifyOuterClass.HomeMarkPointNotify;
|
||||||
|
import emu.grasscutter.net.proto.HomeMarkPointSceneDataOuterClass.HomeMarkPointSceneData;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -13,17 +16,24 @@ public class PacketHomeMarkPointNotify extends BasePacket {
|
|||||||
public PacketHomeMarkPointNotify(Player player) {
|
public PacketHomeMarkPointNotify(Player player) {
|
||||||
super(PacketOpcodes.HomeMarkPointNotify);
|
super(PacketOpcodes.HomeMarkPointNotify);
|
||||||
|
|
||||||
var proto = HomeMarkPointNotifyOuterClass.HomeMarkPointNotify.newBuilder();
|
var proto = HomeMarkPointNotify.newBuilder();
|
||||||
var world = player.getCurHomeWorld();
|
var world = player.getCurHomeWorld();
|
||||||
var owner = world.getHost();
|
var owner = world.getHost();
|
||||||
var home = world.getHome();
|
var home = world.getHome();
|
||||||
|
|
||||||
if (owner.getRealmList() == null) {
|
if (owner.getRealmList() == null || owner.getRealmList().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// send current home mark points.
|
// send current home mark points.
|
||||||
var moduleId = owner.getCurrentRealmId();
|
var moduleId = owner.getCurrentRealmId();
|
||||||
|
var scene = world.getSceneById(moduleId + 2000);
|
||||||
|
if (scene == null) {
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.warn(
|
||||||
|
"Current Realm id is invalid! SceneExcelConfigData.json, game resource not loaded correctly or the realm id maybe wrong?!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var homeScene = home.getHomeSceneItem(moduleId + 2000);
|
var homeScene = home.getHomeSceneItem(moduleId + 2000);
|
||||||
var mainHouse = home.getMainHouseItem(moduleId + 2000);
|
var mainHouse = home.getMainHouseItem(moduleId + 2000);
|
||||||
|
|
||||||
@ -31,12 +41,12 @@ public class PacketHomeMarkPointNotify extends BasePacket {
|
|||||||
.forEach(
|
.forEach(
|
||||||
homeSceneItem -> {
|
homeSceneItem -> {
|
||||||
var markPointData =
|
var markPointData =
|
||||||
HomeMarkPointSceneDataOuterClass.HomeMarkPointSceneData.newBuilder()
|
HomeMarkPointSceneData.newBuilder()
|
||||||
.setModuleId(moduleId)
|
.setModuleId(moduleId)
|
||||||
.setSceneId(homeSceneItem.getSceneId());
|
.setSceneId(homeSceneItem.getSceneId());
|
||||||
|
|
||||||
if (!homeSceneItem.isRoom()) {
|
if (!homeSceneItem.isRoom()) {
|
||||||
var config = world.getSceneById(moduleId + 2000).getScriptManager().getConfig();
|
var config = scene.getScriptManager().getConfig();
|
||||||
markPointData
|
markPointData
|
||||||
.setSafePointPos(
|
.setSafePointPos(
|
||||||
config == null
|
config == null
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.game.entity.EntityMonster;
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.MonsterSummonTagNotifyOuterClass.MonsterSummonTagNotify;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PacketMonsterSummonTagNotify extends BasePacket {
|
||||||
|
|
||||||
|
public PacketMonsterSummonTagNotify(EntityMonster monsterEntity) {
|
||||||
|
super(PacketOpcodes.MonsterSummonTagNotify);
|
||||||
|
|
||||||
|
var proto = MonsterSummonTagNotify.newBuilder().setMonsterEntityId(monsterEntity.getId());
|
||||||
|
monsterEntity.getSummonTagMap().forEach((k, v) -> proto.putSummonTagMap(k, v == null ? 0 : 1));
|
||||||
|
|
||||||
|
this.setData(proto.build());
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,11 @@ public class PacketPostEnterSceneRsp extends BasePacket {
|
|||||||
PostEnterSceneRsp p =
|
PostEnterSceneRsp p =
|
||||||
PostEnterSceneRsp.newBuilder().setEnterSceneToken(player.getEnterSceneToken()).build();
|
PostEnterSceneRsp.newBuilder().setEnterSceneToken(player.getEnterSceneToken()).build();
|
||||||
|
|
||||||
|
//
|
||||||
|
// On moving to new scene:
|
||||||
|
// Unfreeze dungeon entry points that have already been unlocked in this scene.
|
||||||
|
player.unfreezeUnlockedScenePoints();
|
||||||
|
|
||||||
this.setData(p);
|
this.setData(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,27 +6,29 @@ import emu.grasscutter.net.proto.TowerLevelStarCondNotifyOuterClass.TowerLevelSt
|
|||||||
|
|
||||||
public class PacketTowerLevelStarCondNotify extends BasePacket {
|
public class PacketTowerLevelStarCondNotify extends BasePacket {
|
||||||
|
|
||||||
public PacketTowerLevelStarCondNotify(int floorId, int levelIndex) {
|
public PacketTowerLevelStarCondNotify(int floorId, int levelIndex, int lostStar) {
|
||||||
super(PacketOpcodes.TowerLevelStarCondNotify);
|
super(PacketOpcodes.TowerLevelStarCondNotify);
|
||||||
|
|
||||||
TowerLevelStarCondNotify proto =
|
var proto = TowerLevelStarCondNotify.newBuilder().setFloorId(floorId).setLevelIndex(levelIndex);
|
||||||
TowerLevelStarCondNotify.newBuilder()
|
|
||||||
.setFloorId(floorId)
|
|
||||||
.setLevelIndex(levelIndex)
|
|
||||||
.addCondDataList(
|
|
||||||
TowerLevelStarCondData.newBuilder()
|
|
||||||
// .setCondValue(1)
|
|
||||||
.build())
|
|
||||||
.addCondDataList(
|
|
||||||
TowerLevelStarCondData.newBuilder()
|
|
||||||
// .setCondValue(2)
|
|
||||||
.build())
|
|
||||||
.addCondDataList(
|
|
||||||
TowerLevelStarCondData.newBuilder()
|
|
||||||
// .setCondValue(3)
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
this.setData(proto);
|
if (1 <= lostStar && lostStar <= 3) {
|
||||||
|
proto.addCondDataList(
|
||||||
|
TowerLevelStarCondData.newBuilder()
|
||||||
|
// If these are still obfuscated in the next client version,
|
||||||
|
// just set all int fields to the star (1 <= star <= 3)
|
||||||
|
// that failed and set all boolean fields to true.
|
||||||
|
.setNGHNFHCLFBH(lostStar)
|
||||||
|
.setIBGHBFANCBK(true)
|
||||||
|
.setOILLLBMMABH(true)
|
||||||
|
.setOMOECEGOALC(lostStar)
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
proto
|
||||||
|
.addCondDataList(TowerLevelStarCondData.newBuilder().build())
|
||||||
|
.addCondDataList(TowerLevelStarCondData.newBuilder().build())
|
||||||
|
.addCondDataList(TowerLevelStarCondData.newBuilder().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData(proto.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.UnfreezeGroupLimitNotifyOuterClass.UnfreezeGroupLimitNotify;
|
||||||
|
|
||||||
|
public class PacketUnfreezeGroupLimitNotify extends BasePacket {
|
||||||
|
public PacketUnfreezeGroupLimitNotify(int pointId, int sceneId) {
|
||||||
|
super(PacketOpcodes.UnfreezeGroupLimitNotify);
|
||||||
|
this.setData(UnfreezeGroupLimitNotify.newBuilder().setPointId(pointId).setSceneId(sceneId));
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ if exist "%CUR_PATH%%CONFIG%.cmd" (
|
|||||||
|
|
||||||
if not "%JAVA_PATH%" == "DO_NOT_CHECK_PATH" (
|
if not "%JAVA_PATH%" == "DO_NOT_CHECK_PATH" (
|
||||||
if "%JAVA_PATH%" == "\bin\" (
|
if "%JAVA_PATH%" == "\bin\" (
|
||||||
call :LOG [ERROR] JAVA_HOME not found, please setup your windows enviroment for installed java.
|
call :LOG [ERROR] JAVA_HOME not found, please setup your windows environment for installed java.
|
||||||
goto :EXIT
|
goto :EXIT
|
||||||
)
|
)
|
||||||
if not exist "%JAVA_PATH%java.exe" (
|
if not exist "%JAVA_PATH%java.exe" (
|
||||||
|
Reference in New Issue
Block a user