13 Commits

Author SHA1 Message Date
5ebad71e9d Bump to version 1.7.4 2023-11-30 23:41:55 -05:00
564b609028 Update README_ja-JP.md (#2438)
* Update README_ja-JP.md

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

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

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

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

fixed "culivation" to cultivation

* Update README_zh-CN.md

culivation to cultivation

* Update zh-TW to fix "culivation"

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

View File

@ -29,7 +29,7 @@
- Get game version REL4.0.x (4.0.x client can be found here if you don't have it): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md - Get game version REL4.0.x (4.0.x client can be found here if you don't have it): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/4.0.0.md
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer. - Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
- After opening 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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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<>();

View File

@ -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,

View File

@ -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;
} }

View File

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

View File

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

View File

@ -13,6 +13,7 @@ public class TowerLevelData extends GameResource {
private int levelGroupId; private int 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;

View File

@ -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) {

View File

@ -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);

View File

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

View File

@ -68,6 +68,10 @@ public final class DungeonManager {
} }
if (isFinishedSuccessfully()) { 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) {

View File

@ -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));
} }

View File

@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
@ -22,6 +23,7 @@ public class WorldChallenge {
private final int challengeIndex; private final int challengeIndex;
private final List<Integer> paramList; private final List<Integer> paramList;
private int timeLimit; private int timeLimit;
private GameEntity guardEntity;
private final List<ChallengeTrigger> challengeTriggers; private final List<ChallengeTrigger> challengeTriggers;
private final int goal; private final int goal;
private final AtomicInteger score; private final AtomicInteger score;
@ -58,6 +60,7 @@ public class WorldChallenge {
this.challengeTriggers = challengeTriggers; this.challengeTriggers = challengeTriggers;
this.goal = goal; this.goal = goal;
this.score = new AtomicInteger(0); this.score = new AtomicInteger(0);
this.guardEntity = null;
} }
public boolean inProgress() { public boolean inProgress() {
@ -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;

View File

@ -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)));

View File

@ -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));

View File

@ -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();

View File

@ -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 =

View File

@ -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) {

View File

@ -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));

View File

@ -25,6 +25,7 @@ import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.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());
@ -316,6 +334,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 +378,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 +410,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 +451,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) {

View File

@ -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);

View File

@ -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();

View File

@ -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();
}
} }
} }

View File

@ -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;

View File

@ -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);

View File

@ -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);
} }
} }

View File

@ -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);

View File

@ -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(

View File

@ -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()) {

View File

@ -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;
} }

View File

@ -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 -> {
@ -925,6 +936,7 @@ public class SceneScriptManager {
private void callTrigger(SceneTrigger trigger, ScriptArgs params) { private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time // the SetGroupVariableValueByGroup in tower need the param to record the first stage time
ongoingTriggers.add(trigger);
var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params); var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params);
var invocationsCounter = triggerInvocations.get(trigger.getName()); var invocationsCounter = triggerInvocations.get(trigger.getName());
var invocations = invocationsCounter.incrementAndGet(); var invocations = invocationsCounter.incrementAndGet();
@ -956,11 +968,15 @@ public class SceneScriptManager {
} }
// always deregister on error, otherwise only if the count is reached // always deregister on error, otherwise only if the count is reached
if (ret.isboolean() && !ret.checkboolean() // or the trigger should be preserved after a RefreshGroup call
if (trigger.isPreserved()) {
trigger.setPreserved(false);
} else if (ret.isboolean() && !ret.checkboolean()
|| ret.isint() && ret.checkint() != 0 || ret.isint() && ret.checkint() != 0
|| trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) { || trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) {
deregisterTrigger(trigger); deregisterTrigger(trigger);
} }
ongoingTriggers.remove(trigger);
} }
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) { private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
@ -1053,18 +1069,7 @@ public class SceneScriptManager {
} }
// Calculate level // Calculate level
int level = monster.level; int level = getScene().getLevelForMonster(monster.config_id, monster.level);
if (getScene().getDungeonManager() != null) {
level = getScene().getDungeonManager().getLevelForMonster(monster.config_id);
} else if (getScene().getWorld().getWorldLevel() > 0) {
var worldLevelData =
GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel());
if (worldLevelData != null) {
level = worldLevelData.getMonsterLevel();
}
}
// Spawn mob // Spawn mob
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level); EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level);
@ -1104,6 +1109,19 @@ public class SceneScriptManager {
return meta.sceneBlockIndex; return meta.sceneBlockIndex;
} }
public void removeMonstersInGroup(SceneGroup group) {
var configSet =
group.monsters.values().stream().map(m -> m.config_id).collect(Collectors.toSet());
var toRemove =
getScene().getEntities().values().stream()
.filter(e -> e instanceof EntityMonster)
.filter(e -> e.getGroupId() == group.id)
.filter(e -> configSet.contains(e.getConfigId()))
.toList();
getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
}
public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) { public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet()); var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet());
var toRemove = var toRemove =

View File

@ -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.

View File

@ -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) {

View File

@ -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

View File

@ -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);
} }
} }

View File

@ -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);

View File

@ -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()));
} }

View File

@ -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));
} }
} }

View File

@ -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

View File

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