9 Commits

144 changed files with 2893 additions and 4509 deletions

View File

@ -1,51 +0,0 @@
name: Build Docker Container
on:
push:
release:
types: [published]
workflow_dispatch: ~
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Generate Docker Meta
uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker image
uses: docker/build-push-action@v5.2.0
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

1
.gitignore vendored
View File

@ -64,7 +64,6 @@ tmp/
/*.jar /*.jar
/*.sh /*.sh
!entrypoint.sh
GM Handbook*.txt GM Handbook*.txt
handbook.html handbook.html

View File

@ -1,38 +0,0 @@
# Builder
FROM gradle:jdk17-alpine as builder
RUN apk add --update nodejs npm
WORKDIR /app
COPY ./ /app/
RUN gradle jar --no-daemon
# Fetch Data
FROM bitnami/git:2.43.0-debian-11-r1 as data
ARG DATA_REPOSITORY=https://gitlab.com/YuukiPS/GC-Resources.git
ARG DATA_BRANCH=4.0
WORKDIR /app
RUN git clone --branch ${DATA_BRANCH} --depth 1 ${DATA_REPOSITORY}
# Result Container
FROM amazoncorretto:17-alpine
WORKDIR /app
# Copy built assets
COPY --from=builder /app/grasscutter-*.jar /app/grasscutter.jar
COPY --from=builder /app/keystore.p12 /app/keystore.p12
# Copy the resources
COPY --from=data /app/GC-Resources/Resources /app/resources/
# Copy startup files
COPY ./entrypoint.sh /app/
CMD [ "sh", "/app/entrypoint.sh" ]
EXPOSE 80 443 8888 22102

View File

@ -18,27 +18,18 @@
* Spawning monsters via console * Spawning monsters via console
* Inventory features (receiving items/characters, upgrading items/characters, etc) * Inventory features (receiving items/characters, upgrading items/characters, etc)
## Foreward
### **Grasscutter beyond the latest release will have no handholding in terms of instructions.**
Grasscutter has not been actively maintained and currently (as of January 12th, 2025) only works up to version REL4.0.1 (introduction to Fontaine). If you have a beta version/unofficial version of Grasscutter, this guide should theoretically still work, however, we will not provide official support these versions. You can still try your luck in the Discord if you are stuck, but please don't act entitled.
## Quick setup guide ## Quick setup guide
**Note**: For support please join our [Discord](https://discord.gg/T5vZU6UyeG). **Note**: For support please join our [Discord](https://discord.gg/T5vZU6UyeG).
### Quick Start (automatic) ### Quick Start (automatic)
- Get [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) - Get Java 17: https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
- Get [MongoDB Community Server](https://www.mongodb.com/try/download/community) - Get [MongoDB Community Server](https://www.mongodb.com/try/download/community)
- Get game version REL4.0.x (If you don't have a 4.0.x client, you can find it here and open any of the links to download it): - 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
[4.0.x Client-github](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md)
[4.0.x Client-cloud drive](https://www.123pan.com/s/HoqUVv-U7SBA.html)
- ***UPDATE JAN 12, 2025: YOU CANNOT MIX AND MATCH GAME VERSIONS AND SERVER VERSIONS, PLEASE DOWNLOAD THE CORRECT VERSION OF GRASSCUTTER FOR YOUR VERSION OF THE GAME.***
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer. - Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
- After opening Cultivation (as admin), press the download button in the upper right corner. - After opening Culivation (as admin), press the download button in the upper right corner.
- Click `Download All-in-One` - Click `Download All-in-One`
- Click the gear in the upper right corner - Click the gear in the upper right corner
- Set the game Install path to where your game is located. - Set the game Install path to where your game is located.
@ -47,7 +38,7 @@ Grasscutter has not been actively maintained and currently (as of January 12th,
- Click the small button next to launch. - Click the small button next to launch.
- Click the launch button. - Click the launch button.
- Log in with whatever username you want. Password can be anything. - Log in with whatever username you want. Password doesn't matter.
### Building ### Building

View File

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

View File

@ -5,7 +5,7 @@
[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) | [简中](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)
**Aantekening:** We verwelkomen altijd bijdragers aan het project. Lees onze [Gedragscode](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) zorgvuldig door voordat u uw bijdrage toevoegt. **Aantekening:** We verwelkomen altijd bijdragers aan het project. Lees onze [Gedragscode](https://github.com/Grasscutters/Grasscutter/blob/development/README_NL.md#bijdragen-aan-het-project) zorgvuldig door voordat u uw bijdrage toevoegt.
## Huidige functies ## Huidige functies

View File

@ -3,7 +3,7 @@
<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](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)
**ध्यान:** हम हमेशा परियोजना में योगदानकर्ताओं का स्वागत करते हैं।. अपना योगदान जोड़ने से पहले कृपया हमारा ध्यानपूर्वक पढ़ें [आचार संहिता](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md). **ध्यान:** हम हमेशा परियोजना में योगदानकर्ताओं का स्वागत करते हैं।. अपना योगदान जोड़ने से पहले कृपया हमारा ध्यानपूर्वक पढ़ें [आचार संहिता](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).

View File

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

View File

@ -22,25 +22,52 @@
**각주 :** 도움이 필요할 경우 [Discord](https://discord.gg/T5vZU6UyeG)에 가입하세요. **각주 :** 도움이 필요할 경우 [Discord](https://discord.gg/T5vZU6UyeG)에 가입하세요.
### 빠른 설치 (자동) ### 설치에 필요한 것들
- [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 설치 * Java SE - 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 클라이언트를 가지고 있지 않다면, 여기서 찾을 수 있습니다.):
[4.0.x 클라이언트 - GitHub](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md)
[4.0.x 클라이언트 - 구글 드라이브브](https://www.123pan.com/s/HoqUVv-U7SBA.html)
- [최신 Cultivation](https://github.com/Grasscutters/Cultivation/releases/latest) 다운로드하세요. `.msi` 설치파일을 사용하면 됩니다. **각주 :** **실행**만을 원한다면, **jre**만 있어도 괜찮습니다.
- (관리자 권한으로) Cultivation을 실행한 후, 우측 상단에 위치한 다운로드 버튼을 클릭하세요.
- `올인원 다운로드`를 클릭하세요.
- 우측 상단에 위치한 톱니바퀴 버튼을 누르세요.
- 게임 설치 경로를 게임이 위치한 경로로 설정하세요.
- 사용자 지정 Java 경로 설정을 `C:\Program Files\Java\jdk-17\bin\java.exe`로 설정하세요.
- 다른 모든 설정은 기본값으로 두세요.
- 게임 시작 버튼 옆에 위치한 작은 버튼을 누르세요. * [MongoDB](https://www.mongodb.com/try/download/community) (4.0 이상의 버전 추천)
- 게임 시작 버튼을 누르세요.
- 원하는 사용자 이름으로 로그인하세요. 비밀번호는 무엇이든 가능합니다. * 프록시 데몬 : mitmproxy (mitmdump 추천), Fiddler Classic 등.
### 실행
**각주 :** 구버전에서 업데이트 했을 경우, `config.json` 파일을 재생성하기 위해 파일을 삭제하세요.
1. `grasscutter.jar` 얻기
- [Actions](https://github.com/Grasscutters/Grasscutter/suites/6895963598/artifacts/267483297) 탭에서 다운로드
- [직접 빌드하기](#빌드하기)
2. grasscutter.jar 파일이 위치한 폴더에 `resources` 폴더를 생성하고, `BinOutput``ExcelBinOutput` 폴더를 생성한 폴더 내로 옮기세요. *(이 파일들을 얻는 더 자세한 방법에 대해서는 [위키](https://github.com/Grasscutters/Grasscutter/wiki)를 참조하세요.)*
3. Grasscutter를 `java -jar grasscutter.jar` 명령어로 실행합니다. **MongoDB 서비스가 정상적으로 실행되고 있는지 확인하세요.**
### 클라이언트와의 연결
½. [서버 콘솔 명령어](https://github.com/Grasscutters/Grasscutter/wiki/Commands#targeting)를 이용해서 계정을 생성합니다.
1. 리다이렉트 트래픽 : (1가지 선택)
- mitmdump: `mitmdump -s proxy.py -k`
신뢰하는 인증 기관 인증서 (CA Cert) :
**각주 :** CA 인증서는 보통 `%USERPROFILE%\ .mitmproxy` 경로에 저장되며, `http://mitm.it`에서 다운로드 받을 수도 있습니다.
더블 클릭하여 [설치](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을 실행한 후, Setting에서 `Decrypt https traffic` 옵션을 켜고, Tools -> Options -> Connections에 있는 기본 포트를 `8888`을 제외한 다른 포트로 지정합니다. 그리고 [이 스크립트](https://github.lunatic.moe/fiddlerscript)를 불러옵니다.
- [호스트 파일](https://github.com/Grasscutters/Grasscutter/wiki/Running#traffic-route-map)
2. 네트워크 프록시를 `127.0.0.1:8080` 로 설정하거나 지정한 프록시 포트로 설정합니다.
**또한 `start.cmd`를 실행함으로써, 서버와 프록시 데몬을 자동으로 실행되게 할 수 있습니다. 이를 이용하기 위해서는 JAVA_HOME 환경 변수를 등록해야 합니다.**
### 빌드하기 ### 빌드하기
@ -50,50 +77,39 @@ Grasscutter는 종속성 및 컴파일 처리를 위해 Gradle을 이용합니
- [Java SE 개발 키트 - 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) - [Java SE 개발 키트 - 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) (선택, 핸드북을 빌드하기 위해 필요함.)
##### 클론 ##### 윈도우 (온라인)
```shell ```shell
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git git clone https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter cd Grasscutter
.\gradlew.bat # 개발 환경 설정
.\gradlew jar # 컴파일
``` ```
##### 컴파일 ##### 윈도우 (로컬)
**각주**: 핸드북 생성은 일부 시스템에서 실패할 수도 있습니다. 핸드북 생성을 비활성화하려면, `gradlew jar`명령에 `-PskipHandbook=1`명령줄 스위치를 추가하세요.
윈도우:
```shell ```shell
.\gradlew.bat # 환경 준비 cd <로컬 주소>/Grasscutter
.\gradlew jar .\gradlew.bat # 개발 환경 설정
.\gradlew jar # 컴파일
``` ```
리눅스 (GNU): ##### 리눅스
```bash ```bash
git clone https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
chmod +x gradlew chmod +x gradlew
./gradlew jar ./gradlew jar # 컴파일
```
##### 핸드북 컴파일 (수동동)
Gradle 사용:
```shell
./gradlew generateHandbook
```
NPM 사용:
```shell
cd src/handbook
npm install
npm run build
``` ```
프로젝트 폴더의 최상단에서 jar 파일을 찾을 수 있습니다. 프로젝트 폴더의 최상단에서 jar 파일을 찾을 수 있습니다.
### 문제 해결 ### 명령어들은 [위키](https://github.com/Grasscutters/Grasscutter/wiki/Commands)에서 확인할 수 있습니다.
흔한 문제들의 해결방법과 도움을 요청하려면, [우리의 디스코드 서버](https://discord.gg/T5vZU6UyeG)에 참가하고 support 채널에 가보세요.
# 빠른 문제 해결
* 만약 컴파일링이 정상적으로 완료되지 않을 경우, JDK 설치를 확인하세요. (JDK 버전 17 및 JDK의 bin 경로 변수 등록을 확인)
* 클라이언트가 연결되지 않거나, 로그인이 안 되거나, 4206 오류가 뜨는 등의 경우 - 대부분 프록시 데몬의 설치에 문제가 있을 것입니다. Fiddler를 사용하고 있다면, 8888을 제외한 다른 포트에서 구동되고 있는지 확인하세요.
* 구동 순서 : MongoDB > Grasscutter > 프록시 데몬 (mitmdump, fiddler 등) > 게임

View File

@ -26,12 +26,10 @@
- 获取Java 17https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html - 获取Java 17https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
- 获取[MongoDB社区版](https://www.mongodb.com/try/download/community) - 获取[MongoDB社区版](https://www.mongodb.com/try/download/community)
- 获取游戏4.0正式版 (如果你没有4.0的客户端,可以在这里找到): - 获取游戏4.0正式版 (如果你没有4.0的客户端,可以在这里找到):https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md)
[123pan share](https://www.123pan.com/s/HoqUVv-U7SBA.html)
[github](https://github.com/JRSKelvin/GenshinRepository/blob/main/Version%204.0.0.md)
- 下载[最新的Cultivation版本](https://github.com/Grasscutters/Cultivation/releases/latest)(使用以“.msi”为后缀的安装包 - 下载[最新的Cultivation版本](https://github.com/Grasscutters/Cultivation/releases/latest)(使用以“.msi”为后缀的安装包
- 以管理员身份打开Cultivation按右上角的下载按钮。 - 以管理员身份打开Culivation按右上角的下载按钮。
- 点击“下载 Grasscutter 一体化” - 点击“下载 Grasscutter 一体化”
- 点击右上角的齿轮 - 点击右上角的齿轮
- 将游戏安装路径设置为你游戏所在的位置。 - 将游戏安装路径设置为你游戏所在的位置。

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,11 +17,9 @@ public final class StopCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, Player targetPlayer, List<String> args) { public void execute(Player sender, Player targetPlayer, List<String> args) {
CommandHandler.sendMessage(null, translate("commands.stop.success")); CommandHandler.sendMessage(null, translate("commands.stop.success"));
if (Grasscutter.getGameServer() != null) {
for (Player p : Grasscutter.getGameServer().getPlayers().values()) { for (Player p : Grasscutter.getGameServer().getPlayers().values()) {
CommandHandler.sendMessage(p, translate(p, "commands.stop.success")); CommandHandler.sendMessage(p, translate(p, "commands.stop.success"));
} }
}
System.exit(1000); System.exit(1000);
} }

View File

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

View File

@ -140,7 +140,6 @@ public class ConfigContainer {
public boolean autoCreate = false; public boolean autoCreate = false;
public boolean EXPERIMENTAL_RealPassword = false; public boolean EXPERIMENTAL_RealPassword = false;
public String[] defaultPermissions = {}; public String[] defaultPermissions = {};
public String playerEmail = "grasscutter.io";
public int maxPlayer = -1; public int maxPlayer = -1;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ public class Account {
return email; return email;
} else { } else {
// As of game version 3.5+, only the email is displayed to a user. // As of game version 3.5+, only the email is displayed to a user.
return this.getUsername() + "@" + ACCOUNT.playerEmail; return this.getUsername() + "@grasscutter.io";
} }
} }
@ -235,7 +235,7 @@ public class Account {
this.addPermission("*"); this.addPermission("*");
} }
// Set account default language to server default language // Set account default language as server default language
if (!document.containsKey("locale")) { if (!document.containsKey("locale")) {
this.locale = LANGUAGE; this.locale = LANGUAGE;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,6 @@ public final class DungeonManager {
private boolean ended = false; private boolean ended = false;
private int newestWayPoint = 0; private int newestWayPoint = 0;
@Getter private int startSceneTime = 0; @Getter private int startSceneTime = 0;
@Setter @Getter private boolean towerDungeon = false;
DungeonTrialTeam trialTeam = null; DungeonTrialTeam trialTeam = null;
@ -68,10 +67,6 @@ public final class DungeonManager {
} }
if (isFinishedSuccessfully()) { if (isFinishedSuccessfully()) {
// Set ended now because calling EVENT_DUNGEON_SETTLE
// during finishDungeon() may cause reentrance into
// this function, leading to double settles.
ended = true;
finishDungeon(); finishDungeon();
} }
} }
@ -82,14 +77,9 @@ 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);
@ -333,31 +323,15 @@ public final class DungeonManager {
p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON); p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON);
} }
}); });
var future =
scene scene
.getScriptManager() .getScriptManager()
.callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0)); .callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0));
// Note: There is a possible race condition with calling
// EVENT_DUNGEON_SETTLE here asynchronously:
// 1. EVENT_DUNGEON_SETTLE triggers some Lua-side logic,
// which may happen after 2 (below) finishes.
// 2. Some DungeonSettleListener could be comparing some
// Lua variable before its setting in 1 (above) finishes.
// For safety, ensure all events have finished before returning.
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
} }
public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) { public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) {
if (scene.getDungeonSettleListeners() != null) { if (scene.getDungeonSettleListeners() != null) {
scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason)); scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason));
} }
if (isTowerDungeon()) {
scene.getPlayers().get(0).getTowerManager().onEnd();
}
ended = true; ended = true;
} }

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
@ -23,13 +22,12 @@ 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;
private boolean progress; private boolean progress;
private boolean success; private boolean success;
private int startedAt; private long startedAt;
private int finishedTime; private int finishedTime;
/** /**
@ -60,7 +58,6 @@ public class WorldChallenge {
this.challengeTriggers = challengeTriggers; this.challengeTriggers = challengeTriggers;
this.goal = goal; this.goal = goal;
this.score = new AtomicInteger(0); this.score = new AtomicInteger(0);
this.guardEntity = null;
} }
public boolean inProgress() { public boolean inProgress() {
@ -83,16 +80,9 @@ public class WorldChallenge {
return; return;
} }
this.progress = true; this.progress = true;
this.startedAt = getScene().getSceneTimeSeconds(); this.startedAt = System.currentTimeMillis();
getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this));
challengeTriggers.forEach(t -> t.onBegin(this)); challengeTriggers.forEach(t -> t.onBegin(this));
var player = scene.getPlayers().get(0);
var dungeonManager = scene.getDungeonManager();
var towerManager = player.getTowerManager();
if (dungeonManager != null && dungeonManager.isTowerDungeon() && towerManager != null) {
towerManager.onBegin();
}
} }
public void done() { public void done() {
@ -146,10 +136,6 @@ public class WorldChallenge {
this.progress = false; this.progress = false;
this.success = success; this.success = success;
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt)); this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt));
// Despawn all leftover mobs in this challenge's SceneGroup
getScene().getScriptManager().removeMonstersInGroup(group);
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
} }
@ -157,20 +143,6 @@ public class WorldChallenge {
return score.incrementAndGet(); return score.incrementAndGet();
} }
public int getGuardEntityHpPercent() {
if (guardEntity == null) {
Grasscutter.getLogger()
.warn(
"getGuardEntityHpPercent: Could not find guardEntity for this challenge = {}", this);
return 100;
}
var curHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
var maxHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
int percent = (int) (curHp * 100 / maxHp);
return percent;
}
public void onMonsterDeath(EntityMonster monster) { public void onMonsterDeath(EntityMonster monster) {
if (!inProgress()) { if (!inProgress()) {
return; return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.entity; package emu.grasscutter.game.entity;
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.config.ConfigEntityMonster; import emu.grasscutter.data.binout.config.ConfigEntityMonster;
import emu.grasscutter.data.common.PropGrowCurve; import emu.grasscutter.data.common.PropGrowCurve;
@ -25,75 +23,56 @@ import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo; import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo;
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
import emu.grasscutter.net.proto.ServantInfoOuterClass.ServantInfo;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.*; import emu.grasscutter.scripts.data.*;
import emu.grasscutter.server.event.entity.EntityDamageEvent; import emu.grasscutter.server.event.entity.EntityDamageEvent;
import emu.grasscutter.utils.helpers.ProtoHelper; import emu.grasscutter.utils.helpers.ProtoHelper;
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
import java.util.*;
import javax.annotation.Nullable;
import lombok.*; import lombok.*;
import javax.annotation.Nullable;
import java.util.*;
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
public class EntityMonster extends GameEntity { public class EntityMonster extends GameEntity {
@Getter(onMethod_ = @Override) @Getter(onMethod_ = @Override)
private final Int2FloatOpenHashMap fightProperties; private final Int2FloatOpenHashMap fightProperties;
@Getter(onMethod_ = @Override) @Getter(onMethod_ = @Override)
private final Position position; private final Position position;
@Getter(onMethod_ = @Override) @Getter(onMethod_ = @Override)
private final Position rotation; private final Position rotation;
@Getter private final MonsterData monsterData; @Getter private final MonsterData monsterData;
@Getter private final ConfigEntityMonster configEntityMonster; @Getter private final ConfigEntityMonster configEntityMonster;
@Getter private final Position bornPos; @Getter private final Position bornPos;
@Getter private final int level; @Getter private final int level;
@Getter private EntityWeapon weaponEntity; @Getter private EntityWeapon weaponEntity;
@Getter private Map<Integer, EntityMonster> summonTagMap;
@Getter @Setter private int summonedTag;
@Getter @Setter private int ownerEntityId;
@Getter @Setter private int poseId; @Getter @Setter private int poseId;
@Getter @Setter private int aiId = -1; @Getter @Setter private int aiId = -1;
@Getter private List<Player> playerOnBattle; @Getter private List<Player> playerOnBattle;
@Nullable @Getter @Setter private SceneMonster metaMonster; @Nullable @Getter @Setter private SceneMonster metaMonster;
public EntityMonster( public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) {
Scene scene, MonsterData monsterData, Position pos, Position rot, int level) {
super(scene); super(scene);
this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER); this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER);
this.monsterData = monsterData; this.monsterData = monsterData;
this.fightProperties = new Int2FloatOpenHashMap(); this.fightProperties = new Int2FloatOpenHashMap();
this.position = new Position(pos); this.position = new Position(pos);
this.rotation = new Position(rot); this.rotation = new Position();
this.bornPos = this.getPosition().clone(); this.bornPos = this.getPosition().clone();
this.level = level; this.level = level;
this.playerOnBattle = new ArrayList<>(); this.playerOnBattle = new ArrayList<>();
this.summonTagMap = new HashMap<>();
this.summonedTag = 0;
this.ownerEntityId = 0;
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) { if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
this.configEntityMonster = this.configEntityMonster = GameData.getMonsterConfigData().get(
GameData.getMonsterConfigData() GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
.get(GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
} else { } else {
this.configEntityMonster = null; this.configEntityMonster = null;
} }
if (this.configEntityMonster != null
&& this.configEntityMonster.getCombat() != null
&& this.configEntityMonster.getCombat().getSummon() != null
&& this.configEntityMonster.getCombat().getSummon().getSummonTags() != null) {
this.configEntityMonster
.getCombat()
.getSummon()
.getSummonTags()
.forEach(t -> this.summonTagMap.put(t.getSummonTag(), null));
}
// Monster weapon // Monster weapon
if (getMonsterWeaponId() > 0) { if (getMonsterWeaponId() > 0) {
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId()); this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
@ -108,15 +87,18 @@ public class EntityMonster extends GameEntity {
private void addConfigAbility(String name) { private void addConfigAbility(String name) {
var data = GameData.getAbilityData(name); var data = GameData.getAbilityData(name);
if (data != null) { if (data != null) {
this.getWorld().getHost().getAbilityManager().addAbilityToEntity(this, data); this.getWorld().getHost()
.getAbilityManager()
.addAbilityToEntity(this, data);
} }
} }
@Override @Override
public void initAbilities() { public void initAbilities() {
// Affix abilities // Affix abilities
var optionalGroup = var optionalGroup = this.getScene().getLoadedGroups().stream()
this.getScene().getLoadedGroups().stream().filter(g -> g.id == this.getGroupId()).findAny(); .filter(g -> g.id == this.getGroupId())
.findAny();
List<Integer> affixes = null; List<Integer> affixes = null;
if (optionalGroup.isPresent()) { if (optionalGroup.isPresent()) {
var group = optionalGroup.get(); var group = optionalGroup.get();
@ -144,12 +126,14 @@ public class EntityMonster extends GameEntity {
} }
// TODO: Research if any monster is non humanoid // TODO: Research if any monster is non humanoid
for (var ability : for(var ability : GameData.getConfigGlobalCombat()
GameData.getConfigGlobalCombat().getDefaultAbilities().getNonHumanoidMoveAbilities()) { .getDefaultAbilities()
.getNonHumanoidMoveAbilities()) {
this.addConfigAbility(ability); this.addConfigAbility(ability);
} }
if (this.configEntityMonster != null && this.configEntityMonster.getAbilities() != null) { if (this.configEntityMonster != null &&
this.configEntityMonster.getAbilities() != null) {
for (var configAbilityData : this.configEntityMonster.getAbilities()) { for (var configAbilityData : this.configEntityMonster.getAbilities()) {
this.addConfigAbility(configAbilityData.abilityName); this.addConfigAbility(configAbilityData.abilityName);
} }
@ -159,8 +143,9 @@ public class EntityMonster extends GameEntity {
var group = optionalGroup.get(); var group = optionalGroup.get();
var monster = group.monsters.get(getConfigId()); var monster = group.monsters.get(getConfigId());
if (monster != null && monster.isElite) { if (monster != null && monster.isElite) {
this.addConfigAbility( this.addConfigAbility(GameData.getConfigGlobalCombat()
GameData.getConfigGlobalCombat().getDefaultAbilities().getMonterEliteAbilityName()); .getDefaultAbilities()
.getMonterEliteAbilityName());
} }
} }
@ -209,8 +194,7 @@ public class EntityMonster extends GameEntity {
@Override @Override
public void onInteract(Player player, GadgetInteractReq interactReq) { public void onInteract(Player player, GadgetInteractReq interactReq) {
EnvAnimalGatherConfigData gatherData = EnvAnimalGatherConfigData gatherData = GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
if (gatherData == null) { if (gatherData == null) {
return; return;
@ -222,15 +206,9 @@ public class EntityMonster extends GameEntity {
} }
@Override @Override
public void onTick(int sceneTime) { public void onCreate() {
super.onTick(sceneTime);
// Lua event // Lua event
getScene() getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
.getScriptManager()
.callEvent(
new ScriptArgs(
this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
} }
@Override @Override
@ -253,14 +231,7 @@ public class EntityMonster extends GameEntity {
@Override @Override
public void runLuaCallbacks(EntityDamageEvent event) { public void runLuaCallbacks(EntityDamageEvent event) {
super.runLuaCallbacks(event); super.runLuaCallbacks(event);
getScene() getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EVENT_SPECIFIC_MONSTER_HP_CHANGE, getConfigId(), monsterData.getId())
.getScriptManager()
.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()));
@ -279,68 +250,29 @@ public class EntityMonster extends GameEntity {
challenge.ifPresent(c -> c.onMonsterDeath(this)); challenge.ifPresent(c -> c.onMonsterDeath(this));
if (scriptManager.isInit() && this.getGroupId() > 0) { if (scriptManager.isInit() && this.getGroupId() > 0) {
Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()) Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()).ifPresent(s -> s.onMonsterDead(this));
.ifPresent(s -> s.onMonsterDead(this));
// Ensure each EVENT_ANY_MONSTER_DIE runs to completion. // prevent spawn monster after success
// Multiple such events firing at the same time may cause /*if (challenge.map(c -> c.inProgress()).orElse(true)) {
// the same lua trigger to fire multiple times, when it scriptManager.callEvent(new ScriptArgs(EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()).setGroupId(this.getGroupId()));
// should happen only once. } else if (getScene().getChallenge() == null) {
var future = }*/
scriptManager.callEvent( scriptManager.callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
new ScriptArgs(
this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
} }
// Battle Pass trigger // Battle Pass trigger
scene scene.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
.getPlayers()
.forEach(
p ->
p.getBattlePassManager()
.triggerMission(
WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
scene scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
.getPlayers() scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
.forEach( scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
p ->
p.getQuestManager()
.queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
scene
.getPlayers()
.forEach(
p ->
p.getQuestManager()
.queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
scene
.getPlayers()
.forEach(
p ->
p.getQuestManager()
.queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
SceneGroupInstance groupInstance = SceneGroupInstance groupInstance = scene.getScriptManager().getGroupInstanceById(this.getGroupId());
scene.getScriptManager().getGroupInstanceById(this.getGroupId());
if(groupInstance != null && metaMonster != null) if(groupInstance != null && metaMonster != null)
groupInstance.getDeadEntities().add(metaMonster.config_id); groupInstance.getDeadEntities().add(metaMonster.config_id);
scene.triggerDungeonEvent( scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId());
DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId()); scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER, this.getMonsterData().getType().getValue());
scene.triggerDungeonEvent( scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER,
this.getMonsterData().getType().getValue());
scene.triggerDungeonEvent(
DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
// If this entity spawned servants, kill those too.
summonTagMap.values().stream()
.filter(Objects::nonNull)
.forEach(entity -> scene.killEntity(entity, killerId));
} }
public void recalcStats() { public void recalcStats() {
@ -348,86 +280,45 @@ public class EntityMonster extends GameEntity {
MonsterData data = this.getMonsterData(); MonsterData data = this.getMonsterData();
// Get hp percent, set to 100% if none // Get hp percent, set to 100% if none
float hpPercent = float hpPercent = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0
? 1f
: this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)
/ this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
// Clear properties // Clear properties
this.getFightProperties().clear(); this.getFightProperties().clear();
// Base stats // Base stats
MonsterData.definedFightProperties.forEach( MonsterData.definedFightProperties.forEach(prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
// Level curve // Level curve
MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel()); MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel());
if (curve != null) { if (curve != null) {
for (PropGrowCurve growCurve : data.getPropGrowCurves()) { for (PropGrowCurve growCurve : data.getPropGrowCurves()) {
FightProperty prop = FightProperty.getPropByName(growCurve.getType()); FightProperty prop = FightProperty.getPropByName(growCurve.getType());
this.setFightProperty( this.setFightProperty(prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
} }
} }
// Set % stats // Set % stats
FightProperty.forEachCompoundProperty( FightProperty.forEachCompoundProperty(c -> this.setFightProperty(c.getResult(),
c -> this.getFightProperty(c.getFlat()) + (this.getFightProperty(c.getBase()) * (1f + this.getFightProperty(c.getPercent())))));
this.setFightProperty(
c.getResult(),
this.getFightProperty(c.getFlat())
+ (this.getFightProperty(c.getBase())
* (1f + this.getFightProperty(c.getPercent())))));
// If in tower, scale max hp by
// +50%: Floors 3 7
// +100%: Floors 8 11
// +150%: Floor 12
var dungeonManager = getScene().getDungeonManager();
var towerManager = getScene().getPlayers().get(0).getTowerManager();
if (dungeonManager != null && dungeonManager.isTowerDungeon() && towerManager != null) {
var floor = towerManager.getCurrentFloorNumber();
float additionalScaleFactor = 0f;
if (floor >= 12) {
additionalScaleFactor = 1.5f;
} else if (floor >= 8) {
additionalScaleFactor = 1.f;
} else if (floor >= 3) {
additionalScaleFactor = .5f;
}
this.setFightProperty(
FightProperty.FIGHT_PROP_MAX_HP,
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * (1 + additionalScaleFactor));
}
// Set current hp // Set current hp
this.setFightProperty( this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
FightProperty.FIGHT_PROP_CUR_HP,
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
} }
@Override @Override
public SceneEntityInfo toProto() { public SceneEntityInfo toProto() {
var data = this.getMonsterData(); var data = this.getMonsterData();
var aiInfo = var authority = EntityAuthorityInfo.newBuilder()
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(this.getBornPos().toProto());
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(aiInfo) .setAiInfo(SceneEntityAiInfo.newBuilder()
.setIsAiOpen(true)
.setBornPos(this.getBornPos().toProto()))
.setBornPos(this.getBornPos().toProto()) .setBornPos(this.getBornPos().toProto())
.build(); .build();
var entityInfo = var entityInfo = SceneEntityInfo.newBuilder()
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())
@ -438,14 +329,12 @@ public class EntityMonster extends GameEntity {
this.addAllFightPropsToEntityInfo(entityInfo); this.addAllFightPropsToEntityInfo(entityInfo);
entityInfo.addPropList( entityInfo.addPropList(PropPair.newBuilder()
PropPair.newBuilder()
.setType(PlayerProperty.PROP_LEVEL.getId()) .setType(PlayerProperty.PROP_LEVEL.getId())
.setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())) .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel()))
.build()); .build());
var monsterInfo = var monsterInfo = SceneMonsterInfo.newBuilder()
SceneMonsterInfo.newBuilder()
.setMonsterId(getMonsterId()) .setMonsterId(getMonsterId())
.setGroupId(this.getGroupId()) .setGroupId(this.getGroupId())
.setConfigId(this.getConfigId()) .setConfigId(this.getConfigId())
@ -453,26 +342,20 @@ 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 monsterInfo.setTitleId(this.metaMonster.title_id)
.setTitleId(this.metaMonster.title_id)
.setSpecialNameId(this.metaMonster.special_name_id); .setSpecialNameId(this.metaMonster.special_name_id);
} else if (data.getDescribeData() != null) { } else if (data.getDescribeData() != null) {
monsterInfo monsterInfo.setTitleId(data.getDescribeData().getTitleId())
.setTitleId(data.getDescribeData().getTitleId())
.setSpecialNameId(data.getSpecialNameId()); .setSpecialNameId(data.getSpecialNameId());
} }
} }
if (this.getMonsterWeaponId() > 0) { if (this.getMonsterWeaponId() > 0) {
SceneWeaponInfo weaponInfo = SceneWeaponInfo weaponInfo = SceneWeaponInfo.newBuilder()
SceneWeaponInfo.newBuilder()
.setEntityId(this.getWeaponEntity() != null ? this.getWeaponEntity().getId() : 0) .setEntityId(this.getWeaponEntity() != null ? this.getWeaponEntity().getId() : 0)
.setGadgetId(this.getMonsterWeaponId()) .setGadgetId(this.getMonsterWeaponId())
.setAbilityInfo(AbilitySyncStateInfo.newBuilder()) .setAbilityInfo(AbilitySyncStateInfo.newBuilder())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -259,14 +259,8 @@ public class EnergyManager extends BasePlayerManager {
return; return;
} }
// Also reference AvatarSkillData in case the burst gets a different skill ID
// when the avatar is in a different state. For example, Wanderer's burst is
// 10755 usually but when he floats, it becomes 10753.
var skillData = GameData.getAvatarSkillDataMap().get(skillId);
// If the cast skill was a burst, consume energy. // If the cast skill was a burst, consume energy.
if ((avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) {
|| (skillData != null && skillData.getCostElemVal() > 0)) {
avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START); avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
} }
} }
@ -400,11 +394,10 @@ public class EnergyManager extends BasePlayerManager {
public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) { public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) {
for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) { for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) {
// giving the exact amount read off the AvatarSkillData.json // giving the exact amount read off the AvatarSkillData.json
var skillDepot = entityAvatar.getAvatar().getSkillDepot();
if (skillDepot != null) {
entityAvatar.addEnergy( entityAvatar.addEnergy(
skillDepot.getEnergySkillData().getCostElemVal(), changeReason, isFlat); entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(),
} changeReason,
isFlat);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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