mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-13 10:13:51 +00:00
Compare commits
31 Commits
v1.7.3
...
93df2d0b0e
Author | SHA1 | Date | |
---|---|---|---|
93df2d0b0e | |||
e7ed66477f | |||
af70de316e | |||
f29189be8f | |||
4ced11d567 | |||
446e994ff0 | |||
655016c92e | |||
d0e3720748 | |||
db4542653a | |||
76fd5b2e9c | |||
4022267888 | |||
f1f5b54939 | |||
f871f261e1 | |||
eeaccf32c4 | |||
6e1913aacb | |||
9e17e4aacb | |||
770a793c69 | |||
c4402cc287 | |||
5ebad71e9d | |||
564b609028 | |||
cdb0dc560a | |||
d8c3da8fcd | |||
13c40b53a7 | |||
f1c1a84683 | |||
2bcbd41026 | |||
adf8031684 | |||
0bbeaf254b | |||
1fac319eb2 | |||
d224178a64 | |||
d461ee2eb3 | |||
24874e7fba |
51
.github/workflows/build_container.yml
vendored
Normal file
51
.github/workflows/build_container.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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
1
.gitignore
vendored
@ -64,6 +64,7 @@ tmp/
|
|||||||
|
|
||||||
/*.jar
|
/*.jar
|
||||||
/*.sh
|
/*.sh
|
||||||
|
!entrypoint.sh
|
||||||
|
|
||||||
GM Handbook*.txt
|
GM Handbook*.txt
|
||||||
handbook.html
|
handbook.html
|
||||||
|
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# 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
|
10
README.md
10
README.md
@ -24,12 +24,14 @@
|
|||||||
|
|
||||||
### 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 (4.0.x client can be found here if you don't have it): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
|
- Get game version REL4.0.x (If you don't have a 4.0.x client, you can find it here and open any of the links to download it):
|
||||||
|
[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)
|
||||||
|
|
||||||
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
|
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
|
||||||
- After opening Culivation (as admin), press the download button in the upper right corner.
|
- After opening Cultivation (as admin), press the download button in the upper right corner.
|
||||||
- Click `Download All-in-One`
|
- Click `Download All-in-One`
|
||||||
- Click the gear in the upper right corner
|
- Click the gear in the upper right corner
|
||||||
- Set the game Install path to where your game is located.
|
- Set the game Install path to where your game is located.
|
||||||
@ -38,7 +40,7 @@
|
|||||||
|
|
||||||
- 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 doesn't matter.
|
- Log in with whatever username you want. Password can be anything.
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
|
26
build.gradle
26
build.gradle
@ -54,11 +54,11 @@ spotless {
|
|||||||
compileJava.options.encoding = 'UTF-8'
|
compileJava.options.encoding = 'UTF-8'
|
||||||
compileTestJava.options.encoding = 'UTF-8'
|
compileTestJava.options.encoding = 'UTF-8'
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
|
||||||
group = 'io.grasscutter'
|
group = 'io.grasscutter'
|
||||||
version = '1.7.3'
|
version = '1.7.4'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
@ -77,19 +77,19 @@ dependencies {
|
|||||||
|
|
||||||
// Logging libraries.
|
// Logging libraries.
|
||||||
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7'
|
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7'
|
||||||
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.7'
|
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.14'
|
||||||
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.7'
|
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.12'
|
||||||
|
|
||||||
// Line reading libraries.
|
// Line reading libraries.
|
||||||
implementation group: 'org.jline', name: 'jline', version: '3.21.0'
|
implementation group: 'org.jline', name: 'jline', version: '3.25.0'
|
||||||
implementation group: 'org.jline', name: 'jline-terminal-jna', version: '3.21.0'
|
implementation group: 'org.jline', name: 'jline-terminal-jna', version: '3.21.0'
|
||||||
implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0'
|
implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0'
|
||||||
|
|
||||||
// Java Netty for networking.
|
// Java Netty for networking.
|
||||||
implementation group: 'io.netty', name: 'netty-common', version: '4.1.86.Final'
|
implementation group: 'io.netty', name: 'netty-common', version: project.netty_version
|
||||||
implementation group: 'io.netty', name: 'netty-handler', version: '4.1.86.Final'
|
implementation group: 'io.netty', name: 'netty-handler', version: project.netty_version
|
||||||
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.86.Final'
|
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: project.netty_version
|
||||||
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.86.Final'
|
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: project.netty_version
|
||||||
|
|
||||||
// Serialization.
|
// Serialization.
|
||||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'
|
implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'
|
||||||
@ -136,10 +136,10 @@ dependencies {
|
|||||||
testImplementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'
|
testImplementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'
|
||||||
|
|
||||||
// Lombok.
|
// Lombok.
|
||||||
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
compileOnly group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||||
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||||
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||||
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.26'
|
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: project.lombok_version
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
|
@ -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) | [简中](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)
|
[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)
|
||||||
|
|
||||||
**ध्यान:** हम हमेशा परियोजना में योगदानकर्ताओं का स्वागत करते हैं।. अपना योगदान जोड़ने से पहले कृपया हमारा ध्यानपूर्वक पढ़ें [आचार संहिता](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).
|
**ध्यान:** हम हमेशा परियोजना में योगदानकर्ताओं का स्वागत करते हैं।. अपना योगदान जोड़ने से पहले कृपया हमारा ध्यानपूर्वक पढ़ें [आचार संहिता](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).
|
||||||
|
|
||||||
@ -75,4 +75,4 @@ chmod +x gradlew
|
|||||||
|
|
||||||
### समस्या निवारण
|
### समस्या निवारण
|
||||||
|
|
||||||
सामान्य मुद्दों और समाधानों की सूची और सहायता मांगने के लिए कृपया शामिल हों [our Discord server](https://discord.gg/T5vZU6UyeG) और सपोर्ट चैनल पर जाएं.
|
सामान्य मुद्दों और समाधानों की सूची और सहायता मांगने के लिए कृपया शामिल हों [our Discord server](https://discord.gg/T5vZU6UyeG) और सपोर्ट चैनल पर जाएं.
|
||||||
|
@ -3,81 +3,64 @@
|
|||||||
|
|
||||||
<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) | [简中](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)
|
||||||
|
|
||||||
|
|
||||||
***:** 私たちはプロジェクトへの貢献者をいつでも歓迎します。貢献を追加する前に、我々の [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
|
**Attention:** 私たちはプロジェクトへのコントリビュータをいつでも歓迎します。コントリビュートする前に、私たちの [行動規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)をよくお読みください。
|
||||||
|
|
||||||
## 現在機能している物
|
## 現在実装されている機能
|
||||||
|
|
||||||
* ログイン
|
* ログイン
|
||||||
* 戦闘
|
* 戦闘
|
||||||
* フレンドリスト
|
* フレンドリスト
|
||||||
* テレポート
|
* テレポート
|
||||||
* 祈願(ガチャ)
|
* 祈願 (ガチャ)
|
||||||
* マルチプレイは一部機能しています
|
* マルチプレイ (一部)
|
||||||
* コンソールを使用してモンスターをスポーンさせる
|
* コンソールを通したモンスターのスポーン
|
||||||
* インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど)
|
* インベントリ機能 (アイテム/キャラクターの受け取り、アイテム/キャラクターのアップグレードなど)
|
||||||
|
|
||||||
## クイックセットアップガイド
|
## かんたんセットアップガイド
|
||||||
|
|
||||||
***:** サポートが必要な場合はGrasscutterの[Discord](https://discord.gg/T5vZU6UyeG)に参加してください。
|
**Note:** サポートが必要な場合はGrasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加してください。
|
||||||
|
|
||||||
### 動作環境
|
### パパっとスタートアップ
|
||||||
|
|
||||||
* [JAVAのバージョン17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
- [Java (バージョン17以降)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) を用意する
|
||||||
|
- [MongoDB Community Server](https://www.mongodb.com/try/download/community) を用意する
|
||||||
|
- ゲームバージョンがREL4.0.Xのクライアントを用意する (4.0.Xのクライアントを持っていない場合は右のリンクからダウンロード): [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 Kits - 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
- [Java SE Development Kit 17以降](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [NodeJS](https://nodejs.org/en/download) (任意、ハンドブックの生成に必要)
|
||||||
|
|
||||||
##### Windows
|
##### Clone
|
||||||
|
```shell
|
||||||
|
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
|
||||||
|
cd Grasscutter
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Compile
|
||||||
|
|
||||||
|
**Note:** 環境によってはハンドブックの生成が失敗する場合があります。ハンドブックの生成をさせない場合は `gradlew jar` コマンドに `-PskipHandbook=1` を付け加えてください。
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/Grasscutters/Grasscutter.git
|
git clone https://github.com/Grasscutters/Grasscutter.git
|
||||||
@ -86,7 +69,7 @@ cd Grasscutter
|
|||||||
.\gradlew jar # コンパイル
|
.\gradlew jar # コンパイル
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Linux
|
Linux:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Grasscutters/Grasscutter.git
|
git clone https://github.com/Grasscutters/Grasscutter.git
|
||||||
@ -95,13 +78,23 @@ chmod +x gradlew
|
|||||||
./gradlew jar # コンパイル
|
./gradlew jar # コンパイル
|
||||||
```
|
```
|
||||||
|
|
||||||
生成されたjarファイルはプロジェクトフォルダのルートに有ります。
|
##### 手動によるハンドブックの生成
|
||||||
|
|
||||||
### コマンドリストは[wiki](https://github.com/Grasscutters/Grasscutter/wiki/Commands)へ移動しました。
|
Gradleを使用する場合:
|
||||||
|
```shell
|
||||||
|
./gradlew generateHandbook
|
||||||
|
```
|
||||||
|
|
||||||
# トラブルシューティング
|
NPMを使用する場合:
|
||||||
|
```shell
|
||||||
|
cd src/handbook
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
* コンパイルが失敗した場合JDKがインストールされているか確認してください。(JDKのバージョンが17以降であることと、環境変数でJDKのパスが設定されている必要があります)
|
|
||||||
* クライアントが接続できない・ログインできない・エラーコード4206・またその他場合、ほとんどは、プロキシデーモンの設定が問題です。Fiddlerを使っている場合はデフォルトポートを8888以外の別のポートに変更してみてください。
|
生成されたjarファイルはプロジェクトのルートフォルダにあります。
|
||||||
Fiddlerを使用している場合はポートが8888以外に設定されていることを確認してください。
|
|
||||||
* 起動シーケンス(順番): MongoDB > Grasscutter > プロキシツール (mitmdumpかfiddler、その他) > ゲーム
|
### トラブルシューティング
|
||||||
|
|
||||||
|
よく散見されるトラブルとそれに対する解決策のまとめリストや、質問し誰かの助けを得たい場合は、Grasscutterの[Discordサーバー](https://discord.gg/T5vZU6UyeG)に参加し、サポートチャンネルを参照してください。
|
||||||
|
@ -26,10 +26,12 @@
|
|||||||
|
|
||||||
- 获取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社区版](https://www.mongodb.com/try/download/community)
|
- 获取[MongoDB社区版](https://www.mongodb.com/try/download/community)
|
||||||
- 获取游戏4.0正式版 (如果你没有4.0的客户端,可以在这里找到):https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md)
|
- 获取游戏4.0正式版 (如果你没有4.0的客户端,可以在这里找到):
|
||||||
|
[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”为后缀的安装包)。
|
||||||
- 以管理员身份打开Culivation,按右上角的下载按钮。
|
- 以管理员身份打开Cultivation,按右上角的下载按钮。
|
||||||
- 点击“下载 Grasscutter 一体化”
|
- 点击“下载 Grasscutter 一体化”
|
||||||
- 点击右上角的齿轮
|
- 点击右上角的齿轮
|
||||||
- 将游戏安装路径设置为你游戏所在的位置。
|
- 将游戏安装路径设置为你游戏所在的位置。
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
- 下載遊戲版本 REL3.7(如果你沒有的話,可以在[這裡](https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/3.7.0.md)找到 3.7 客戶端)
|
- 下載遊戲版本 REL3.7(如果你沒有的話,可以在[這裡](https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/3.7.0.md)找到 3.7 客戶端)
|
||||||
|
|
||||||
- 下載 [最新的 Cultivation 版本](https://github.com/Grasscutters/Cultivation/releases/latest)。使用 `.msi` 安裝程式。
|
- 下載 [最新的 Cultivation 版本](https://github.com/Grasscutters/Cultivation/releases/latest)。使用 `.msi` 安裝程式。
|
||||||
- 以管理員身分打開 Culivation,按右上角的下載按鈕。
|
- 以管理員身分打開 Cultivation,按右上角的下載按鈕。
|
||||||
- 點擊 `Download All-in-One`
|
- 點擊 `Download All-in-One`
|
||||||
- 點擊右上角的齒輪
|
- 點擊右上角的齒輪
|
||||||
- 將遊戲安裝路徑設置為你的遊戲所在的位置。
|
- 將遊戲安裝路徑設置為你的遊戲所在的位置。
|
||||||
|
3
entrypoint.sh
Normal file
3
entrypoint.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#/bin/sh
|
||||||
|
|
||||||
|
java -jar /app/grasscutter.jar
|
@ -1,2 +1,6 @@
|
|||||||
org.gradle.jvmargs=-Xmx4096m
|
org.gradle.jvmargs=-Xmx8G
|
||||||
|
|
||||||
|
netty_version=4.1.111.Final
|
||||||
|
lombok_version=1.18.34
|
||||||
|
|
||||||
# spikehd was here :)
|
# spikehd was here :)
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -29,13 +29,15 @@ 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.reflections.util.ConfigurationBuilder;
|
||||||
|
import org.reflections.util.FilterBuilder;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
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");
|
|
||||||
@Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class);
|
@Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class);
|
||||||
|
|
||||||
|
public static final Reflections reflector;
|
||||||
@Getter public static ConfigContainer config;
|
@Getter public static ConfigContainer config;
|
||||||
|
|
||||||
@Getter @Setter private static Language language;
|
@Getter @Setter private static Language language;
|
||||||
@ -75,6 +77,16 @@ public final class Grasscutter {
|
|||||||
var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver");
|
var mongoLogger = (Logger) LoggerFactory.getLogger("org.mongodb.driver");
|
||||||
mongoLogger.setLevel(Level.OFF);
|
mongoLogger.setLevel(Level.OFF);
|
||||||
|
|
||||||
|
// Configure the reflector.
|
||||||
|
reflector =
|
||||||
|
new Reflections(
|
||||||
|
new ConfigurationBuilder()
|
||||||
|
.forPackage("emu.grasscutter")
|
||||||
|
.filterInputsBy(
|
||||||
|
new FilterBuilder()
|
||||||
|
.includePackage("emu.grasscutter")
|
||||||
|
.excludePackage("emu.grasscutter.net.proto")));
|
||||||
|
|
||||||
// Load server configuration.
|
// Load server configuration.
|
||||||
Grasscutter.loadConfig();
|
Grasscutter.loadConfig();
|
||||||
// Attempt to update configuration.
|
// Attempt to update configuration.
|
||||||
|
@ -112,7 +112,13 @@ public final class DefaultAuthenticators {
|
|||||||
cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)),
|
cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)),
|
||||||
StandardCharsets.UTF_8);
|
StandardCharsets.UTF_8);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
decryptedPassword = request.getPasswordRequest().password;
|
if (requestData.is_crypto) {
|
||||||
|
response.retcode = -201;
|
||||||
|
response.message = translate("messages.dispatch.account.password_crypto_error");
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
decryptedPassword = request.getPasswordRequest().password;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decryptedPassword == null) {
|
if (decryptedPassword == null) {
|
||||||
|
@ -35,9 +35,10 @@ public class ConfigContainer {
|
|||||||
* HTTP server should start immediately.
|
* HTTP server should start immediately.
|
||||||
* Version 13 - 'game.useUniquePacketKey' was added to control whether the
|
* Version 13 - 'game.useUniquePacketKey' was added to control whether the
|
||||||
* encryption key used for packets is a constant or randomly generated.
|
* encryption key used for packets is a constant or randomly generated.
|
||||||
|
* Version 14 - 'game.timeout' was added to control the UDP client timeout.
|
||||||
*/
|
*/
|
||||||
private static int version() {
|
private static int version() {
|
||||||
return 13;
|
return 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,6 +141,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +184,9 @@ public class ConfigContainer {
|
|||||||
|
|
||||||
/* Kcp internal work interval (milliseconds) */
|
/* Kcp internal work interval (milliseconds) */
|
||||||
public int kcpInterval = 20;
|
public int kcpInterval = 20;
|
||||||
|
/* Time to wait (in seconds) before terminating a connection. */
|
||||||
|
public long timeout = 30;
|
||||||
|
|
||||||
/* Controls whether packets should be logged in console or not */
|
/* Controls whether packets should be logged in console or not */
|
||||||
public ServerDebugMode logPackets = ServerDebugMode.NONE;
|
public ServerDebugMode logPackets = ServerDebugMode.NONE;
|
||||||
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */
|
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */
|
||||||
|
@ -302,6 +302,10 @@ public final class GameData {
|
|||||||
private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap =
|
private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap =
|
||||||
new Int2ObjectOpenHashMap<>();
|
new Int2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static final Int2ObjectMap<HomeWorldModuleData> homeWorldModuleDataMap =
|
||||||
|
new Int2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private static final Int2ObjectMap<HomeWorldNPCData> homeWorldNPCDataMap =
|
private static final Int2ObjectMap<HomeWorldNPCData> homeWorldNPCDataMap =
|
||||||
new Int2ObjectOpenHashMap<>();
|
new Int2ObjectOpenHashMap<>();
|
||||||
|
@ -42,6 +42,7 @@ public class AbilityModifier implements Serializable {
|
|||||||
public String stacking;
|
public String stacking;
|
||||||
|
|
||||||
public AbilityMixinData[] modifierMixins;
|
public AbilityMixinData[] modifierMixins;
|
||||||
|
public AbilityModifierProperty properties;
|
||||||
|
|
||||||
public ElementType elementType;
|
public ElementType elementType;
|
||||||
public DynamicFloat elementDurability = DynamicFloat.ZERO;
|
public DynamicFloat elementDurability = DynamicFloat.ZERO;
|
||||||
@ -327,6 +328,9 @@ public class AbilityModifier implements Serializable {
|
|||||||
public String srcKey, dstKey;
|
public String srcKey, dstKey;
|
||||||
|
|
||||||
public int skillID;
|
public int skillID;
|
||||||
|
public int resistanceListID;
|
||||||
|
public int monsterID;
|
||||||
|
public int summonTag;
|
||||||
|
|
||||||
public AbilityModifierAction[] actions;
|
public AbilityModifierAction[] actions;
|
||||||
public AbilityModifierAction[] successActions;
|
public AbilityModifierAction[] successActions;
|
||||||
@ -369,6 +373,11 @@ public class AbilityModifier implements Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AbilityModifierProperty implements Serializable {
|
||||||
|
public float Actor_HpThresholdRatio;
|
||||||
|
// Add more properties here when GC needs them.
|
||||||
|
}
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
LockHP,
|
LockHP,
|
||||||
Invincible,
|
Invincible,
|
||||||
|
@ -8,4 +8,5 @@ import lombok.experimental.FieldDefaults;
|
|||||||
public class ConfigCombat {
|
public class ConfigCombat {
|
||||||
// There are more values that can be added that might be useful in the json
|
// There are more values that can be added that might be useful in the json
|
||||||
ConfigCombatProperty property;
|
ConfigCombatProperty property;
|
||||||
|
ConfigCombatSummon summon;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package emu.grasscutter.data.binout.config.fields;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ConfigCombatSummon {
|
||||||
|
List<SummonTag> summonTags;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class SummonTag {
|
||||||
|
int summonTag;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package emu.grasscutter.data.excels;
|
||||||
|
|
||||||
|
import emu.grasscutter.data.GameResource;
|
||||||
|
import emu.grasscutter.data.ResourceType;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@ResourceType(name = "HomeworldModuleExcelConfigData.json")
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
@Getter
|
||||||
|
public class HomeWorldModuleData extends GameResource {
|
||||||
|
int Id;
|
||||||
|
boolean isFree;
|
||||||
|
int worldSceneId;
|
||||||
|
int defaultRoomSceneId;
|
||||||
|
}
|
@ -13,6 +13,7 @@ public class TowerLevelData extends GameResource {
|
|||||||
private int levelGroupId;
|
private int levelGroupId;
|
||||||
private int dungeonId;
|
private int dungeonId;
|
||||||
private List<TowerLevelCond> conds;
|
private List<TowerLevelCond> conds;
|
||||||
|
private int monsterLevel;
|
||||||
|
|
||||||
public static class TowerLevelCond {
|
public static class TowerLevelCond {
|
||||||
private TowerCondType towerCondType;
|
private TowerCondType towerCondType;
|
||||||
|
@ -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() + "@grasscutter.io";
|
return this.getUsername() + "@" + ACCOUNT.playerEmail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +235,7 @@ public class Account {
|
|||||||
this.addPermission("*");
|
this.addPermission("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set account default language as server default language
|
// Set account default language to server default language
|
||||||
if (!document.containsKey("locale")) {
|
if (!document.containsKey("locale")) {
|
||||||
this.locale = LANGUAGE;
|
this.locale = LANGUAGE;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package emu.grasscutter.game.ability;
|
|||||||
|
|
||||||
import emu.grasscutter.data.GameData;
|
import emu.grasscutter.data.GameData;
|
||||||
import emu.grasscutter.data.binout.AbilityData;
|
import emu.grasscutter.data.binout.AbilityData;
|
||||||
|
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
|
||||||
import emu.grasscutter.game.entity.GameEntity;
|
import emu.grasscutter.game.entity.GameEntity;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.net.proto.AbilityStringOuterClass.AbilityString;
|
import emu.grasscutter.net.proto.AbilityStringOuterClass.AbilityString;
|
||||||
@ -24,6 +25,7 @@ public class Ability {
|
|||||||
private static Map<String, Object2FloatMap<String>> abilitySpecialsModified = new HashMap<>();
|
private static Map<String, Object2FloatMap<String>> abilitySpecialsModified = new HashMap<>();
|
||||||
|
|
||||||
@Getter private int hash;
|
@Getter private int hash;
|
||||||
|
@Getter private Set<Integer> avatarSkillStartIds;
|
||||||
|
|
||||||
public Ability(AbilityData data, GameEntity owner, Player playerOwner) {
|
public Ability(AbilityData data, GameEntity owner, Player playerOwner) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
@ -44,6 +46,49 @@ public class Ability {
|
|||||||
hash = Utils.abilityHash(data.abilityName);
|
hash = Utils.abilityHash(data.abilityName);
|
||||||
|
|
||||||
data.initialize();
|
data.initialize();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Collect skill IDs referenced by AvatarSkillStart modifier actions
|
||||||
|
// in onAbilityStart and in every modifier's onAdded action set.
|
||||||
|
// These skill IDs will be used by AbilityManager to determine whether
|
||||||
|
// an elemental burst has fired correctly.
|
||||||
|
//
|
||||||
|
avatarSkillStartIds = new HashSet<>();
|
||||||
|
if (data.onAbilityStart != null) {
|
||||||
|
avatarSkillStartIds.addAll(
|
||||||
|
Arrays.stream(data.onAbilityStart)
|
||||||
|
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
|
||||||
|
.map(action -> action.skillID)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
avatarSkillStartIds.addAll(
|
||||||
|
data.modifiers.values().stream()
|
||||||
|
.map(
|
||||||
|
m ->
|
||||||
|
(List<AbilityModifierAction>)
|
||||||
|
(m.onAdded == null ? Collections.emptyList() : Arrays.asList(m.onAdded)))
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
|
||||||
|
.map(action -> action.skillID)
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
if (data.onAdded != null) {
|
||||||
|
processOnAddedAbilityModifiers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processOnAddedAbilityModifiers() {
|
||||||
|
for (AbilityModifierAction modifierAction : data.onAdded) {
|
||||||
|
if (modifierAction.type == null) continue;
|
||||||
|
|
||||||
|
if (modifierAction.type == AbilityModifierAction.Type.ApplyModifier) {
|
||||||
|
if (modifierAction.modifierName == null) continue;
|
||||||
|
else if (!data.modifiers.containsKey(modifierAction.modifierName)) continue;
|
||||||
|
|
||||||
|
var modifierData = data.modifiers.get(modifierAction.modifierName);
|
||||||
|
owner.onAddAbilityModifier(modifierData);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getAbilityName(AbilityString abString) {
|
public static String getAbilityName(AbilityString abString) {
|
||||||
|
@ -21,7 +21,7 @@ import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalar
|
|||||||
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
|
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
|
||||||
import emu.grasscutter.server.event.player.PlayerUseSkillEvent;
|
import emu.grasscutter.server.event.player.PlayerUseSkillEvent;
|
||||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@ -48,9 +48,64 @@ public final class AbilityManager extends BasePlayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Getter private boolean abilityInvulnerable = false;
|
@Getter private boolean abilityInvulnerable = false;
|
||||||
|
private int burstCasterId;
|
||||||
|
private int burstSkillId;
|
||||||
|
|
||||||
public AbilityManager(Player player) {
|
public AbilityManager(Player player) {
|
||||||
super(player);
|
super(player);
|
||||||
|
removePendingEnergyClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePendingEnergyClear() {
|
||||||
|
this.burstCasterId = 0;
|
||||||
|
this.burstSkillId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPossibleElementalBurst(Ability ability, AbilityModifier modifier, int entityId) {
|
||||||
|
//
|
||||||
|
// Possibly clear avatar energy spent on elemental burst
|
||||||
|
// and set invulnerability.
|
||||||
|
//
|
||||||
|
// Problem: Burst can misfire occasionally, like hitting Q when
|
||||||
|
// dashing, doing E, or switching avatars. The client would
|
||||||
|
// still send EvtDoSkillSuccNotify, but the burst may not
|
||||||
|
// actually happen. We don't know when to clear avatar energy.
|
||||||
|
//
|
||||||
|
// When burst does happen, a number of AbilityInvokeEntry will
|
||||||
|
// come in. Use the Ability it references and search for any
|
||||||
|
// modifier with type=AvatarSkillStart, skillID=burst skill ID.
|
||||||
|
//
|
||||||
|
// If that is missing, search for modifier action that sets
|
||||||
|
// invulnerability as a fallback.
|
||||||
|
//
|
||||||
|
if (this.burstCasterId == 0) return;
|
||||||
|
|
||||||
|
boolean skillInvincibility = modifier.state == AbilityModifier.State.Invincible;
|
||||||
|
if (modifier.onAdded != null) {
|
||||||
|
skillInvincibility |=
|
||||||
|
Arrays.stream(modifier.onAdded)
|
||||||
|
.filter(
|
||||||
|
action ->
|
||||||
|
action.type == AbilityModifierAction.Type.AttachAbilityStateResistance
|
||||||
|
&& action.resistanceListID == 11002)
|
||||||
|
.toList()
|
||||||
|
.size()
|
||||||
|
> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.burstCasterId == entityId
|
||||||
|
&& (ability.getAvatarSkillStartIds().contains(this.burstSkillId) || skillInvincibility)) {
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.trace(
|
||||||
|
"Caster ID's {} burst successful, clearing energy and setting invulnerability",
|
||||||
|
entityId);
|
||||||
|
this.abilityInvulnerable = true;
|
||||||
|
this.player
|
||||||
|
.getEnergyManager()
|
||||||
|
.handleEvtDoSkillSuccNotify(
|
||||||
|
this.player.getSession(), this.burstSkillId, this.burstCasterId);
|
||||||
|
this.removePendingEnergyClear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void registerHandlers() {
|
public static void registerHandlers() {
|
||||||
@ -280,8 +335,9 @@ public final class AbilityManager extends BasePlayerManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the player as invulnerable.
|
// Track this elemental burst to possibly clear avatar energy later.
|
||||||
this.abilityInvulnerable = true;
|
this.burstSkillId = skillId;
|
||||||
|
this.burstCasterId = casterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -454,6 +510,8 @@ public final class AbilityManager extends BasePlayerManager {
|
|||||||
modifierData);
|
modifierData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPossibleElementalBurst(instancedAbility, modifierData, invoke.getEntityId());
|
||||||
|
|
||||||
AbilityModifierController modifier =
|
AbilityModifierController modifier =
|
||||||
new AbilityModifierController(instancedAbility, instancedAbilityData, modifierData);
|
new AbilityModifierController(instancedAbility, instancedAbilityData, modifierData);
|
||||||
|
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package emu.grasscutter.game.ability.actions;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
|
import emu.grasscutter.data.GameData;
|
||||||
|
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
|
||||||
|
import emu.grasscutter.game.ability.Ability;
|
||||||
|
import emu.grasscutter.game.entity.*;
|
||||||
|
import emu.grasscutter.game.world.*;
|
||||||
|
import emu.grasscutter.net.proto.EPKDEHOJFLIOuterClass.EPKDEHOJFLI;
|
||||||
|
import emu.grasscutter.server.packet.send.PacketMonsterSummonTagNotify;
|
||||||
|
import emu.grasscutter.utils.*;
|
||||||
|
|
||||||
|
@AbilityAction(AbilityModifierAction.Type.Summon)
|
||||||
|
public class ActionSummon extends AbilityActionHandler {
|
||||||
|
@Override
|
||||||
|
public synchronized boolean execute(
|
||||||
|
Ability ability, AbilityModifierAction action, ByteString abilityData, GameEntity target) {
|
||||||
|
EPKDEHOJFLI summonPosRot = null;
|
||||||
|
try {
|
||||||
|
// In game version 4.0, summoned entity's
|
||||||
|
// position and rotation are packed in EPKDEHOJFLI.
|
||||||
|
// This is packet AbilityActionSummon and has two fields:
|
||||||
|
// 4: Vector pos
|
||||||
|
// 13: Vector rot
|
||||||
|
summonPosRot = EPKDEHOJFLI.parseFrom(abilityData);
|
||||||
|
} catch (InvalidProtocolBufferException e) {
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.error("Failed to parse abilityData: {}", Utils.bytesToHex(abilityData.toByteArray()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = new Position(summonPosRot.getPos());
|
||||||
|
var rot = new Position(summonPosRot.getRot());
|
||||||
|
var monsterId = action.monsterID;
|
||||||
|
|
||||||
|
var scene = target.getScene();
|
||||||
|
|
||||||
|
var monsterData = GameData.getMonsterDataMap().get(monsterId);
|
||||||
|
if (monsterData == null) {
|
||||||
|
Grasscutter.getLogger().error("Failed to find monster by ID {}", monsterId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof EntityMonster ownerEntity) {
|
||||||
|
var level = scene.getLevelForMonster(0, ownerEntity.getLevel());
|
||||||
|
var entity = new EntityMonster(scene, monsterData, pos, rot, level);
|
||||||
|
ownerEntity.getSummonTagMap().put(action.summonTag, entity);
|
||||||
|
entity.setSummonedTag(action.summonTag);
|
||||||
|
entity.setOwnerEntityId(target.getId());
|
||||||
|
scene.addEntity(entity);
|
||||||
|
scene.getPlayers().get(0).sendPacket(new PacketMonsterSummonTagNotify(ownerEntity));
|
||||||
|
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.trace(
|
||||||
|
"Spawned entityId {} monsterId {} pos {} rot {}, target { {} }, action { {} }",
|
||||||
|
entity.getId(),
|
||||||
|
monsterId,
|
||||||
|
pos,
|
||||||
|
rot,
|
||||||
|
target,
|
||||||
|
action);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,6 +68,10 @@ public final class DungeonManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFinishedSuccessfully()) {
|
if (isFinishedSuccessfully()) {
|
||||||
|
// Set ended now because calling EVENT_DUNGEON_SETTLE
|
||||||
|
// during finishDungeon() may cause reentrance into
|
||||||
|
// this function, leading to double settles.
|
||||||
|
ended = true;
|
||||||
finishDungeon();
|
finishDungeon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,8 +82,13 @@ public final class DungeonManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getLevelForMonster(int id) {
|
public int getLevelForMonster(int id) {
|
||||||
// TODO should use levelConfigMap? and how?
|
if (isTowerDungeon()) {
|
||||||
return dungeonData.getShowLevel();
|
// Tower dungeons have their own level setting in TowerLevelData
|
||||||
|
return scene.getPlayers().get(0).getTowerManager().getCurrentMonsterLevel();
|
||||||
|
} else {
|
||||||
|
// TODO should use levelConfigMap? and how?
|
||||||
|
return dungeonData.getShowLevel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean activateRespawnPoint(int pointId) {
|
public boolean activateRespawnPoint(int pointId) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package emu.grasscutter.game.dungeons;
|
package emu.grasscutter.game.dungeons;
|
||||||
|
|
||||||
|
import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult;
|
||||||
import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult.DungeonEndReason;
|
import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult.DungeonEndReason;
|
||||||
import emu.grasscutter.game.dungeons.dungeon_results.TowerResult;
|
import emu.grasscutter.game.dungeons.dungeon_results.TowerResult;
|
||||||
import emu.grasscutter.server.packet.send.*;
|
import emu.grasscutter.server.packet.send.*;
|
||||||
@ -25,16 +26,22 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
|
|||||||
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
||||||
var stars = towerManager.getCurLevelStars();
|
var stars = towerManager.getCurLevelStars();
|
||||||
|
|
||||||
towerManager.notifyCurLevelRecordChangeWhenDone(stars);
|
if (endReason == DungeonEndReason.COMPLETED) {
|
||||||
scene.broadcastPacket(
|
// Update star record only when challenge completes successfully.
|
||||||
new PacketTowerFloorRecordChangeNotify(
|
towerManager.notifyCurLevelRecordChangeWhenDone(stars);
|
||||||
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor()));
|
scene.broadcastPacket(
|
||||||
|
new PacketTowerFloorRecordChangeNotify(
|
||||||
|
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor()));
|
||||||
|
}
|
||||||
|
|
||||||
var challenge = scene.getChallenge();
|
var challenge = scene.getChallenge();
|
||||||
|
var finishedTime = challenge == null ? challenge.getFinishedTime() : 0;
|
||||||
var dungeonStats =
|
var dungeonStats =
|
||||||
new DungeonEndStats(
|
new DungeonEndStats(scene.getKilledMonsterCount(), finishedTime, 0, endReason);
|
||||||
scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason);
|
var result =
|
||||||
var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars);
|
endReason == DungeonEndReason.COMPLETED
|
||||||
|
? new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars)
|
||||||
|
: new BaseDungeonResult(dungeonData, dungeonStats);
|
||||||
|
|
||||||
scene.broadcastPacket(new PacketDungeonSettleNotify(result));
|
scene.broadcastPacket(new PacketDungeonSettleNotify(result));
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter;
|
|||||||
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
|
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
|
||||||
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
|
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
|
||||||
import emu.grasscutter.game.entity.*;
|
import emu.grasscutter.game.entity.*;
|
||||||
|
import emu.grasscutter.game.props.FightProperty;
|
||||||
import emu.grasscutter.game.props.WatcherTriggerType;
|
import emu.grasscutter.game.props.WatcherTriggerType;
|
||||||
import emu.grasscutter.game.world.Scene;
|
import emu.grasscutter.game.world.Scene;
|
||||||
import emu.grasscutter.scripts.constants.EventType;
|
import emu.grasscutter.scripts.constants.EventType;
|
||||||
@ -22,12 +23,13 @@ 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 long startedAt;
|
private int startedAt;
|
||||||
private int finishedTime;
|
private int finishedTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,6 +60,7 @@ public class WorldChallenge {
|
|||||||
this.challengeTriggers = challengeTriggers;
|
this.challengeTriggers = challengeTriggers;
|
||||||
this.goal = goal;
|
this.goal = goal;
|
||||||
this.score = new AtomicInteger(0);
|
this.score = new AtomicInteger(0);
|
||||||
|
this.guardEntity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean inProgress() {
|
public boolean inProgress() {
|
||||||
@ -143,6 +146,10 @@ public class WorldChallenge {
|
|||||||
this.progress = false;
|
this.progress = false;
|
||||||
this.success = success;
|
this.success = success;
|
||||||
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt));
|
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt));
|
||||||
|
|
||||||
|
// Despawn all leftover mobs in this challenge's SceneGroup
|
||||||
|
getScene().getScriptManager().removeMonstersInGroup(group);
|
||||||
|
|
||||||
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
|
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +157,20 @@ public class WorldChallenge {
|
|||||||
return score.incrementAndGet();
|
return score.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getGuardEntityHpPercent() {
|
||||||
|
if (guardEntity == null) {
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.warn(
|
||||||
|
"getGuardEntityHpPercent: Could not find guardEntity for this challenge = {}", this);
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
var curHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
|
||||||
|
var maxHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
|
||||||
|
int percent = (int) (curHp * 100 / maxHp);
|
||||||
|
return percent;
|
||||||
|
}
|
||||||
|
|
||||||
public void onMonsterDeath(EntityMonster monster) {
|
public void onMonsterDeath(EntityMonster monster) {
|
||||||
if (!inProgress()) {
|
if (!inProgress()) {
|
||||||
return;
|
return;
|
||||||
|
@ -33,7 +33,7 @@ public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHand
|
|||||||
realGroup,
|
realGroup,
|
||||||
challengeId, // Id
|
challengeId, // Id
|
||||||
challengeIndex, // Index
|
challengeIndex, // Index
|
||||||
List.of(monstersToKill, 0),
|
List.of(monstersToKill, gadgetCFGId),
|
||||||
0, // Limit
|
0, // Limit
|
||||||
monstersToKill, // Goal
|
monstersToKill, // Goal
|
||||||
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));
|
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));
|
||||||
|
@ -36,6 +36,6 @@ public class KillMonsterCountInTimeIncChallengeFactoryHandler implements Challen
|
|||||||
List.of(
|
List.of(
|
||||||
new KillMonsterCountTrigger(),
|
new KillMonsterCountTrigger(),
|
||||||
new InTimeTrigger(),
|
new InTimeTrigger(),
|
||||||
new KillMonsterTimeIncTrigger(timeInc)));
|
new KillMonsterTimeIncTrigger(timeLimit, timeInc)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
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.List;
|
import java.util.*;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
|
||||||
public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler {
|
public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler {
|
||||||
@ -28,6 +29,16 @@ 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,
|
||||||
@ -36,6 +47,6 @@ public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryH
|
|||||||
List.of(targetCount, timeLimit),
|
List.of(targetCount, timeLimit),
|
||||||
timeLimit, // Limit
|
timeLimit, // Limit
|
||||||
targetCount, // Goal
|
targetCount, // Goal
|
||||||
List.of(new KillMonsterCountTrigger(), new InTimeTrigger()));
|
challengeTriggers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package emu.grasscutter.game.dungeons.challenge.trigger;
|
|||||||
|
|
||||||
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
|
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
|
||||||
import emu.grasscutter.game.entity.EntityGadget;
|
import emu.grasscutter.game.entity.EntityGadget;
|
||||||
import emu.grasscutter.game.props.FightProperty;
|
|
||||||
import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
|
import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
|
||||||
|
|
||||||
public class GuardTrigger extends ChallengeTrigger {
|
public class GuardTrigger extends ChallengeTrigger {
|
||||||
@ -14,7 +13,12 @@ public class GuardTrigger extends ChallengeTrigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onBegin(WorldChallenge challenge) {
|
public void onBegin(WorldChallenge challenge) {
|
||||||
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, 100));
|
challenge.setGuardEntity(
|
||||||
|
challenge.getScene().getEntityByConfigId(entityToProtectCFGId, challenge.getGroup().id));
|
||||||
|
lastSendPercent = challenge.getGuardEntityHpPercent();
|
||||||
|
challenge
|
||||||
|
.getScene()
|
||||||
|
.broadcastPacket(new PacketChallengeDataNotify(challenge, 2, lastSendPercent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -22,9 +26,7 @@ public class GuardTrigger extends ChallengeTrigger {
|
|||||||
if (gadget.getConfigId() != entityToProtectCFGId) {
|
if (gadget.getConfigId() != entityToProtectCFGId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
|
var percent = challenge.getGuardEntityHpPercent();
|
||||||
var maxHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
|
|
||||||
int percent = (int) (curHp / maxHp);
|
|
||||||
|
|
||||||
if (percent != lastSendPercent) {
|
if (percent != lastSendPercent) {
|
||||||
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent));
|
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent));
|
||||||
|
@ -18,12 +18,6 @@ public class InTimeTrigger extends ChallengeTrigger {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCheckTimeout(WorldChallenge challenge) {
|
public void onCheckTimeout(WorldChallenge challenge) {
|
||||||
// In Tower challenges, time can run out without
|
|
||||||
// causing the challenge to fail. (Player just
|
|
||||||
// gets 0 stars when they ultimately finish.)
|
|
||||||
var dungeonManager = challenge.getScene().getDungeonManager();
|
|
||||||
if (dungeonManager != null && dungeonManager.isTowerDungeon()) return;
|
|
||||||
|
|
||||||
var current = challenge.getScene().getSceneTimeSeconds();
|
var current = challenge.getScene().getSceneTimeSeconds();
|
||||||
if (current - challenge.getStartedAt() > challenge.getTimeLimit()) {
|
if (current - challenge.getStartedAt() > challenge.getTimeLimit()) {
|
||||||
challenge.fail();
|
challenge.fail();
|
||||||
|
@ -6,22 +6,33 @@ import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
|
|||||||
|
|
||||||
public class KillMonsterTimeIncTrigger extends ChallengeTrigger {
|
public class KillMonsterTimeIncTrigger extends ChallengeTrigger {
|
||||||
|
|
||||||
private int increment;
|
private final int maxTime;
|
||||||
|
private final int increment;
|
||||||
|
|
||||||
public KillMonsterTimeIncTrigger(int increment) {
|
public KillMonsterTimeIncTrigger(int maxTime, int increment) {
|
||||||
|
this.maxTime = maxTime;
|
||||||
this.increment = increment;
|
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) {
|
||||||
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 0, increment));
|
var scene = challenge.getScene();
|
||||||
|
var elapsed = scene.getSceneTimeSeconds() - challenge.getStartedAt();
|
||||||
|
var timeLeft = challenge.getTimeLimit() - elapsed;
|
||||||
|
var increment = this.increment;
|
||||||
|
if (increment == 0) {
|
||||||
|
// Refresh time limit back to max
|
||||||
|
increment = maxTime - timeLeft;
|
||||||
|
} else if (maxTime < timeLeft + increment) {
|
||||||
|
// Don't add back more time than original limit
|
||||||
|
increment -= timeLeft + increment - maxTime;
|
||||||
|
}
|
||||||
challenge.setTimeLimit(challenge.getTimeLimit() + increment);
|
challenge.setTimeLimit(challenge.getTimeLimit() + increment);
|
||||||
|
scene.broadcastPacket(
|
||||||
|
new PacketChallengeDataNotify(
|
||||||
|
challenge, 2, timeLeft + increment + scene.getSceneTimeSeconds()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,11 +32,12 @@ public class TowerResult extends BaseDungeonResult {
|
|||||||
@Override
|
@Override
|
||||||
protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
|
protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
|
||||||
var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE;
|
var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE;
|
||||||
if (challenge.isSuccess() && canJump) {
|
if (challenge.isSuccess()) {
|
||||||
continueStatus =
|
if (hasNextLevel) {
|
||||||
hasNextLevel
|
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE;
|
||||||
? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE
|
} else if (canJump) {
|
||||||
: ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
|
continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var towerLevelEndNotify =
|
var towerLevelEndNotify =
|
||||||
|
@ -70,6 +70,11 @@ public abstract class EntityBaseGadget extends GameEntity {
|
|||||||
.setSourceEntityId(getId())
|
.setSourceEntityId(getId())
|
||||||
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
|
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
|
||||||
.setEventSource(getConfigId()));
|
.setEventSource(getConfigId()));
|
||||||
|
|
||||||
|
var challenge = getScene().getChallenge();
|
||||||
|
if (challenge != null && this instanceof EntityGadget gadget) {
|
||||||
|
challenge.onGadgetDamage(gadget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void fillFightProps(ConfigEntityGadget configGadget) {
|
protected void fillFightProps(ConfigEntityGadget configGadget) {
|
||||||
|
@ -5,6 +5,7 @@ import emu.grasscutter.data.GameData;
|
|||||||
import emu.grasscutter.data.binout.config.ConfigEntityGadget;
|
import emu.grasscutter.data.binout.config.ConfigEntityGadget;
|
||||||
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData;
|
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData;
|
||||||
import emu.grasscutter.data.excels.GadgetData;
|
import emu.grasscutter.data.excels.GadgetData;
|
||||||
|
import emu.grasscutter.data.excels.monster.MonsterCurveData;
|
||||||
import emu.grasscutter.game.entity.gadget.*;
|
import emu.grasscutter.game.entity.gadget.*;
|
||||||
import emu.grasscutter.game.entity.gadget.platform.*;
|
import emu.grasscutter.game.entity.gadget.platform.*;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
@ -104,6 +105,25 @@ public class EntityGadget extends EntityBaseGadget {
|
|||||||
this.bornRot = this.getRotation().clone();
|
this.bornRot = this.getRotation().clone();
|
||||||
this.fillFightProps(configGadget);
|
this.fillFightProps(configGadget);
|
||||||
|
|
||||||
|
// Check if this gadget is the abyss defense objective's gadget.
|
||||||
|
// That doesn't have a level and defaults to having 5000 hp, so it dies in like 2 hits on 11-1.
|
||||||
|
// I'll forgive player skill issues and scale its hp up here.
|
||||||
|
// TODO: find out how its fight props are actually scaled
|
||||||
|
if (gadgetData.getJsonName().equals("SceneObj_Gear_Operator_Mamolu_Entity")) {
|
||||||
|
MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(11);
|
||||||
|
if (curve != null) {
|
||||||
|
FightProperty[] hpProps = {
|
||||||
|
FightProperty.FIGHT_PROP_MAX_HP,
|
||||||
|
FightProperty.FIGHT_PROP_BASE_HP,
|
||||||
|
FightProperty.FIGHT_PROP_CUR_HP
|
||||||
|
};
|
||||||
|
for (var prop : hpProps) {
|
||||||
|
setFightProperty(
|
||||||
|
prop, this.getFightProperty(prop) * curve.getMultByProp("GROW_CURVE_HP_ENVIRONMENT"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (GameData.getGadgetMappingMap().containsKey(gadgetId)) {
|
if (GameData.getGadgetMappingMap().containsKey(gadgetId)) {
|
||||||
var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController();
|
var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController();
|
||||||
this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName));
|
this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName));
|
||||||
|
@ -25,6 +25,7 @@ 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;
|
||||||
@ -49,6 +50,9 @@ public class EntityMonster extends GameEntity {
|
|||||||
@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;
|
||||||
|
|
||||||
@ -67,6 +71,9 @@ public class EntityMonster extends GameEntity {
|
|||||||
this.bornPos = this.getPosition().clone();
|
this.bornPos = this.getPosition().clone();
|
||||||
this.level = level;
|
this.level = level;
|
||||||
this.playerOnBattle = new ArrayList<>();
|
this.playerOnBattle = new ArrayList<>();
|
||||||
|
this.summonTagMap = new HashMap<>();
|
||||||
|
this.summonedTag = 0;
|
||||||
|
this.ownerEntityId = 0;
|
||||||
|
|
||||||
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
|
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
|
||||||
this.configEntityMonster =
|
this.configEntityMonster =
|
||||||
@ -76,6 +83,17 @@ public class EntityMonster extends GameEntity {
|
|||||||
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());
|
||||||
@ -204,7 +222,9 @@ public class EntityMonster extends GameEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onTick(int sceneTime) {
|
||||||
|
super.onTick(sceneTime);
|
||||||
|
|
||||||
// Lua event
|
// Lua event
|
||||||
getScene()
|
getScene()
|
||||||
.getScriptManager()
|
.getScriptManager()
|
||||||
@ -316,6 +336,11 @@ public class EntityMonster extends GameEntity {
|
|||||||
this.getMonsterData().getType().getValue());
|
this.getMonsterData().getType().getValue());
|
||||||
scene.triggerDungeonEvent(
|
scene.triggerDungeonEvent(
|
||||||
DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
|
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() {
|
||||||
@ -355,6 +380,28 @@ public class EntityMonster extends GameEntity {
|
|||||||
+ (this.getFightProperty(c.getBase())
|
+ (this.getFightProperty(c.getBase())
|
||||||
* (1f + this.getFightProperty(c.getPercent())))));
|
* (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,
|
FightProperty.FIGHT_PROP_CUR_HP,
|
||||||
@ -365,14 +412,17 @@ public class EntityMonster extends GameEntity {
|
|||||||
public SceneEntityInfo toProto() {
|
public SceneEntityInfo toProto() {
|
||||||
var data = this.getMonsterData();
|
var data = this.getMonsterData();
|
||||||
|
|
||||||
|
var aiInfo =
|
||||||
|
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(this.getBornPos().toProto());
|
||||||
|
if (ownerEntityId != 0) {
|
||||||
|
aiInfo.setServantInfo(ServantInfo.newBuilder().setMasterEntityId(ownerEntityId));
|
||||||
|
}
|
||||||
|
|
||||||
var authority =
|
var authority =
|
||||||
EntityAuthorityInfo.newBuilder()
|
EntityAuthorityInfo.newBuilder()
|
||||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||||
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
||||||
.setAiInfo(
|
.setAiInfo(aiInfo)
|
||||||
SceneEntityAiInfo.newBuilder()
|
|
||||||
.setIsAiOpen(true)
|
|
||||||
.setBornPos(this.getBornPos().toProto()))
|
|
||||||
.setBornPos(this.getBornPos().toProto())
|
.setBornPos(this.getBornPos().toProto())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -403,7 +453,10 @@ 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) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package emu.grasscutter.game.entity;
|
package emu.grasscutter.game.entity;
|
||||||
|
|
||||||
import emu.grasscutter.data.GameData;
|
import emu.grasscutter.data.GameData;
|
||||||
|
import emu.grasscutter.data.binout.*;
|
||||||
import emu.grasscutter.game.ability.*;
|
import emu.grasscutter.game.ability.*;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.game.props.*;
|
import emu.grasscutter.game.props.*;
|
||||||
@ -32,6 +33,8 @@ public abstract class GameEntity {
|
|||||||
@Getter @Setter private int lastMoveReliableSeq;
|
@Getter @Setter private int lastMoveReliableSeq;
|
||||||
|
|
||||||
@Getter @Setter private boolean lockHP;
|
@Getter @Setter private boolean lockHP;
|
||||||
|
private boolean limbo;
|
||||||
|
private float limboHpThreshold;
|
||||||
|
|
||||||
@Setter(AccessLevel.PROTECTED)
|
@Setter(AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
@ -110,6 +113,21 @@ public abstract class GameEntity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setLimbo(float hpThreshold) {
|
||||||
|
limbo = true;
|
||||||
|
limboHpThreshold = hpThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onAddAbilityModifier(AbilityModifier data) {
|
||||||
|
// Set limbo state (invulnerability at a certain HP threshold)
|
||||||
|
// if ability modifier calls for it
|
||||||
|
if (data.state == AbilityModifier.State.Limbo
|
||||||
|
&& data.properties != null
|
||||||
|
&& data.properties.Actor_HpThresholdRatio > .0f) {
|
||||||
|
this.setLimbo(data.properties.Actor_HpThresholdRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected MotionInfo getMotionInfo() {
|
protected MotionInfo getMotionInfo() {
|
||||||
return MotionInfo.newBuilder()
|
return MotionInfo.newBuilder()
|
||||||
.setPos(this.getPosition().toProto())
|
.setPos(this.getPosition().toProto())
|
||||||
@ -167,12 +185,27 @@ public abstract class GameEntity {
|
|||||||
return; // If the event is canceled, do not damage the entity.
|
return; // If the event is canceled, do not damage the entity.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float effectiveDamage = 0;
|
||||||
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||||
if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
|
if (limbo) {
|
||||||
// Add negative HP to the current HP property.
|
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||||
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
|
float curRatio = curHp / maxHp;
|
||||||
|
if (curRatio > limboHpThreshold) {
|
||||||
|
// OK if this hit takes HP below threshold.
|
||||||
|
effectiveDamage = event.getDamage();
|
||||||
|
}
|
||||||
|
if (effectiveDamage >= curHp && limboHpThreshold > .0f) {
|
||||||
|
// Don't let entity die while in limbo.
|
||||||
|
effectiveDamage = curHp - 1;
|
||||||
|
}
|
||||||
|
} else if (curHp != Float.POSITIVE_INFINITY && !lockHP
|
||||||
|
|| lockHP && curHp <= event.getDamage()) {
|
||||||
|
effectiveDamage = event.getDamage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add negative HP to the current HP property.
|
||||||
|
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -effectiveDamage);
|
||||||
|
|
||||||
this.lastAttackType = attackType;
|
this.lastAttackType = attackType;
|
||||||
this.checkIfDead();
|
this.checkIfDead();
|
||||||
this.runLuaCallbacks(event);
|
this.runLuaCallbacks(event);
|
||||||
|
@ -9,9 +9,10 @@ import emu.grasscutter.database.DatabaseHelper;
|
|||||||
import emu.grasscutter.game.avatar.Avatar;
|
import emu.grasscutter.game.avatar.Avatar;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.game.props.SceneType;
|
import emu.grasscutter.game.props.SceneType;
|
||||||
import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass;
|
import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo;
|
||||||
import emu.grasscutter.server.packet.send.*;
|
import emu.grasscutter.server.packet.send.*;
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntSets;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -36,6 +37,10 @@ public class GameHome {
|
|||||||
|| sceneData.getSceneType() == SceneType.SCENE_HOME_ROOM)
|
|| sceneData.getSceneType() == SceneType.SCENE_HOME_ROOM)
|
||||||
.map(SceneData::getId)
|
.map(SceneData::getId)
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
|
public static final Set<Integer> HOME_MODULE_IDS =
|
||||||
|
GameData.getHomeWorldModuleDataMap().isEmpty()
|
||||||
|
? IntSets.fromTo(1, 6)
|
||||||
|
: GameData.getHomeWorldModuleDataMap().keySet();
|
||||||
|
|
||||||
@Id String id;
|
@Id String id;
|
||||||
|
|
||||||
@ -181,6 +186,7 @@ public class GameHome {
|
|||||||
|
|
||||||
public void onOwnerLogin(Player player) {
|
public void onOwnerLogin(Player player) {
|
||||||
this.player = player; // update player pointer. (prevent offline player from sending packet)
|
this.player = player; // update player pointer. (prevent offline player from sending packet)
|
||||||
|
this.fixModuleIdIfInvalid();
|
||||||
player.getSession().send(new PacketHomeBasicInfoNotify(player, false));
|
player.getSession().send(new PacketHomeBasicInfoNotify(player, false));
|
||||||
player.getSession().send(new PacketPlayerHomeCompInfoNotify(player));
|
player.getSession().send(new PacketPlayerHomeCompInfoNotify(player));
|
||||||
player.getSession().send(new PacketHomeComfortInfoNotify(player));
|
player.getSession().send(new PacketHomeComfortInfoNotify(player));
|
||||||
@ -194,6 +200,35 @@ public class GameHome {
|
|||||||
player.getSession().send(new PacketHomeResourceNotify(player));
|
player.getSession().send(new PacketHomeResourceNotify(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fixModuleIdIfInvalid() {
|
||||||
|
if (this.player.hasSentLoginPackets() || this.player.getRealmList() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player
|
||||||
|
.getRealmList()
|
||||||
|
.removeIf(integer -> !HOME_MODULE_IDS.contains(integer)); // Delete invalid module ids.
|
||||||
|
|
||||||
|
if (this.player.getRealmList().isEmpty()) {
|
||||||
|
this.player.setRealmList(null);
|
||||||
|
this.player.save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.player.getCurrentRealmId() <= 0 || !this.player.getCurHomeWorld().isRealmIdValid()) {
|
||||||
|
int firstRId = this.player.getRealmList().iterator().next();
|
||||||
|
this.player.setCurrentRealmId(firstRId);
|
||||||
|
this.player.save();
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.info(
|
||||||
|
"Set player {}'s current realm id to {} cuz the id is invalid.",
|
||||||
|
this.player.getUid(),
|
||||||
|
firstRId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player.getCurHomeWorld().refreshModuleManager(); // Apply module id fix.
|
||||||
|
}
|
||||||
|
|
||||||
public void onPlayerChangedAvatarCostume(Avatar avatar) {
|
public void onPlayerChangedAvatarCostume(Avatar avatar) {
|
||||||
var world = this.player.getServer().getHomeWorldOrCreate(this.player);
|
var world = this.player.getServer().getHomeWorldOrCreate(this.player);
|
||||||
world.broadcastPacket(
|
world.broadcastPacket(
|
||||||
@ -239,8 +274,7 @@ public class GameHome {
|
|||||||
return this.finishedTalkIdMap.get(avatarId);
|
return this.finishedTalkIdMap.get(avatarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo>
|
public List<HomeAvatarTalkFinishInfo> toAvatarTalkFinishInfoProto() {
|
||||||
toAvatarTalkFinishInfoProto() {
|
|
||||||
if (this.finishedTalkIdMap == null) {
|
if (this.finishedTalkIdMap == null) {
|
||||||
this.finishedTalkIdMap = new HashMap<>();
|
this.finishedTalkIdMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
@ -248,7 +282,7 @@ public class GameHome {
|
|||||||
return this.finishedTalkIdMap.entrySet().stream()
|
return this.finishedTalkIdMap.entrySet().stream()
|
||||||
.map(
|
.map(
|
||||||
e -> {
|
e -> {
|
||||||
return HomeAvatarTalkFinishInfoOuterClass.HomeAvatarTalkFinishInfo.newBuilder()
|
return HomeAvatarTalkFinishInfo.newBuilder()
|
||||||
.setAvatarId(e.getKey())
|
.setAvatarId(e.getKey())
|
||||||
.addAllFinishTalkIdList(e.getValue())
|
.addAllFinishTalkIdList(e.getValue())
|
||||||
.build();
|
.build();
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
package emu.grasscutter.game.home;
|
package emu.grasscutter.game.home;
|
||||||
|
|
||||||
import com.github.davidmoten.guavamini.Lists;
|
import com.github.davidmoten.guavamini.Lists;
|
||||||
|
import emu.grasscutter.game.home.suite.HomeSuiteItem;
|
||||||
import emu.grasscutter.game.home.suite.event.HomeAvatarRewardEvent;
|
import emu.grasscutter.game.home.suite.event.HomeAvatarRewardEvent;
|
||||||
import emu.grasscutter.game.home.suite.event.HomeAvatarSummonEvent;
|
import emu.grasscutter.game.home.suite.event.HomeAvatarSummonEvent;
|
||||||
import emu.grasscutter.game.home.suite.event.SuiteEventType;
|
import emu.grasscutter.game.home.suite.event.SuiteEventType;
|
||||||
import emu.grasscutter.game.inventory.GameItem;
|
import emu.grasscutter.game.inventory.GameItem;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass;
|
import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify;
|
||||||
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass;
|
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify;
|
||||||
import emu.grasscutter.net.proto.RetcodeOuterClass;
|
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||||
import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonAllEventNotify;
|
import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonAllEventNotify;
|
||||||
import emu.grasscutter.utils.Either;
|
import emu.grasscutter.utils.Either;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.experimental.FieldDefaults;
|
import lombok.experimental.FieldDefaults;
|
||||||
@ -24,8 +26,8 @@ public class HomeModuleManager {
|
|||||||
final HomeWorld homeWorld;
|
final HomeWorld homeWorld;
|
||||||
final GameHome home;
|
final GameHome home;
|
||||||
final int moduleId;
|
final int moduleId;
|
||||||
final HomeScene outdoor;
|
@Nullable final HomeScene outdoor;
|
||||||
HomeScene indoor;
|
@Nullable HomeScene indoor;
|
||||||
final List<HomeAvatarRewardEvent> rewardEvents;
|
final List<HomeAvatarRewardEvent> rewardEvents;
|
||||||
final List<HomeAvatarSummonEvent> summonEvents;
|
final List<HomeAvatarSummonEvent> summonEvents;
|
||||||
|
|
||||||
@ -45,8 +47,14 @@ public class HomeModuleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.outdoor.onTick();
|
if (this.outdoor != null) {
|
||||||
this.indoor.onTick();
|
this.outdoor.onTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indoor != null) {
|
||||||
|
this.indoor.onTick();
|
||||||
|
}
|
||||||
|
|
||||||
this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
|
this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +75,7 @@ public class HomeModuleManager {
|
|||||||
this.rewardEvents.clear();
|
this.rewardEvents.clear();
|
||||||
var allBlockItems =
|
var allBlockItems =
|
||||||
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
|
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
.map(HomeSceneItem::getBlockItems)
|
.map(HomeSceneItem::getBlockItems)
|
||||||
.map(Map::values)
|
.map(Map::values)
|
||||||
.flatMap(Collection::stream)
|
.flatMap(Collection::stream)
|
||||||
@ -114,6 +123,7 @@ public class HomeModuleManager {
|
|||||||
private void cancelSummonEventsIfAvatarLeave() {
|
private void cancelSummonEventsIfAvatarLeave() {
|
||||||
var avatars =
|
var avatars =
|
||||||
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
|
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
.map(HomeSceneItem::getBlockItems)
|
.map(HomeSceneItem::getBlockItems)
|
||||||
.map(Map::values)
|
.map(Map::values)
|
||||||
.flatMap(Collection::stream)
|
.flatMap(Collection::stream)
|
||||||
@ -127,16 +137,16 @@ public class HomeModuleManager {
|
|||||||
|
|
||||||
public Either<List<GameItem>, Integer> claimAvatarRewards(int eventId) {
|
public Either<List<GameItem>, Integer> claimAvatarRewards(int eventId) {
|
||||||
if (this.rewardEvents.isEmpty()) {
|
if (this.rewardEvents.isEmpty()) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
|
return Either.right(Retcode.RET_FAIL_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
var event = this.rewardEvents.remove(0);
|
var event = this.rewardEvents.remove(0);
|
||||||
if (event.getEventId() != eventId) {
|
if (event.getEventId() != eventId) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
|
return Either.right(Retcode.RET_FAIL_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
|
if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
|
return Either.right(Retcode.RET_FAIL_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Either.left(event.giveRewards());
|
return Either.left(event.giveRewards());
|
||||||
@ -144,32 +154,34 @@ public class HomeModuleManager {
|
|||||||
|
|
||||||
public Either<HomeAvatarSummonEvent, Integer> fireAvatarSummonEvent(
|
public Either<HomeAvatarSummonEvent, Integer> fireAvatarSummonEvent(
|
||||||
Player owner, int avatarId, int guid, int suiteId) {
|
Player owner, int avatarId, int guid, int suiteId) {
|
||||||
var targetSuite =
|
HomeSuiteItem targetSuite = null;
|
||||||
((HomeScene) owner.getScene())
|
if (owner.getScene() instanceof HomeScene homeScene) {
|
||||||
.getSceneItem().getBlockItems().values().stream()
|
targetSuite =
|
||||||
.map(HomeBlockItem::getSuiteList)
|
homeScene.getSceneItem().getBlockItems().values().stream()
|
||||||
.flatMap(Collection::stream)
|
.map(HomeBlockItem::getSuiteList)
|
||||||
.filter(suite -> suite.getGuid() == guid)
|
.flatMap(Collection::stream)
|
||||||
.findFirst()
|
.filter(suite -> suite.getGuid() == guid)
|
||||||
.orElse(null);
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isInRewardEvent(avatarId)) {
|
if (this.isInRewardEvent(avatarId)) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_DUPLICATE_AVATAR_VALUE);
|
return Either.right(Retcode.RET_DUPLICATE_AVATAR_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.rewardEvents.stream().anyMatch(event -> event.getGuid() == guid)) {
|
if (this.rewardEvents.stream().anyMatch(event -> event.getGuid() == guid)) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
|
return Either.right(Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
|
this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
|
||||||
|
|
||||||
if (targetSuite == null) {
|
if (targetSuite == null) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
|
var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
|
||||||
if (eventData == null) {
|
if (eventData == null) {
|
||||||
return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
var event =
|
var event =
|
||||||
@ -184,8 +196,8 @@ public class HomeModuleManager {
|
|||||||
this.summonEvents.removeIf(event -> event.getEventId() == eventId);
|
this.summonEvents.removeIf(event -> event.getEventId() == eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify toRewardEventProto() {
|
public HomeAvatarRewardEventNotify toRewardEventProto() {
|
||||||
var notify = HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify.newBuilder();
|
var notify = HomeAvatarRewardEventNotify.newBuilder();
|
||||||
if (!this.rewardEvents.isEmpty()) {
|
if (!this.rewardEvents.isEmpty()) {
|
||||||
notify.setRewardEvent(this.rewardEvents.get(0).toProto()).setIsEventTrigger(true);
|
notify.setRewardEvent(this.rewardEvents.get(0).toProto()).setIsEventTrigger(true);
|
||||||
|
|
||||||
@ -198,9 +210,8 @@ public class HomeModuleManager {
|
|||||||
return notify.build();
|
return notify.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify
|
public HomeAvatarSummonAllEventNotify toSummonEventProto() {
|
||||||
toSummonEventProto() {
|
return HomeAvatarSummonAllEventNotify.newBuilder()
|
||||||
return HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify.newBuilder()
|
|
||||||
.addAllSummonEventList(
|
.addAllSummonEventList(
|
||||||
this.summonEvents.stream().map(HomeAvatarSummonEvent::toProto).toList())
|
this.summonEvents.stream().map(HomeAvatarSummonEvent::toProto).toList())
|
||||||
.build();
|
.build();
|
||||||
@ -210,12 +221,12 @@ public class HomeModuleManager {
|
|||||||
return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
|
return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeSceneItem getOutdoorSceneItem() {
|
@Nullable public HomeSceneItem getOutdoorSceneItem() {
|
||||||
return this.outdoor.getSceneItem();
|
return this.outdoor == null ? null : this.outdoor.getSceneItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeSceneItem getIndoorSceneItem() {
|
@Nullable public HomeSceneItem getIndoorSceneItem() {
|
||||||
return this.indoor.getSceneItem();
|
return this.indoor == null ? null : this.indoor.getSceneItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSetModule() {
|
public void onSetModule() {
|
||||||
@ -223,8 +234,14 @@ public class HomeModuleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.outdoor.addEntities(this.getOutdoorSceneItem().getAnimals(this.outdoor));
|
if (this.outdoor != null) {
|
||||||
this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
|
this.outdoor.addEntities(this.getOutdoorSceneItem().getAnimals(this.outdoor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indoor != null) {
|
||||||
|
this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
|
||||||
|
}
|
||||||
|
|
||||||
this.fireAllAvatarRewardEvents();
|
this.fireAllAvatarRewardEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +250,12 @@ public class HomeModuleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.outdoor.getEntities().clear();
|
if (this.outdoor != null) {
|
||||||
this.indoor.getEntities().clear();
|
this.outdoor.getEntities().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indoor != null) {
|
||||||
|
this.indoor.getEntities().clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package emu.grasscutter.game.home;
|
|||||||
import emu.grasscutter.data.GameData;
|
import emu.grasscutter.data.GameData;
|
||||||
import emu.grasscutter.game.entity.EntityTeam;
|
import emu.grasscutter.game.entity.EntityTeam;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.game.world.Scene;
|
|
||||||
import emu.grasscutter.game.world.World;
|
import emu.grasscutter.game.world.World;
|
||||||
import emu.grasscutter.net.packet.BasePacket;
|
import emu.grasscutter.net.packet.BasePacket;
|
||||||
import emu.grasscutter.net.proto.ChatInfoOuterClass;
|
import emu.grasscutter.net.proto.ChatInfoOuterClass;
|
||||||
@ -13,6 +12,7 @@ import emu.grasscutter.server.packet.send.PacketPlayerChatNotify;
|
|||||||
import emu.grasscutter.server.packet.send.PacketPlayerGameTimeNotify;
|
import emu.grasscutter.server.packet.send.PacketPlayerGameTimeNotify;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -66,7 +66,7 @@ public class HomeWorld extends World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRealmIdValid() {
|
public boolean isRealmIdValid() {
|
||||||
return this.getHost().getCurrentRealmId() > 0;
|
return this.getSceneById(this.getHost().getCurrentRealmId() + 2000) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -147,11 +147,13 @@ public class HomeWorld extends World {
|
|||||||
player.setWorld(null);
|
player.setWorld(null);
|
||||||
|
|
||||||
// Remove from scene
|
// Remove from scene
|
||||||
Scene scene = this.getSceneById(player.getSceneId());
|
var scene = this.getSceneById(player.getSceneId());
|
||||||
scene.removePlayer(player);
|
if (scene != null) {
|
||||||
|
scene.removePlayer(player);
|
||||||
|
}
|
||||||
|
|
||||||
// Info packet for other players
|
// Info packet for other players
|
||||||
if (this.getPlayers().size() > 0) {
|
if (!this.getPlayers().isEmpty()) {
|
||||||
this.updatePlayerInfos(player);
|
this.updatePlayerInfos(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +169,7 @@ public class HomeWorld extends World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HomeScene getSceneById(int sceneId) {
|
@Nullable public HomeScene getSceneById(int sceneId) {
|
||||||
var scene = this.getScenes().get(sceneId);
|
var scene = this.getScenes().get(sceneId);
|
||||||
if (scene instanceof HomeScene homeScene) {
|
if (scene instanceof HomeScene homeScene) {
|
||||||
return homeScene;
|
return homeScene;
|
||||||
|
@ -139,11 +139,15 @@ public class HomeWorldMPSystem extends BaseGameSystem {
|
|||||||
|
|
||||||
int realmId = 2000 + owner.getCurrentRealmId();
|
int realmId = 2000 + owner.getCurrentRealmId();
|
||||||
var item = targetHome.getHomeSceneItem(realmId);
|
var item = targetHome.getHomeSceneItem(realmId);
|
||||||
|
var scene = world.getSceneById(realmId);
|
||||||
targetHome.save();
|
targetHome.save();
|
||||||
var pos =
|
|
||||||
toSafe
|
Position pos;
|
||||||
? world.getSceneById(realmId).getScriptManager().getConfig().born_pos
|
if (scene != null) {
|
||||||
: item.getBornPos();
|
pos = toSafe ? scene.getScriptManager().getConfig().born_pos : item.getBornPos();
|
||||||
|
} else {
|
||||||
|
pos = item.getBornPos();
|
||||||
|
}
|
||||||
|
|
||||||
if (teleportPoint != 0) {
|
if (teleportPoint != 0) {
|
||||||
var target = item.getTeleportPointPos(teleportPoint);
|
var target = item.getTeleportPointPos(teleportPoint);
|
||||||
|
@ -259,8 +259,14 @@ public class EnergyManager extends BasePlayerManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also reference AvatarSkillData in case the burst gets a different skill ID
|
||||||
|
// when the avatar is in a different state. For example, Wanderer's burst is
|
||||||
|
// 10755 usually but when he floats, it becomes 10753.
|
||||||
|
var skillData = GameData.getAvatarSkillDataMap().get(skillId);
|
||||||
|
|
||||||
// If the cast skill was a burst, consume energy.
|
// If the cast skill was a burst, consume energy.
|
||||||
if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) {
|
if ((avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill())
|
||||||
|
|| (skillData != null && skillData.getCostElemVal() > 0)) {
|
||||||
avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
|
avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setOpenState(int openState, int value, boolean sendNotify) {
|
private void setOpenState(int openState, int value, boolean sendNotify) {
|
||||||
int previousValue = this.player.getOpenStates().getOrDefault(openState, 0);
|
int previousValue = this.player.getOpenStates().getOrDefault(openState, -1 /* non-existent */);
|
||||||
|
|
||||||
if (value != previousValue) {
|
if (value != previousValue) {
|
||||||
this.player.getOpenStates().put(openState, value);
|
this.player.getOpenStates().put(openState, value);
|
||||||
|
@ -29,6 +29,11 @@ public class TowerManager extends BasePlayerManager {
|
|||||||
return this.getTowerData().currentFloorId;
|
return this.getTowerData().currentFloorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** floor number: 1 - 12 * */
|
||||||
|
public int getCurrentFloorNumber() {
|
||||||
|
return GameData.getTowerFloorDataMap().get(getCurrentFloorId()).getFloorIndex();
|
||||||
|
}
|
||||||
|
|
||||||
public int getCurrentLevelId() {
|
public int getCurrentLevelId() {
|
||||||
return this.getTowerData().currentLevelId + this.getTowerData().currentLevel;
|
return this.getTowerData().currentLevelId + this.getTowerData().currentLevel;
|
||||||
}
|
}
|
||||||
@ -40,7 +45,7 @@ public class TowerManager extends BasePlayerManager {
|
|||||||
|
|
||||||
public void onTick() {
|
public void onTick() {
|
||||||
var challenge = player.getScene().getChallenge();
|
var challenge = player.getScene().getChallenge();
|
||||||
if (challenge == null || !challenge.inProgress()) return;
|
if (!inProgress || challenge == null || !challenge.inProgress()) return;
|
||||||
|
|
||||||
// Check star conditions and notify client if any failed.
|
// Check star conditions and notify client if any failed.
|
||||||
int stars = getCurLevelStars();
|
int stars = getCurLevelStars();
|
||||||
@ -93,8 +98,17 @@ public class TowerManager extends BasePlayerManager {
|
|||||||
player.getTeamManager().setupTemporaryTeam(towerTeams);
|
player.getTeamManager().setupTemporaryTeam(towerTeams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TowerLevelData getCurrentTowerLevelDataMap() {
|
||||||
|
return GameData.getTowerLevelDataMap().get(getCurrentLevelId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentMonsterLevel() {
|
||||||
|
// monsterLevel given in TowerLevelExcelConfigData.json is off by one.
|
||||||
|
return getCurrentTowerLevelDataMap().getMonsterLevel() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
public void enterLevel(int enterPointId) {
|
public void enterLevel(int enterPointId) {
|
||||||
var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
|
var levelData = getCurrentTowerLevelDataMap();
|
||||||
|
|
||||||
var dungeonId = levelData.getDungeonId();
|
var dungeonId = levelData.getDungeonId();
|
||||||
|
|
||||||
@ -140,7 +154,7 @@ public class TowerManager extends BasePlayerManager {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
|
var levelData = getCurrentTowerLevelDataMap();
|
||||||
// 0-based indexing. "star" = 0 means checking for 1-star conditions.
|
// 0-based indexing. "star" = 0 means checking for 1-star conditions.
|
||||||
int star;
|
int star;
|
||||||
for (star = 2; star >= 0; star--) {
|
for (star = 2; star >= 0; star--) {
|
||||||
@ -153,8 +167,11 @@ public class TowerManager extends BasePlayerManager {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) {
|
} else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) {
|
||||||
// TODO: Check monolith health
|
var params = levelData.getHpCond(star);
|
||||||
break;
|
var hpPercent = challenge.getGuardEntityHpPercent();
|
||||||
|
if (hpPercent >= params.getMinimumHpPercentage()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Grasscutter.getLogger()
|
Grasscutter.getLogger()
|
||||||
.error(
|
.error(
|
||||||
|
@ -600,7 +600,7 @@ public class Scene {
|
|||||||
// Should be OK to check only player 0,
|
// Should be OK to check only player 0,
|
||||||
// as no other players could enter Tower
|
// as no other players could enter Tower
|
||||||
var towerManager = getPlayers().get(0).getTowerManager();
|
var towerManager = getPlayers().get(0).getTowerManager();
|
||||||
if (towerManager != null) {
|
if (towerManager != null && towerManager.isInProgress()) {
|
||||||
towerManager.onTick();
|
towerManager.onTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,6 +767,19 @@ public class Scene {
|
|||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getLevelForMonster(int configId, int defaultLevel) {
|
||||||
|
if (getDungeonManager() != null) {
|
||||||
|
return getDungeonManager().getLevelForMonster(configId);
|
||||||
|
} else if (getWorld().getWorldLevel() > 0) {
|
||||||
|
var worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel());
|
||||||
|
|
||||||
|
if (worldLevelData != null) {
|
||||||
|
return worldLevelData.getMonsterLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultLevel;
|
||||||
|
}
|
||||||
|
|
||||||
public void checkNpcGroup() {
|
public void checkNpcGroup() {
|
||||||
Set<SceneNpcBornEntry> npcBornEntries = ConcurrentHashMap.newKeySet();
|
Set<SceneNpcBornEntry> npcBornEntries = ConcurrentHashMap.newKeySet();
|
||||||
for (Player player : this.getPlayers()) {
|
for (Player player : this.getPlayers()) {
|
||||||
|
@ -5,14 +5,19 @@ import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportTy
|
|||||||
import emu.grasscutter.Grasscutter;
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.data.GameData;
|
import emu.grasscutter.data.GameData;
|
||||||
import emu.grasscutter.data.excels.dungeon.DungeonData;
|
import emu.grasscutter.data.excels.dungeon.DungeonData;
|
||||||
import emu.grasscutter.game.entity.*;
|
import emu.grasscutter.game.entity.EntityTeam;
|
||||||
|
import emu.grasscutter.game.entity.EntityWorld;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.game.player.Player.SceneLoadState;
|
import emu.grasscutter.game.player.Player.SceneLoadState;
|
||||||
import emu.grasscutter.game.props.*;
|
import emu.grasscutter.game.props.EnterReason;
|
||||||
|
import emu.grasscutter.game.props.EntityIdType;
|
||||||
|
import emu.grasscutter.game.props.PlayerProperty;
|
||||||
|
import emu.grasscutter.game.props.SceneType;
|
||||||
import emu.grasscutter.game.quest.enums.QuestContent;
|
import emu.grasscutter.game.quest.enums.QuestContent;
|
||||||
import emu.grasscutter.game.world.data.TeleportProperties;
|
import emu.grasscutter.game.world.data.TeleportProperties;
|
||||||
import emu.grasscutter.net.packet.BasePacket;
|
import emu.grasscutter.net.packet.BasePacket;
|
||||||
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.*;
|
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHint;
|
||||||
|
import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo.SystemHintType;
|
||||||
import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType;
|
import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType;
|
||||||
import emu.grasscutter.scripts.data.SceneConfig;
|
import emu.grasscutter.scripts.data.SceneConfig;
|
||||||
import emu.grasscutter.server.event.player.PlayerTeleportEvent;
|
import emu.grasscutter.server.event.player.PlayerTeleportEvent;
|
||||||
@ -21,10 +26,20 @@ 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 io.netty.util.concurrent.FastThreadLocalThread;
|
||||||
import it.unimi.dsi.fastutil.ints.*;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
import java.util.*;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
|
||||||
import java.util.concurrent.*;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
import lombok.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.LinkedBlockingDeque;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.val;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class World implements Iterable<Player> {
|
public class World implements Iterable<Player> {
|
||||||
@ -124,7 +139,7 @@ public class World implements Iterable<Player> {
|
|||||||
* @param sceneId The scene ID.
|
* @param sceneId The scene ID.
|
||||||
* @return The scene.
|
* @return The scene.
|
||||||
*/
|
*/
|
||||||
public Scene getSceneById(int sceneId) {
|
@Nullable public Scene getSceneById(int sceneId) {
|
||||||
// Get scene normally
|
// Get scene normally
|
||||||
var scene = this.getScenes().get(sceneId);
|
var scene = this.getScenes().get(sceneId);
|
||||||
if (scene != null) {
|
if (scene != null) {
|
||||||
@ -152,7 +167,7 @@ public class World implements Iterable<Player> {
|
|||||||
* @param idType The entity type.
|
* @param idType The entity type.
|
||||||
* @return The next entity ID.
|
* @return The next entity ID.
|
||||||
*/
|
*/
|
||||||
public int getNextEntityId(EntityIdType idType) {
|
public synchronized int getNextEntityId(EntityIdType idType) {
|
||||||
return (idType.getId() << 24) + ++this.nextEntityId;
|
return (idType.getId() << 24) + ++this.nextEntityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
src/main/java/emu/grasscutter/net/IKcpSession.java
Normal file
27
src/main/java/emu/grasscutter/net/IKcpSession.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package emu.grasscutter.net;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
/** This is most closely related to the previous `KcpTunnel` interface. */
|
||||||
|
public interface IKcpSession {
|
||||||
|
/**
|
||||||
|
* @return The session's unique logger.
|
||||||
|
*/
|
||||||
|
Logger getLogger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The connecting client's address.
|
||||||
|
*/
|
||||||
|
InetSocketAddress getAddress();
|
||||||
|
|
||||||
|
/** Closes the server's connection to the client. */
|
||||||
|
void close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends raw data to the client.
|
||||||
|
*
|
||||||
|
* @param data The data to send. This should not be KCP-encoded.
|
||||||
|
*/
|
||||||
|
void send(byte[] data);
|
||||||
|
}
|
38
src/main/java/emu/grasscutter/net/INetworkTransport.java
Normal file
38
src/main/java/emu/grasscutter/net/INetworkTransport.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package emu.grasscutter.net;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
|
import emu.grasscutter.server.game.GameServer;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
|
public interface INetworkTransport {
|
||||||
|
/**
|
||||||
|
* Waits for the server to be active. This should be used to ensure that the server is ready to
|
||||||
|
* accept connections.
|
||||||
|
*/
|
||||||
|
default GameServer waitForServer() throws InterruptedException {
|
||||||
|
int depth = 0;
|
||||||
|
|
||||||
|
GameServer server;
|
||||||
|
while ((server = Grasscutter.getGameServer()) == null) {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
if (depth++ > 5) {
|
||||||
|
throw new IllegalStateException("Game server is not available!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is invoked when the transport should start listening for incoming connections.
|
||||||
|
*
|
||||||
|
* @param listening The address/port to listen on.
|
||||||
|
*/
|
||||||
|
void start(InetSocketAddress listening);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is invoked when the transport should stop listening for incoming connections. This should
|
||||||
|
* also close all active connections.
|
||||||
|
*/
|
||||||
|
void shutdown();
|
||||||
|
}
|
46
src/main/java/emu/grasscutter/net/impl/KcpSessionImpl.java
Normal file
46
src/main/java/emu/grasscutter/net/impl/KcpSessionImpl.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package emu.grasscutter.net.impl;
|
||||||
|
|
||||||
|
import emu.grasscutter.net.IKcpSession;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import kcp.highway.Ukcp;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the default implementation of a KCP session. It uses {@link Ukcp} as the underlying
|
||||||
|
* wrapper.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class KcpSessionImpl implements IKcpSession {
|
||||||
|
private final Ukcp handle;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
public KcpSessionImpl(Ukcp handle) {
|
||||||
|
this.handle = handle;
|
||||||
|
this.logger = LoggerFactory.getLogger("KcpSession " + handle.getConv());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InetSocketAddress getAddress() {
|
||||||
|
return this.getHandle().user().getRemoteAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
this.getHandle().close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(byte[] data) {
|
||||||
|
var buffer = Unpooled.wrappedBuffer(data);
|
||||||
|
try {
|
||||||
|
this.getHandle().write(buffer);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
this.getLogger().warn("Unable to send packet.", ex);
|
||||||
|
} finally {
|
||||||
|
buffer.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
src/main/java/emu/grasscutter/net/impl/NetworkTransportImpl.java
Normal file
122
src/main/java/emu/grasscutter/net/impl/NetworkTransportImpl.java
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package emu.grasscutter.net.impl;
|
||||||
|
|
||||||
|
import static emu.grasscutter.config.Configuration.GAME_INFO;
|
||||||
|
|
||||||
|
import emu.grasscutter.net.INetworkTransport;
|
||||||
|
import emu.grasscutter.server.game.GameSession;
|
||||||
|
import emu.grasscutter.utils.Utils;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.DefaultEventLoop;
|
||||||
|
import io.netty.channel.EventLoop;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import kcp.highway.ChannelConfig;
|
||||||
|
import kcp.highway.KcpListener;
|
||||||
|
import kcp.highway.KcpServer;
|
||||||
|
import kcp.highway.Ukcp;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of a {@link INetworkTransport}. Uses {@link KcpServer} as the
|
||||||
|
* underlying transport.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class NetworkTransportImpl extends KcpServer implements INetworkTransport {
|
||||||
|
private final EventLoop networkLoop = new DefaultEventLoop();
|
||||||
|
private final ConcurrentHashMap<Ukcp, GameSession> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(InetSocketAddress listening) {
|
||||||
|
var settings = new ChannelConfig();
|
||||||
|
settings.setTimeoutMillis(GAME_INFO.timeout * 1000);
|
||||||
|
settings.nodelay(true, GAME_INFO.kcpInterval, 2, true);
|
||||||
|
settings.setMtu(1400);
|
||||||
|
settings.setSndwnd(256);
|
||||||
|
settings.setRcvwnd(256);
|
||||||
|
settings.setUseConvChannel(true);
|
||||||
|
settings.setAckNoDelay(false);
|
||||||
|
|
||||||
|
this.init(new Listener(), settings, listening);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
this.stop();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.networkLoop.shutdownGracefully();
|
||||||
|
if (!this.networkLoop.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
log.warn("Network loop did not terminate in time.");
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to shutdown network loop.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Listener implements KcpListener {
|
||||||
|
@Override
|
||||||
|
public void onConnected(Ukcp ukcp) {
|
||||||
|
var transport = NetworkTransportImpl.this;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var server = transport.waitForServer();
|
||||||
|
var session = new KcpSessionImpl(ukcp);
|
||||||
|
var gameSession = new GameSession(server, session);
|
||||||
|
|
||||||
|
transport.sessions.put(ukcp, gameSession);
|
||||||
|
gameSession.onConnected();
|
||||||
|
} catch (InterruptedException | IllegalStateException ex) {
|
||||||
|
NetworkTransportImpl.log.warn("Unable to establish connection.", ex);
|
||||||
|
ukcp.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleReceive(ByteBuf byteBuf, Ukcp ukcp) {
|
||||||
|
var transport = NetworkTransportImpl.this;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var session = transport.sessions.get(ukcp);
|
||||||
|
if (session == null) {
|
||||||
|
NetworkTransportImpl.log.debug("Received data from unknown session.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the buffer to avoid reference issues.
|
||||||
|
var data = Utils.byteBufToArray(byteBuf);
|
||||||
|
|
||||||
|
transport.networkLoop.submit(
|
||||||
|
() -> {
|
||||||
|
// Fun fact: if we don't catch exceptions here,
|
||||||
|
// we run the risk of locking the entire network loop.
|
||||||
|
try {
|
||||||
|
session.onReceived(data);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
session.getLogger().warn("Unable to handle received data.", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
NetworkTransportImpl.log.warn("Unable to handle received data.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleException(Throwable throwable, Ukcp ukcp) {
|
||||||
|
NetworkTransportImpl.log.debug("Exception occurred in session.", throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClose(Ukcp ukcp) {
|
||||||
|
var sessions = NetworkTransportImpl.this.sessions;
|
||||||
|
var session = sessions.get(ukcp);
|
||||||
|
if (session == null) {
|
||||||
|
NetworkTransportImpl.log.debug("Received close from unknown session.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.onDisconnected();
|
||||||
|
sessions.remove(ukcp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,7 @@ public class SceneScriptManager {
|
|||||||
/** 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;
|
||||||
@ -76,6 +77,7 @@ public class SceneScriptManager {
|
|||||||
public SceneScriptManager(Scene scene) {
|
public SceneScriptManager(Scene scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.currentTriggers = new ConcurrentHashMap<>();
|
this.currentTriggers = new ConcurrentHashMap<>();
|
||||||
|
this.ongoingTriggers = ConcurrentHashMap.newKeySet();
|
||||||
this.triggersByGroupScene = new ConcurrentHashMap<>();
|
this.triggersByGroupScene = new ConcurrentHashMap<>();
|
||||||
this.activeGroupTimers = new ConcurrentHashMap<>();
|
this.activeGroupTimers = new ConcurrentHashMap<>();
|
||||||
this.triggerInvocations = new ConcurrentHashMap<>();
|
this.triggerInvocations = new ConcurrentHashMap<>();
|
||||||
@ -264,6 +266,15 @@ public class SceneScriptManager {
|
|||||||
|
|
||||||
this.addGroupSuite(groupInstance, suiteData, entitiesAdded);
|
this.addGroupSuite(groupInstance, suiteData, entitiesAdded);
|
||||||
|
|
||||||
|
// refreshGroup may be called by a trigger.
|
||||||
|
// If that trigger has been refreshed, ensure it does not get
|
||||||
|
// deregistered anyway when the trigger completes its invocation.
|
||||||
|
for (var triggerSet : currentTriggers.values()) {
|
||||||
|
var toSave = new HashSet<SceneTrigger>(triggerSet);
|
||||||
|
toSave.retainAll(ongoingTriggers);
|
||||||
|
toSave.forEach(t -> t.setPreserved(true));
|
||||||
|
}
|
||||||
|
|
||||||
// Refesh variables here
|
// Refesh variables here
|
||||||
group.variables.forEach(
|
group.variables.forEach(
|
||||||
variable -> {
|
variable -> {
|
||||||
@ -496,6 +507,11 @@ public class SceneScriptManager {
|
|||||||
.forEach(
|
.forEach(
|
||||||
block -> {
|
block -> {
|
||||||
block.load(sceneId, meta.context);
|
block.load(sceneId, meta.context);
|
||||||
|
if (block.groups == null) {
|
||||||
|
Grasscutter.getLogger().error("block.groups null for block {}", block.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
block.groups.values().stream()
|
block.groups.values().stream()
|
||||||
.filter(g -> !g.dynamic_load)
|
.filter(g -> !g.dynamic_load)
|
||||||
.forEach(
|
.forEach(
|
||||||
@ -925,6 +941,7 @@ public class SceneScriptManager {
|
|||||||
|
|
||||||
private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
|
private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
|
||||||
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time
|
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time
|
||||||
|
ongoingTriggers.add(trigger);
|
||||||
var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params);
|
var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params);
|
||||||
var invocationsCounter = triggerInvocations.get(trigger.getName());
|
var invocationsCounter = triggerInvocations.get(trigger.getName());
|
||||||
var invocations = invocationsCounter.incrementAndGet();
|
var invocations = invocationsCounter.incrementAndGet();
|
||||||
@ -956,11 +973,15 @@ public class SceneScriptManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// always deregister on error, otherwise only if the count is reached
|
// always deregister on error, otherwise only if the count is reached
|
||||||
if (ret.isboolean() && !ret.checkboolean()
|
// or the trigger should be preserved after a RefreshGroup call
|
||||||
|
if (trigger.isPreserved()) {
|
||||||
|
trigger.setPreserved(false);
|
||||||
|
} else if (ret.isboolean() && !ret.checkboolean()
|
||||||
|| ret.isint() && ret.checkint() != 0
|
|| ret.isint() && ret.checkint() != 0
|
||||||
|| trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) {
|
|| trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) {
|
||||||
deregisterTrigger(trigger);
|
deregisterTrigger(trigger);
|
||||||
}
|
}
|
||||||
|
ongoingTriggers.remove(trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
|
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
|
||||||
@ -1053,18 +1074,7 @@ public class SceneScriptManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate level
|
// Calculate level
|
||||||
int level = monster.level;
|
int level = getScene().getLevelForMonster(monster.config_id, monster.level);
|
||||||
|
|
||||||
if (getScene().getDungeonManager() != null) {
|
|
||||||
level = getScene().getDungeonManager().getLevelForMonster(monster.config_id);
|
|
||||||
} else if (getScene().getWorld().getWorldLevel() > 0) {
|
|
||||||
var worldLevelData =
|
|
||||||
GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel());
|
|
||||||
|
|
||||||
if (worldLevelData != null) {
|
|
||||||
level = worldLevelData.getMonsterLevel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn mob
|
// Spawn mob
|
||||||
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level);
|
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level);
|
||||||
@ -1104,6 +1114,19 @@ public class SceneScriptManager {
|
|||||||
return meta.sceneBlockIndex;
|
return meta.sceneBlockIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeMonstersInGroup(SceneGroup group) {
|
||||||
|
var configSet =
|
||||||
|
group.monsters.values().stream().map(m -> m.config_id).collect(Collectors.toSet());
|
||||||
|
var toRemove =
|
||||||
|
getScene().getEntities().values().stream()
|
||||||
|
.filter(e -> e instanceof EntityMonster)
|
||||||
|
.filter(e -> e.getGroupId() == group.id)
|
||||||
|
.filter(e -> configSet.contains(e.getConfigId()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
|
||||||
|
}
|
||||||
|
|
||||||
public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
|
public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
|
||||||
var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet());
|
var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet());
|
||||||
var toRemove =
|
var toRemove =
|
||||||
|
@ -201,7 +201,7 @@ public class ScriptLib {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
||||||
if (towerManager.isInProgress()) {
|
if (towerManager.isInProgress() && towerManager.getCurrentTimeLimit() > 0) {
|
||||||
// Tower scripts call ActiveChallenge twice in mirror stages.
|
// Tower scripts call ActiveChallenge twice in mirror stages.
|
||||||
// The second call provides the time _taken_ in the first stage,
|
// The second call provides the time _taken_ in the first stage,
|
||||||
// not the actual time limit for the challenge.
|
// not the actual time limit for the challenge.
|
||||||
|
@ -17,6 +17,7 @@ public final class SceneTrigger {
|
|||||||
private String tag;
|
private String tag;
|
||||||
|
|
||||||
public transient SceneGroup currentGroup;
|
public transient SceneGroup currentGroup;
|
||||||
|
private boolean preserved;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
|
@ -62,7 +62,11 @@ public final class ScriptMonsterTideService {
|
|||||||
|
|
||||||
public SceneMonster getNextMonster() {
|
public SceneMonster getNextMonster() {
|
||||||
var nextId = this.monsterConfigOrders.poll();
|
var nextId = this.monsterConfigOrders.poll();
|
||||||
if (currentGroup.monsters.containsKey(nextId)) {
|
if (nextId == null) {
|
||||||
|
// AutoMonsterTide has been called with fewer monster config IDs than the total tide count.
|
||||||
|
// Get last config ID from the list, then.
|
||||||
|
return currentGroup.monsters.get(monsterConfigIds.get(monsterConfigIds.size() - 1));
|
||||||
|
} else if (currentGroup.monsters.containsKey(nextId)) {
|
||||||
return currentGroup.monsters.get(nextId);
|
return currentGroup.monsters.get(nextId);
|
||||||
}
|
}
|
||||||
// TODO some monster config_id do not exist in groups, so temporarily set it to the first
|
// TODO some monster config_id do not exist in groups, so temporarily set it to the first
|
||||||
|
@ -32,6 +32,8 @@ import emu.grasscutter.game.talk.TalkSystem;
|
|||||||
import emu.grasscutter.game.tower.TowerSystem;
|
import emu.grasscutter.game.tower.TowerSystem;
|
||||||
import emu.grasscutter.game.world.World;
|
import emu.grasscutter.game.world.World;
|
||||||
import emu.grasscutter.game.world.WorldDataSystem;
|
import emu.grasscutter.game.world.WorldDataSystem;
|
||||||
|
import emu.grasscutter.net.INetworkTransport;
|
||||||
|
import emu.grasscutter.net.impl.NetworkTransportImpl;
|
||||||
import emu.grasscutter.net.packet.PacketHandler;
|
import emu.grasscutter.net.packet.PacketHandler;
|
||||||
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
|
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
|
||||||
import emu.grasscutter.server.dispatch.DispatchClient;
|
import emu.grasscutter.server.dispatch.DispatchClient;
|
||||||
@ -47,14 +49,20 @@ import java.net.*;
|
|||||||
import java.time.*;
|
import java.time.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import kcp.highway.*;
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.*;
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public final class GameServer extends KcpServer implements Iterable<Player> {
|
@Slf4j
|
||||||
|
public final class GameServer implements Iterable<Player> {
|
||||||
|
/** This can be set by plugins to change the network transport implementation. */
|
||||||
|
@Setter private static Class<? extends INetworkTransport> transport = NetworkTransportImpl.class;
|
||||||
|
|
||||||
// Game server base
|
// Game server base
|
||||||
private final InetSocketAddress address;
|
private final InetSocketAddress address;
|
||||||
|
private final INetworkTransport netTransport;
|
||||||
|
|
||||||
private final GameServerPacketHandler packetHandler;
|
private final GameServerPacketHandler packetHandler;
|
||||||
private final Map<Integer, Player> players;
|
private final Map<Integer, Player> players;
|
||||||
private final Set<World> worlds;
|
private final Set<World> worlds;
|
||||||
@ -106,6 +114,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
|||||||
this.taskMap = null;
|
this.taskMap = null;
|
||||||
|
|
||||||
this.address = null;
|
this.address = null;
|
||||||
|
this.netTransport = null;
|
||||||
this.packetHandler = null;
|
this.packetHandler = null;
|
||||||
this.dispatchClient = null;
|
this.dispatchClient = null;
|
||||||
this.players = null;
|
this.players = null;
|
||||||
@ -131,16 +140,18 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelConfig = new ChannelConfig();
|
// Create the network transport.
|
||||||
channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true);
|
INetworkTransport transport;
|
||||||
channelConfig.setMtu(1400);
|
try {
|
||||||
channelConfig.setSndwnd(256);
|
transport = GameServer.transport.getDeclaredConstructor().newInstance();
|
||||||
channelConfig.setRcvwnd(256);
|
} catch (Exception ex) {
|
||||||
channelConfig.setTimeoutMillis(30 * 1000); // 30s
|
log.error("Failed to create network transport.", ex);
|
||||||
channelConfig.setUseConvChannel(true);
|
transport = new NetworkTransportImpl();
|
||||||
channelConfig.setAckNoDelay(false);
|
}
|
||||||
|
|
||||||
this.init(GameSessionManager.getListener(), channelConfig, address);
|
// Initialize the transport.
|
||||||
|
this.netTransport = transport;
|
||||||
|
this.netTransport.start(this.address = address);
|
||||||
|
|
||||||
EnergyManager.initialize();
|
EnergyManager.initialize();
|
||||||
StaminaManager.initialize();
|
StaminaManager.initialize();
|
||||||
@ -149,7 +160,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
|||||||
CombineManger.initialize();
|
CombineManger.initialize();
|
||||||
|
|
||||||
// Game Server base
|
// Game Server base
|
||||||
this.address = address;
|
|
||||||
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
|
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
|
||||||
this.dispatchClient = new DispatchClient(GameServer.getDispatchUrl());
|
this.dispatchClient = new DispatchClient(GameServer.getDispatchUrl());
|
||||||
this.players = new ConcurrentHashMap<>();
|
this.players = new ConcurrentHashMap<>();
|
||||||
@ -184,7 +194,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
|||||||
|
|
||||||
private static InetSocketAddress getAdapterInetSocketAddress() {
|
private static InetSocketAddress getAdapterInetSocketAddress() {
|
||||||
InetSocketAddress inetSocketAddress;
|
InetSocketAddress inetSocketAddress;
|
||||||
if (GAME_INFO.bindAddress.equals("")) {
|
if (GAME_INFO.bindAddress.isEmpty()) {
|
||||||
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort);
|
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort);
|
||||||
} else {
|
} else {
|
||||||
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort);
|
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort);
|
||||||
@ -353,19 +363,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
|||||||
this.getWorlds().forEach(World::save);
|
this.getWorlds().forEach(World::save);
|
||||||
|
|
||||||
Utils.sleep(1000L); // Wait 1 second for operations to finish.
|
Utils.sleep(1000L); // Wait 1 second for operations to finish.
|
||||||
this.stop(); // Stop the server.
|
|
||||||
|
|
||||||
try {
|
|
||||||
var threadPool = GameSessionManager.getLogicThread();
|
|
||||||
|
|
||||||
// Shutdown network thread.
|
|
||||||
threadPool.shutdownGracefully();
|
|
||||||
// Wait for the network thread to finish.
|
|
||||||
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
||||||
Grasscutter.getLogger().error("Logic thread did not terminate!");
|
|
||||||
}
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull @Override
|
@NotNull @Override
|
||||||
|
@ -7,18 +7,18 @@ import emu.grasscutter.Grasscutter;
|
|||||||
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||||
import emu.grasscutter.game.Account;
|
import emu.grasscutter.game.Account;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
|
import emu.grasscutter.net.IKcpSession;
|
||||||
import emu.grasscutter.net.packet.*;
|
import emu.grasscutter.net.packet.*;
|
||||||
import emu.grasscutter.server.event.game.SendPacketEvent;
|
import emu.grasscutter.server.event.game.SendPacketEvent;
|
||||||
import emu.grasscutter.utils.*;
|
import emu.grasscutter.utils.*;
|
||||||
import io.netty.buffer.*;
|
import io.netty.buffer.*;
|
||||||
import java.io.File;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.file.Path;
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
public class GameSession implements GameSessionManager.KcpChannel {
|
public class GameSession implements IGameSession {
|
||||||
private final GameServer server;
|
@Getter private final GameServer server;
|
||||||
private GameSessionManager.KcpTunnel tunnel;
|
private IKcpSession session;
|
||||||
|
|
||||||
@Getter @Setter private Account account;
|
@Getter @Setter private Account account;
|
||||||
@Getter private Player player;
|
@Getter private Player player;
|
||||||
@ -33,8 +33,10 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
@Getter private long lastPingTime;
|
@Getter private long lastPingTime;
|
||||||
private int lastClientSeq = 10;
|
private int lastClientSeq = 10;
|
||||||
|
|
||||||
public GameSession(GameServer server) {
|
public GameSession(GameServer server, IKcpSession session) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
this.session = session;
|
||||||
|
|
||||||
this.state = SessionState.WAITING_FOR_TOKEN;
|
this.state = SessionState.WAITING_FOR_TOKEN;
|
||||||
this.lastPingTime = System.currentTimeMillis();
|
this.lastPingTime = System.currentTimeMillis();
|
||||||
|
|
||||||
@ -44,24 +46,12 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameServer getServer() {
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InetSocketAddress getAddress() {
|
public InetSocketAddress getAddress() {
|
||||||
try {
|
return this.session.getAddress();
|
||||||
return tunnel.getAddress();
|
|
||||||
} catch (Throwable ignore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean useSecretKey() {
|
public Logger getLogger() {
|
||||||
return useSecretKey;
|
return this.session.getLogger();
|
||||||
}
|
|
||||||
|
|
||||||
public String getAccountId() {
|
|
||||||
return this.getAccount().getId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void setPlayer(Player player) {
|
public synchronized void setPlayer(Player player) {
|
||||||
@ -83,30 +73,17 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
return ++lastClientSeq;
|
return ++lastClientSeq;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void replayPacket(int opcode, String name) {
|
|
||||||
Path filePath = FileUtils.getPluginPath(name);
|
|
||||||
File p = filePath.toFile();
|
|
||||||
|
|
||||||
if (!p.exists()) return;
|
|
||||||
|
|
||||||
byte[] packet = FileUtils.read(p);
|
|
||||||
|
|
||||||
BasePacket basePacket = new BasePacket(opcode);
|
|
||||||
basePacket.setData(packet);
|
|
||||||
|
|
||||||
send(basePacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void logPacket(String sendOrRecv, int opcode, byte[] payload) {
|
public void logPacket(String sendOrRecv, int opcode, byte[] payload) {
|
||||||
Grasscutter.getLogger()
|
this.session
|
||||||
.info(sendOrRecv + ": " + PacketOpcodesUtils.getOpcodeName(opcode) + " (" + opcode + ")");
|
.getLogger()
|
||||||
|
.info("{}: {} ({})", sendOrRecv, PacketOpcodesUtils.getOpcodeName(opcode), opcode);
|
||||||
if (GAME_INFO.isShowPacketPayload) System.out.println(Utils.bytesToHex(payload));
|
if (GAME_INFO.isShowPacketPayload) System.out.println(Utils.bytesToHex(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void send(BasePacket packet) {
|
public void send(BasePacket packet) {
|
||||||
// Test
|
// Test
|
||||||
if (packet.getOpcode() <= 0) {
|
if (packet.getOpcode() <= 0) {
|
||||||
Grasscutter.getLogger().warn("Tried to send packet with missing cmd id!");
|
this.session.getLogger().warn("Attempted to send packet with unknown ID!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,28 +123,24 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
if (packet.shouldEncrypt) {
|
if (packet.shouldEncrypt) {
|
||||||
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
|
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
|
||||||
}
|
}
|
||||||
tunnel.writeData(bytes);
|
this.session.send(bytes);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ex) {
|
||||||
Grasscutter.getLogger().debug("Unable to send packet to client.");
|
this.session.getLogger().debug("Unable to send packet to client.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnected(GameSessionManager.KcpTunnel tunnel) {
|
public void onConnected() {
|
||||||
this.tunnel = tunnel;
|
|
||||||
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
|
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleReceive(byte[] bytes) {
|
public void onReceived(byte[] bytes) {
|
||||||
// Decrypt and turn back into a packet
|
// Decrypt and turn back into a packet
|
||||||
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
|
Crypto.xor(bytes, this.useSecretKey ? this.encryptKey : Crypto.DISPATCH_KEY);
|
||||||
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
|
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
|
||||||
|
|
||||||
// Log
|
|
||||||
// logPacket(packet);
|
|
||||||
// Handle
|
|
||||||
try {
|
try {
|
||||||
boolean allDebug = GAME_INFO.logPackets == ServerDebugMode.ALL;
|
boolean allDebug = GAME_INFO.logPackets == ServerDebugMode.ALL;
|
||||||
while (packet.readableBytes() > 0) {
|
while (packet.readableBytes() > 0) {
|
||||||
@ -179,11 +152,13 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
int const1 = packet.readShort();
|
int const1 = packet.readShort();
|
||||||
if (const1 != 17767) {
|
if (const1 != 17767) {
|
||||||
if (allDebug) {
|
if (allDebug) {
|
||||||
Grasscutter.getLogger()
|
this.session
|
||||||
.error("Bad Data Package Received: got {} ,expect 17767", const1);
|
.getLogger()
|
||||||
|
.error("Invalid packet header received: got {}, expected 17767", const1);
|
||||||
}
|
}
|
||||||
return; // Bad packet
|
return; // Bad packet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
int opcode = packet.readShort();
|
int opcode = packet.readShort();
|
||||||
int headerLength = packet.readShort();
|
int headerLength = packet.readShort();
|
||||||
@ -197,8 +172,9 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
int const2 = packet.readShort();
|
int const2 = packet.readShort();
|
||||||
if (const2 != -30293) {
|
if (const2 != -30293) {
|
||||||
if (allDebug) {
|
if (allDebug) {
|
||||||
Grasscutter.getLogger()
|
this.session
|
||||||
.error("Bad Data Package Received: got {} ,expect -30293", const2);
|
.getLogger()
|
||||||
|
.error("Invalid packet footer received: got {}, expected -30293", const2);
|
||||||
}
|
}
|
||||||
return; // Bad packet
|
return; // Bad packet
|
||||||
}
|
}
|
||||||
@ -226,16 +202,15 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
// Handle
|
// Handle
|
||||||
getServer().getPacketHandler().handle(this, opcode, header, payload);
|
getServer().getPacketHandler().handle(this, opcode, header, payload);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception ex) {
|
||||||
e.printStackTrace();
|
this.session.getLogger().warn("Unable to process packet.", ex);
|
||||||
} finally {
|
} finally {
|
||||||
// byteBuf.release(); //Needn't
|
|
||||||
packet.release();
|
packet.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleClose() {
|
public void onDisconnected() {
|
||||||
setState(SessionState.INACTIVE);
|
setState(SessionState.INACTIVE);
|
||||||
// send disconnection pack in case of reconnection
|
// send disconnection pack in case of reconnection
|
||||||
Grasscutter.getLogger()
|
Grasscutter.getLogger()
|
||||||
@ -247,19 +222,20 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
|||||||
player.onLogout();
|
player.onLogout();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
|
this.send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify));
|
||||||
} catch (Throwable ignore) {
|
} catch (Throwable ex) {
|
||||||
Grasscutter.getLogger().warn("closing {} error", getAddress().getAddress().getHostAddress());
|
this.session.getLogger().warn("Failed to disconnect client.", ex);
|
||||||
}
|
}
|
||||||
tunnel = null;
|
|
||||||
|
this.session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
tunnel.close();
|
this.session.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return getState() == SessionState.ACTIVE;
|
return this.getState() == SessionState.ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SessionState {
|
public enum SessionState {
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
package emu.grasscutter.server.game;
|
|
||||||
|
|
||||||
import emu.grasscutter.Grasscutter;
|
|
||||||
import emu.grasscutter.utils.Utils;
|
|
||||||
import io.netty.buffer.*;
|
|
||||||
import io.netty.channel.DefaultEventLoop;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import kcp.highway.*;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public class GameSessionManager {
|
|
||||||
@Getter private static final DefaultEventLoop logicThread = new DefaultEventLoop();
|
|
||||||
private static final ConcurrentHashMap<Ukcp, GameSession> sessions = new ConcurrentHashMap<>();
|
|
||||||
private static final KcpListener listener =
|
|
||||||
new KcpListener() {
|
|
||||||
@Override
|
|
||||||
public void onConnected(Ukcp ukcp) {
|
|
||||||
int times = 0;
|
|
||||||
GameServer server = Grasscutter.getGameServer();
|
|
||||||
while (server == null) { // Waiting server to establish
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
ukcp.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (times++ > 5) {
|
|
||||||
Grasscutter.getLogger().error("Service is not available!");
|
|
||||||
ukcp.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
server = Grasscutter.getGameServer();
|
|
||||||
}
|
|
||||||
GameSession conversation = new GameSession(server);
|
|
||||||
conversation.onConnected(
|
|
||||||
new KcpTunnel() {
|
|
||||||
@Override
|
|
||||||
public InetSocketAddress getAddress() {
|
|
||||||
return ukcp.user().getRemoteAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeData(byte[] bytes) {
|
|
||||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
|
|
||||||
ukcp.write(buf);
|
|
||||||
buf.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
ukcp.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSrtt() {
|
|
||||||
return ukcp.srtt();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sessions.put(ukcp, conversation);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleReceive(ByteBuf buf, Ukcp kcp) {
|
|
||||||
var byteData = Utils.byteBufToArray(buf);
|
|
||||||
logicThread.execute(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
var conversation = sessions.get(kcp);
|
|
||||||
if (conversation != null) {
|
|
||||||
conversation.handleReceive(byteData);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleException(Throwable ex, Ukcp ukcp) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleClose(Ukcp ukcp) {
|
|
||||||
GameSession conversation = sessions.get(ukcp);
|
|
||||||
if (conversation != null) {
|
|
||||||
conversation.handleClose();
|
|
||||||
sessions.remove(ukcp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static KcpListener getListener() {
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface KcpTunnel {
|
|
||||||
InetSocketAddress getAddress();
|
|
||||||
|
|
||||||
void writeData(byte[] bytes);
|
|
||||||
|
|
||||||
void close();
|
|
||||||
|
|
||||||
int getSrtt();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KcpChannel {
|
|
||||||
void onConnected(KcpTunnel tunnel);
|
|
||||||
|
|
||||||
void handleClose();
|
|
||||||
|
|
||||||
void handleReceive(byte[] bytes);
|
|
||||||
}
|
|
||||||
}
|
|
20
src/main/java/emu/grasscutter/server/game/IGameSession.java
Normal file
20
src/main/java/emu/grasscutter/server/game/IGameSession.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package emu.grasscutter.server.game;
|
||||||
|
|
||||||
|
public interface IGameSession {
|
||||||
|
/**
|
||||||
|
* Invoked when the server establishes a connection to the client.
|
||||||
|
*
|
||||||
|
* <p>This is invoked after the KCP handshake is completed.
|
||||||
|
*/
|
||||||
|
void onConnected();
|
||||||
|
|
||||||
|
/** Invoked when the server loses connection to the client. */
|
||||||
|
void onDisconnected();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the server receives data from the client.
|
||||||
|
*
|
||||||
|
* @param data The raw data (not KCP-encoded) received from the client.
|
||||||
|
*/
|
||||||
|
void onReceived(byte[] data);
|
||||||
|
}
|
@ -218,6 +218,8 @@ public final class HttpServer {
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<img src="https://http.cat/404" />
|
<img src="https://http.cat/404" />
|
||||||
|
<h1>Grasscutter cannot find the route you're trying to access.</h1>
|
||||||
|
<p>Your proxy is active, so if you're trying to download something close the game/stop the proxy.</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
""");
|
""");
|
||||||
|
@ -25,8 +25,13 @@ public final class HandbookHandler implements Router {
|
|||||||
* found.
|
* found.
|
||||||
*/
|
*/
|
||||||
public HandbookHandler() {
|
public HandbookHandler() {
|
||||||
|
if (!HANDBOOK.enable) {
|
||||||
|
this.serve = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.handbook = new String(FileUtils.readResource("/html/handbook.html"));
|
this.handbook = new String(FileUtils.readResource("/html/handbook.html"));
|
||||||
this.serve = HANDBOOK.enable && this.handbook.length() > 0;
|
this.serve = !this.handbook.isEmpty();
|
||||||
|
|
||||||
var server = HANDBOOK.server;
|
var server = HANDBOOK.server;
|
||||||
if (this.serve && server.enforced) {
|
if (this.serve && server.enforced) {
|
||||||
|
@ -21,7 +21,6 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
|
|||||||
|
|
||||||
// Handle skill notify in other managers.
|
// Handle skill notify in other managers.
|
||||||
player.getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId);
|
player.getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId);
|
||||||
player.getEnergyManager().handleEvtDoSkillSuccNotify(session, skillId, casterId);
|
|
||||||
player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_SKILL, skillId);
|
player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_SKILL, skillId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package emu.grasscutter.server.packet.recv;
|
package emu.grasscutter.server.packet.recv;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.net.packet.Opcodes;
|
import emu.grasscutter.net.packet.Opcodes;
|
||||||
import emu.grasscutter.net.packet.PacketHandler;
|
import emu.grasscutter.net.packet.PacketHandler;
|
||||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||||
import emu.grasscutter.net.proto.HomeChangeModuleReqOuterClass;
|
import emu.grasscutter.net.proto.HomeChangeModuleReqOuterClass.HomeChangeModuleReq;
|
||||||
|
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||||
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
|
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
|
||||||
import emu.grasscutter.server.game.GameSession;
|
import emu.grasscutter.server.game.GameSession;
|
||||||
import emu.grasscutter.server.packet.send.PacketHomeAvatarTalkFinishInfoNotify;
|
import emu.grasscutter.server.packet.send.PacketHomeAvatarTalkFinishInfoNotify;
|
||||||
@ -16,12 +18,20 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||||
HomeChangeModuleReqOuterClass.HomeChangeModuleReq req =
|
var req = HomeChangeModuleReq.parseFrom(payload);
|
||||||
HomeChangeModuleReqOuterClass.HomeChangeModuleReq.parseFrom(payload);
|
|
||||||
|
|
||||||
var homeWorld = session.getPlayer().getCurHomeWorld();
|
var homeWorld = session.getPlayer().getCurHomeWorld();
|
||||||
if (!homeWorld.getGuests().isEmpty()) {
|
if (!homeWorld.getGuests().isEmpty()) {
|
||||||
session.send(new PacketHomeChangeModuleRsp());
|
session.send(new PacketHomeChangeModuleRsp(Retcode.RET_HOME_HAS_GUEST));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int realmId = 2000 + req.getTargetModuleId();
|
||||||
|
var scene = homeWorld.getSceneById(realmId);
|
||||||
|
|
||||||
|
if (scene == null) {
|
||||||
|
Grasscutter.getLogger().warn("scene == null! Changing module will fail.");
|
||||||
|
session.send(new PacketHomeChangeModuleRsp(Retcode.RET_INVALID_SCENE_ID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,8 +41,6 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
|
|||||||
session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer()));
|
session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer()));
|
||||||
session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
|
session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
|
||||||
|
|
||||||
int realmId = 2000 + req.getTargetModuleId();
|
|
||||||
var scene = homeWorld.getSceneById(realmId);
|
|
||||||
var pos = scene.getScriptManager().getConfig().born_pos;
|
var pos = scene.getScriptManager().getConfig().born_pos;
|
||||||
|
|
||||||
homeWorld.transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);
|
homeWorld.transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);
|
||||||
|
@ -34,8 +34,7 @@ public class HandlerHomeSceneJumpReq extends PacketHandler {
|
|||||||
pos = home.getSceneMap().get(realmId).getBornPos();
|
pos = home.getSceneMap().get(realmId).getBornPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
world.transferPlayerToScene(
|
world.transferPlayerToScene(session.getPlayer(), scene.getId(), pos);
|
||||||
session.getPlayer(), req.getIsEnterRoomScene() ? homeScene.getRoomSceneId() : realmId, pos);
|
|
||||||
|
|
||||||
session.send(new PacketHomeSceneJumpRsp(req.getIsEnterRoomScene()));
|
session.send(new PacketHomeSceneJumpRsp(req.getIsEnterRoomScene()));
|
||||||
}
|
}
|
||||||
|
@ -2,28 +2,23 @@ package emu.grasscutter.server.packet.send;
|
|||||||
|
|
||||||
import emu.grasscutter.net.packet.BasePacket;
|
import emu.grasscutter.net.packet.BasePacket;
|
||||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||||
import emu.grasscutter.net.proto.HomeChangeModuleRspOuterClass;
|
import emu.grasscutter.net.proto.HomeChangeModuleRspOuterClass.HomeChangeModuleRsp;
|
||||||
import emu.grasscutter.net.proto.RetcodeOuterClass;
|
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||||
|
|
||||||
public class PacketHomeChangeModuleRsp extends BasePacket {
|
public class PacketHomeChangeModuleRsp extends BasePacket {
|
||||||
|
|
||||||
public PacketHomeChangeModuleRsp(int targetModuleId) {
|
public PacketHomeChangeModuleRsp(int targetModuleId) {
|
||||||
super(PacketOpcodes.HomeChangeModuleRsp);
|
super(PacketOpcodes.HomeChangeModuleRsp);
|
||||||
|
|
||||||
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp proto =
|
var proto =
|
||||||
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp.newBuilder()
|
HomeChangeModuleRsp.newBuilder().setRetcode(0).setTargetModuleId(targetModuleId).build();
|
||||||
.setRetcode(0)
|
|
||||||
.setTargetModuleId(targetModuleId)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
this.setData(proto);
|
this.setData(proto);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PacketHomeChangeModuleRsp() {
|
public PacketHomeChangeModuleRsp(Retcode retcode) {
|
||||||
super(PacketOpcodes.HomeChangeModuleRsp);
|
super(PacketOpcodes.HomeChangeModuleRsp);
|
||||||
|
|
||||||
this.setData(
|
this.setData(HomeChangeModuleRsp.newBuilder().setRetcode(retcode.getNumber()));
|
||||||
HomeChangeModuleRspOuterClass.HomeChangeModuleRsp.newBuilder()
|
|
||||||
.setRetcode(RetcodeOuterClass.Retcode.RET_HOME_HAS_GUEST_VALUE));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package emu.grasscutter.server.packet.send;
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.game.home.HomeBlockItem;
|
import emu.grasscutter.game.home.HomeBlockItem;
|
||||||
import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
|
import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.net.packet.*;
|
import emu.grasscutter.net.packet.BasePacket;
|
||||||
import emu.grasscutter.net.proto.*;
|
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||||
|
import emu.grasscutter.net.proto.HomeMarkPointNotifyOuterClass.HomeMarkPointNotify;
|
||||||
|
import emu.grasscutter.net.proto.HomeMarkPointSceneDataOuterClass.HomeMarkPointSceneData;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -13,17 +16,24 @@ public class PacketHomeMarkPointNotify extends BasePacket {
|
|||||||
public PacketHomeMarkPointNotify(Player player) {
|
public PacketHomeMarkPointNotify(Player player) {
|
||||||
super(PacketOpcodes.HomeMarkPointNotify);
|
super(PacketOpcodes.HomeMarkPointNotify);
|
||||||
|
|
||||||
var proto = HomeMarkPointNotifyOuterClass.HomeMarkPointNotify.newBuilder();
|
var proto = HomeMarkPointNotify.newBuilder();
|
||||||
var world = player.getCurHomeWorld();
|
var world = player.getCurHomeWorld();
|
||||||
var owner = world.getHost();
|
var owner = world.getHost();
|
||||||
var home = world.getHome();
|
var home = world.getHome();
|
||||||
|
|
||||||
if (owner.getRealmList() == null) {
|
if (owner.getRealmList() == null || owner.getRealmList().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// send current home mark points.
|
// send current home mark points.
|
||||||
var moduleId = owner.getCurrentRealmId();
|
var moduleId = owner.getCurrentRealmId();
|
||||||
|
var scene = world.getSceneById(moduleId + 2000);
|
||||||
|
if (scene == null) {
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.warn(
|
||||||
|
"Current Realm id is invalid! SceneExcelConfigData.json, game resource not loaded correctly or the realm id maybe wrong?!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var homeScene = home.getHomeSceneItem(moduleId + 2000);
|
var homeScene = home.getHomeSceneItem(moduleId + 2000);
|
||||||
var mainHouse = home.getMainHouseItem(moduleId + 2000);
|
var mainHouse = home.getMainHouseItem(moduleId + 2000);
|
||||||
|
|
||||||
@ -31,12 +41,12 @@ public class PacketHomeMarkPointNotify extends BasePacket {
|
|||||||
.forEach(
|
.forEach(
|
||||||
homeSceneItem -> {
|
homeSceneItem -> {
|
||||||
var markPointData =
|
var markPointData =
|
||||||
HomeMarkPointSceneDataOuterClass.HomeMarkPointSceneData.newBuilder()
|
HomeMarkPointSceneData.newBuilder()
|
||||||
.setModuleId(moduleId)
|
.setModuleId(moduleId)
|
||||||
.setSceneId(homeSceneItem.getSceneId());
|
.setSceneId(homeSceneItem.getSceneId());
|
||||||
|
|
||||||
if (!homeSceneItem.isRoom()) {
|
if (!homeSceneItem.isRoom()) {
|
||||||
var config = world.getSceneById(moduleId + 2000).getScriptManager().getConfig();
|
var config = scene.getScriptManager().getConfig();
|
||||||
markPointData
|
markPointData
|
||||||
.setSafePointPos(
|
.setSafePointPos(
|
||||||
config == null
|
config == null
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package emu.grasscutter.server.packet.send;
|
||||||
|
|
||||||
|
import emu.grasscutter.game.entity.EntityMonster;
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.MonsterSummonTagNotifyOuterClass.MonsterSummonTagNotify;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PacketMonsterSummonTagNotify extends BasePacket {
|
||||||
|
|
||||||
|
public PacketMonsterSummonTagNotify(EntityMonster monsterEntity) {
|
||||||
|
super(PacketOpcodes.MonsterSummonTagNotify);
|
||||||
|
|
||||||
|
var proto = MonsterSummonTagNotify.newBuilder().setMonsterEntityId(monsterEntity.getId());
|
||||||
|
monsterEntity.getSummonTagMap().forEach((k, v) -> proto.putSummonTagMap(k, v == null ? 0 : 1));
|
||||||
|
|
||||||
|
this.setData(proto.build());
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user