88 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
205b79dc02 Merge remote-tracking branch 'origin/development' into development 2023-10-31 22:36:22 -04:00
0e033e3f77 Bump to version 1.7.3 2023-10-31 22:23:45 -04:00
583a41ab2c Format code [skip actions] 2023-11-01 01:54:08 +00:00
cf6fb275be Add events to support scene group substitution (#2413)
* Add events to support scene group substitution

* make event members private with getter/setter

* delete stray unused var
2023-10-31 21:52:01 -04:00
269f7b4fbf Fix typo in start.cmd (#2415)
enviroment -> environment
2023-10-31 19:50:31 -04:00
9b4ce34f4a Format code [skip actions] 2023-10-26 02:29:17 +00:00
f86259a430 Fix some revives; improve dungeon exit flow (#2409) 2023-10-25 22:27:48 -04:00
837e30e04b Format code [skip actions] 2023-10-19 13:19:46 +00:00
f5703e5964 Fix mirror tower stages; fix tower time challenge and star scoring (#2406) 2023-10-19 09:18:12 -04:00
bc8e7c21ce Format code [skip actions] 2023-10-17 05:42:17 +00:00
b7a9d28f02 Fix reset tag without notification (#2405) 2023-10-17 01:41:24 -04:00
770cd62370 Fix daily dungeon flow (#2398)
* Fix dungeon entry, daily changes, replay flow; fix Mond's weapon mats domain unlock

* add note to DungeonEntryToBeExploreNotify
2023-10-17 01:41:04 -04:00
6745d1126e Format code [skip actions] 2023-10-14 16:11:33 +00:00
0803618bf5 Format code [skip actions] 2023-10-14 16:10:31 +00:00
cfc8a4866f Add reset scene tag subcommand (#2403)
* Add reset scene tag subcommand

* Fix Control Flow
2023-10-14 12:10:16 -04:00
fd75ba7b9b Fix triggered Monster Tide spawn; fix Tower dungeon handoff (#2397)
* Abyss: Fix monster tide trigger; fix dungeon handoff

* back out unrelated changes
2023-10-14 12:08:49 -04:00
d32a75e980 Fix setPrevScene bug (#2396) 2023-10-07 00:40:36 -04:00
9a198bd231 Alphabetize and Format ScriptLib.java (#2395)
I deeply apologize to anyone who is trying to find the history of the code from before today.
2023-10-07 00:40:15 -04:00
453dc9717d Format code [skip actions] 2023-10-02 14:58:22 +00:00
582d7af9c4 Send QUEST_COND_STATE_NOT_EQUAL and QUEST_COND_STATE_EQUAL on login (#2394) 2023-10-02 10:56:09 -04:00
cab3bfb5a7 Fix bug in quest cond state not equal (#2393)
* Fix bug in ConditionStateNotEqual.java

* Do the same fix for ConditionStateEqual.java
2023-10-02 03:25:35 -04:00
cf574e99cb Format code [skip actions] 2023-10-01 05:41:41 +00:00
3094facb88 Bump to version 1.7.2 2023-09-30 23:16:35 -04:00
6e309b6fee Merge remote-tracking branch 'origin/development' into development 2023-09-30 23:15:50 -04:00
b5e35f5409 Add legacy documentation on Hide and Seek 2023-09-30 18:10:10 -04:00
a3fd10c3be Fix NullPointerException when refilling an avatar with no skill depot 2023-09-30 18:09:40 -04:00
b6e7d69949 Send QUEST_COND_NONE on every login (#2386)
For players that enabled questing late
2023-09-25 19:30:33 -04:00
5faf39d359 Format code [skip actions] 2023-09-23 17:45:57 +00:00
0dd95450b1 Bare-bones Hangouts implementation (#2384) 2023-09-23 13:44:31 -04:00
0f0e7aca68 use pretty_host in domain filtering (#2382) 2023-09-22 23:33:01 -04:00
5ee4812ac5 fix: login too slow (#2380) 2023-09-20 21:23:08 -04:00
ec2bfffdd1 Format code [skip actions] 2023-09-18 01:03:10 +00:00
7f5059cb8f Format code [skip actions] 2023-09-18 01:03:02 +00:00
43db7eba8f tp to move directly in the same scene (#2375) 2023-09-17 21:01:21 -04:00
ff6a51db30 Update position parameters to support rotation-based offsets (#2374) 2023-09-17 21:00:58 -04:00
047feaf4aa Format code [skip actions] 2023-09-17 02:57:50 +00:00
88315ec712 Format code [skip actions] 2023-09-17 02:57:38 +00:00
fdad4218e7 Fix freeze on startup (#2368)
* DON'T load resources in static init!!!

* Update src/main/java/emu/grasscutter/Grasscutter.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/tools/Tools.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
2023-09-16 22:55:46 -04:00
5f5e6c38b1 Add rotation to /spawn (#2372) 2023-09-16 22:55:25 -04:00
92bd09eeed Format code [skip actions] 2023-09-16 23:00:20 +00:00
30f7580184 Merge remote-tracking branch 'origin/development' into development 2023-09-16 18:59:02 -04:00
65eaaa96f2 Apply changes from #2310 2023-09-16 18:58:36 -04:00
70a961e167 Format code [skip actions] 2023-09-16 22:44:16 +00:00
dd78addc29 Fix ability modifier serialization 2023-09-16 18:36:16 -04:00
43467ebc85 Factor fight properties into healing calculation 2023-09-16 18:32:01 -04:00
1f15d6219b Format code [skip actions] 2023-09-16 21:40:49 +00:00
fb0c2dbc84 Merge pull request #2341
* enableRandomEncryptSeed

* update config version

* Merge branch 'Grasscutters:development' into random-seed

* make the codes more beautiful

* move KahnsSort to algorithms

* rename variables

* Merge branch 'development' into random-seed
2023-09-16 14:38:39 -07:00
b8f7aea168 Format code [skip actions] 2023-09-16 21:33:36 +00:00
cf8092e8ba Update languages [skip actions] 2023-09-16 21:32:21 +00:00
0e44d18ae9 Better SceneTags (#2361)
* Add scene tag handling

* Move warns to debug

* Move to PlayerProgressManager

* Add success message

* Inline check

* Improve per-scene handling

* Update src/main/java/emu/grasscutter/command/commands/SetSceneTagCommand.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/command/commands/SetSceneTagCommand.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Fix types

* Small fix

* Update ScriptLib.java

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
2023-09-16 17:31:59 -04:00
5bd8f532c1 i forgot to add ` 2023-09-16 17:23:54 -04:00
ad62b6b11d Format code [skip actions] 2023-09-16 00:53:28 +00:00
c9a43a5e98 Format code [skip actions] 2023-09-16 00:52:37 +00:00
5458d36102 fix: HomeModuleManager isnt ticked unless player changes module (#2365) 2023-09-15 20:51:16 -04:00
c4dbb6851b Implement passJumpDungeon. Make dungeons kick you out to the correct scene (#2366) 2023-09-15 20:50:56 -04:00
2643c6b3b7 Format code [skip actions] 2023-09-15 00:45:41 +00:00
84e1371499 Fixes for alchemy dude's quest part 2 (#2362)
* Make platforms able to reset

platforms apparently are able to run again once reaching the last point. As well, canceling a timer seems to return a 0, not a 1

* Fix Seelies

Added HandlerClientScriptEventNotify (shoutouts to Hartie!) and cleaned up the platform stuff in ScriptLib.

There is a problem with HandlerClientScriptEventNotify where the client seems to only pass 0 into param1 instead of the configId. I coded in a workaround, but someone with greater access to things should check up on what is going on
2023-09-14 20:44:30 -04:00
f955bb1e16 Little update to item giving handling (#2363)
* Little update to HandlerItemGivingReq.java

GIVING_METHOD_GROUP is used in cooking girl's quest

* Send the giving packet at the start of relogs, even if encountered before.

* Make item checking not exact

You can have more items in your inventory than what you are submitting.
2023-09-14 20:44:08 -04:00
2b64814534 fix: home worlds are ticked twice (#2360) 2023-09-14 01:22:29 -04:00
ea5ee075a7 Merge remote-tracking branch 'origin/development' into development 2023-09-13 21:03:44 -04:00
6108a3bb37 Handbook documentation & build flow fixes 2023-09-13 21:03:32 -04:00
98a83b649e Add more precise instructions for the handbook (#2359)
* Add more precise instructions for the handbook

* Reformat Building section
2023-09-13 21:02:20 -04:00
4f62e484ad Update ru-RU.json (#2356)
translation & typo
2023-09-12 11:56:31 -04:00
5fd31aece8 Fix home nulls (#2355)
* Add null checking

* Sanity check for moduleManager
2023-09-12 01:07:17 -04:00
8de281d4da Format code [skip actions] 2023-09-10 23:21:44 +00:00
fbe2b138ee Fixes for alchemy dude's quest (#2352)
* Add drops for gadgets

Gadgets only have drop_id when they are not chests (chest_drop_id). When drop_id is not set (0), handleChestDrop quickly exits

* Implement QUEST_COND_ITEM_GIVING_FINISHED

Took the oppertunity to Rename ContentFinishGivingItem to ItemGiving

* Store accept conditions like fail and finish content are

Took the oppertunity to clean up some old code as well

conditions are stored in QuestManager

* Update src/main/java/emu/grasscutter/game/quest/QuestManager.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update ConditionItemGivingFinished.java

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
2023-09-10 19:20:28 -04:00
47c96fd964 Regular ScriptLib update! (#2349)
* Regular ScriptLib update!

* Move RefreshGroup down to debug

* Update SetGadgetEnableInteract
2023-09-09 20:49:22 -04:00
04370f1a21 Update CN README to game REL 4.0 (#2350) 2023-09-09 20:48:32 -04:00
7845c54570 Format code [skip actions] 2023-09-09 03:07:29 +00:00
818b638bed Added Hindi Language Support (#2347)
* Added Hindi Language Support 

Please review the code

* Update README_HE.md

* Update README_NL.md

* Update README_es-ES.md

* Update README_fil-PH.md

* Update README_fr-FR.md

* Update README_id-ID.md

* Update README_it-IT.md

* Update README_ja-JP.md

* Update README_ko-KR.md

* Update README_pl-PL.md

* Update README_ru-RU.md

* Update README_vi-VN.md

* Update README_zh-CN.md

* Update README_zh-TW.md
2023-09-08 23:07:18 -04:00
a9402f487f Implement RegionShape.POLYGON and RegionShape.CYLINDER (#2348)
Also took the opportunity to sort them in order and use multiplication instead of pow.
2023-09-08 23:06:14 -04:00
cdcdf924bd Format code [skip actions] 2023-09-08 03:35:28 +00:00
fc42f665a7 feat: implement teapot suite (#2344)
* feat: implement teapot suite

* fix: home animals, check respawn, etc

* fix: NPE and cancel summon events

* fix: forgot to send eventId also
2023-09-07 23:34:03 -04:00
83602f78ae Update readme to 4.0 from 3.7 (#2343) 2023-09-07 23:33:13 -04:00
8db1f597ce Format code [skip actions] 2023-09-02 23:51:35 +00:00
184 changed files with 5870 additions and 2359 deletions

View File

@ -26,10 +26,10 @@
- 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 REL3.7 (3.7 client can be found here if you don't have it): https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/3.7.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.
@ -46,25 +46,49 @@ Grasscutter uses Gradle to handle dependencies & building.
**Requirements:** **Requirements:**
- [Java SE Development Kits - 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or higher - [Java Development Kit 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or higher
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download) (Optional, for building the handbook)
##### Windows ##### Clone
```shell ```shell
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter cd Grasscutter
.\gradlew.bat # Setting up environments
.\gradlew jar # Compile
``` ```
##### Linux (GNU) ##### Compile
**Note**: Handbook generation may fail on some systems. To disable the handbook generation, append `-PskipHandbook=1` to the `gradlew jar` command.
Windows:
```shell
.\gradlew.bat # Setting up environments
.\gradlew jar
```
Linux (GNU):
```bash ```bash
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
chmod +x gradlew chmod +x gradlew
./gradlew jar # Compile ./gradlew jar
```
##### Compiling the Handbook (Manually)
With Gradle:
```shell
./gradlew generateHandbook
```
With NPM:
```shell
cd src/handbook
npm install
npm run build
``` ```
You can find the output jar in the root of the project folder. You can find the output jar in the root of the project folder.

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.1' version = '1.7.4'
java { java {
withJavadocJar() withJavadocJar()
@ -386,6 +386,12 @@ tasks.register('generateHandbook') {
return return
} }
// Install dependencies before building.
exec {
workingDir 'src/handbook'
commandLine npm, 'install'
}
// Build the handbook. // Build the handbook.
exec { exec {
workingDir 'src/handbook' workingDir 'src/handbook'

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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).

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) | [हिंदी](README_hn-IN.md)
**Aantekening:** We verwelkomen altijd bijdragers aan het project. Lees onze [Gedragscode](https://github.com/Grasscutters/Grasscutter/blob/development/README_NL.md#bijdragen-aan-het-project) zorgvuldig door voordat u uw bijdrage toevoegt. **Aantekening:** We verwelkomen altijd bijdragers aan het project. Lees onze [Gedragscode](https://github.com/Grasscutters/Grasscutter/blob/development/README_NL.md#bijdragen-aan-het-project) zorgvuldig door voordat u uw bijdrage toevoegt.

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**Atención:** Siempre damos la bienvenida a contribuidores del proyecto. Antes de añadir tu contribución, por favor lee cuidadosamente nuestro [Código de conducta](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md). **Atención:** Siempre damos la bienvenida a contribuidores del proyecto. Antes de añadir tu contribución, por favor lee cuidadosamente nuestro [Código de conducta](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**Atensyon:** Ang mga kontributor ay laging welcome sa proyektong ito. Bago mag-bigay ng kontribusyon, basahin muna ng mabuti ang [Code of Conduct](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md). **Atensyon:** Ang mga kontributor ay laging welcome sa proyektong ito. Bago mag-bigay ng kontribusyon, basahin muna ng mabuti ang [Code of Conduct](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) | [हिंदी](README_hn-IN.md)
**Attention:** De nouveaux contributeurs sont toujours les bienvenus. Avant d'ajouter votre contribution, veuillez lire le [code de conduite](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md). **Attention:** De nouveaux contributeurs sont toujours les bienvenus. Avant d'ajouter votre contribution, veuillez lire le [code de conduite](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).

78
docs/README_hn-IN.md Normal file
View File

@ -0,0 +1,78 @@
![Grasscutter](https://socialify.git.ci/Grasscutters/Grasscutter/image?description=1&forks=1&issues=1&language=1&logo=https%3A%2F%2Fs2.loli.net%2F2022%2F04%2F25%2FxOiJn7lCdcT5Mw1.png&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
<div align="center"><img alt="Documentation" src="https://img.shields.io/badge/Wiki-Grasscutter-blue?style=for-the-badge&link=https://github.com/Grasscutters/Grasscutter/wiki&link=https://github.com/Grasscutters/Grasscutter/wiki"> <img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Grasscutters/Grasscutter?logo=java&style=for-the-badge"> <img alt="GitHub" src="https://img.shields.io/github/license/Grasscutters/Grasscutter?style=for-the-badge"> <img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/Grasscutters/Grasscutter?style=for-the-badge"> <img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/Grasscutters/Grasscutter/build.yml?branch=development&logo=github&style=for-the-badge"></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)
**ध्यान:** हम हमेशा परियोजना में योगदानकर्ताओं का स्वागत करते हैं।. अपना योगदान जोड़ने से पहले कृपया हमारा ध्यानपूर्वक पढ़ें [आचार संहिता](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).
## वर्तमान सुविधाएँ
* लॉग इन करना
* युद्ध
* मित्रों की सूची
* टेलीपोर्टेशन
* गाचा प्रणाली
* सह-ऑप * आंशिक रूप से * काम करता है
* कंसोल के माध्यम से राक्षसों को जन्म देना
* इन्वेंट्री सुविधाएँ (आइटम / वर्ण प्राप्त करना, आइटम / वर्णों को अपग्रेड करना, आदि)
## त्वरित सेटअप गाइड
**टिप्पणी**: समर्थन के लिए कृपया हमसे जुड़ें [Discord](https://discord.gg/T5vZU6UyeG).
### त्वरित प्रारंभ (स्वचालित)
- Get Java 17: https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
**ध्यान दें:** बस **सर्वर शुरू करने** के लिए, आपको बस **jre** की आवश्यकता है।
- Get [MongoDB Community Server](https://www.mongodb.com/try/download/community)
* प्रॉक्सी: मिटमडंप (अनुशंसित), मिटमप्रॉक्सी, फिडलर क्लासिक, आदि।
- गेम संस्करण REL3.7 प्राप्त करें (यदि आपके पास 3.7 क्लाइंट नहीं है तो उसे यहां पाया जा सकता है):: https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/3.7.0.md
- डाउनलोड करें [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). उपयोग `.msi` इंस्टालरr.
- कलिवेशन (एडमिन के रूप में) खोलने के बाद, ऊपरी दाएं कोने में डाउनलोड बटन दबाएं।
- `डाउनलोड ऑल-इन-वन` पर क्लिक करें
- ऊपरी दाएं कोने में गियर पर क्लिक करें
- गेम इंस्टॉल पथ को उस स्थान पर सेट करें जहां आपका गेम स्थित है.
- कस्टम जावा पथ को इस पर सेट करें `C:\Program Files\Java\jdk-17\bin\java.exe`
- अन्य सभी सेटिंग्स को डिफ़ॉल्ट पर छोड़ दें
- लॉन्च करने के लिए आगे छोटे बटन पर क्लिक करें.
- लॉन्च बटन पर क्लिक करें.
- आप जो भी उपयोगकर्ता नाम चाहते हैं उसके साथ लॉग इन करें। पासवर्ड कोई मायने नहीं रखता.
### इमारत
ग्रासकटर निर्भरता और निर्माण को संभालने के लिए ग्रैडल का उपयोग करता है।
**आवश्यकताएं:**
- [Java SE Development Kits - 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or higher
- [Git](https://git-scm.com/downloads)
##### विंडोज
```shell
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
.\gradlew.bat # Setting up environments
.\gradlew jar # Compile
```
##### लिनक्स (जीएनयू)
```bash
git clone --recurse-submodules https://github.com/Grasscutters/Grasscutter.git
cd Grasscutter
chmod +x gradlew
./gradlew jar # Compile
```
आप आउटपुट जार को प्रोजेक्ट फ़ोल्डर के रूट में पा सकते हैं।.
### समस्या निवारण
सामान्य मुद्दों और समाधानों की सूची और सहायता मांगने के लिए कृपया शामिल हों [our Discord server](https://discord.gg/T5vZU6UyeG) और सपोर्ट चैनल पर जाएं.

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**Perhatian:** Kami selalu menyambut kontributor untuk proyek ini. Sebelum menambahkan kontribusi Anda, harap baca [Kode Etik](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) kami. **Perhatian:** Kami selalu menyambut kontributor untuk proyek ini. Sebelum menambahkan kontribusi Anda, harap baca [Kode Etik](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) kami.

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**Attenzione:** Diamo sempre il benvenuto ai contributori del progetto. Prima di contribuire, leggi attentamente il nostro [Codice di condotta](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md). **Attenzione:** Diamo sempre il benvenuto ai contributori del progetto. Prima di contribuire, leggi attentamente il nostro [Codice di condotta](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).

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) [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

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)을 주의 깊게 읽어주세요.

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**Uwaga:** Zawsze jesteśmy otwarci na wasz wkład w projekt. Przed zaproponowaniem zmian przeczytaj [zasady postępowania (ENG)](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md). **Uwaga:** Zawsze jesteśmy otwarci na wasz wkład w projekt. Przed zaproponowaniem zmian przeczytaj [zasady postępowania (ENG)](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md).

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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).

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**Chú ý:** Chúng tôi luôn chào đón những người đóng góp cho dự án. Trước khi đóng góp, xin vui lòng đọc kỹ ["các quy tắc" (Code of Conduct)](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) của chúng tôi . **Chú ý:** Chúng tôi luôn chào đón những người đóng góp cho dự án. Trước khi đóng góp, xin vui lòng đọc kỹ ["các quy tắc" (Code of Conduct)](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) của chúng tôi .

View File

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)。
@ -26,10 +26,10 @@
- 获取Java 17https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html - 获取Java 17https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
- 获取[MongoDB社区版](https://www.mongodb.com/try/download/community) - 获取[MongoDB社区版](https://www.mongodb.com/try/download/community)
- 获取游戏3.7正式版 (如果你没有3.7的客户端可以在这里找到https://github.com/MAnggiarMustofa/GI-Download-Library/blob/main/GenshinImpact/Client/3.7.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

@ -3,7 +3,7 @@
<div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div> <div align="center"><a href="https://discord.gg/T5vZU6UyeG"><img alt="Discord - Grasscutter" src="https://img.shields.io/discord/965284035985305680?label=Discord&logo=discord&style=for-the-badge"></a></div>
[EN](../README.md) | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) | [FR](README_fr-FR.md) | [ES](README_es-ES.md) | [HE](README_HE.md) | [RU](README_ru-RU.md) | [PL](README_pl-PL.md) | [ID](README_id-ID.md) | [KR](README_ko-KR.md) | [FIL/PH](README_fil-PH.md) | [NL](README_NL.md) | [JP](README_ja-JP.md) | [IT](README_it-IT.md) | [VI](README_vi-VN.md) [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)
**請注意:** 歡迎成為本專案的貢獻者。在提交 PR 之前, 請仔細閱讀[程式碼規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)。 **請注意:** 歡迎成為本專案的貢獻者。在提交 PR 之前, 請仔細閱讀[程式碼規範](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md)。
@ -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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

@ -36,21 +36,21 @@ public final class HomePlantSubFieldDataOuterClass {
int getEntityIdList(int index); int getEntityIdList(int index);
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return The enum numeric value on the wire for cAKDDMKAIMD. * @return The enum numeric value on the wire for status.
*/ */
int getCAKDDMKAIMDValue(); int getStatusValue();
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return The cAKDDMKAIMD. * @return The status.
*/ */
emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getCAKDDMKAIMD(); emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getStatus();
/** /**
* <code>uint32 JHFNDBIHLNB = 8;</code> * <code>uint32 seed_id = 8;</code>
* @return The jHFNDBIHLNB. * @return The seedId.
*/ */
int getJHFNDBIHLNB(); int getSeedId();
/** /**
* <code>fixed32 end_time = 14;</code> * <code>fixed32 end_time = 14;</code>
@ -59,10 +59,10 @@ public final class HomePlantSubFieldDataOuterClass {
int getEndTime(); int getEndTime();
/** /**
* <code>uint32 KHFGOPCOAGM = 3;</code> * <code>uint32 gather_point_type = 3;</code>
* @return The kHFGOPCOAGM. * @return The gatherPointType.
*/ */
int getKHFGOPCOAGM(); int getGatherPointType();
} }
/** /**
* <pre> * <pre>
@ -82,7 +82,7 @@ public final class HomePlantSubFieldDataOuterClass {
} }
private HomePlantSubFieldData() { private HomePlantSubFieldData() {
entityIdList_ = emptyIntList(); entityIdList_ = emptyIntList();
cAKDDMKAIMD_ = 0; status_ = 0;
} }
@java.lang.Override @java.lang.Override
@ -118,7 +118,7 @@ public final class HomePlantSubFieldDataOuterClass {
break; break;
case 24: { case 24: {
kHFGOPCOAGM_ = input.readUInt32(); gatherPointType_ = input.readUInt32();
break; break;
} }
case 48: { case 48: {
@ -145,12 +145,12 @@ public final class HomePlantSubFieldDataOuterClass {
case 56: { case 56: {
int rawValue = input.readEnum(); int rawValue = input.readEnum();
cAKDDMKAIMD_ = rawValue; status_ = rawValue;
break; break;
} }
case 64: { case 64: {
jHFNDBIHLNB_ = input.readUInt32(); seedId_ = input.readUInt32();
break; break;
} }
case 117: { case 117: {
@ -221,34 +221,34 @@ public final class HomePlantSubFieldDataOuterClass {
} }
private int entityIdListMemoizedSerializedSize = -1; private int entityIdListMemoizedSerializedSize = -1;
public static final int CAKDDMKAIMD_FIELD_NUMBER = 7; public static final int STATUS_FIELD_NUMBER = 7;
private int cAKDDMKAIMD_; private int status_;
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return The enum numeric value on the wire for cAKDDMKAIMD. * @return The enum numeric value on the wire for status.
*/ */
@java.lang.Override public int getCAKDDMKAIMDValue() { @java.lang.Override public int getStatusValue() {
return cAKDDMKAIMD_; return status_;
} }
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return The cAKDDMKAIMD. * @return The status.
*/ */
@java.lang.Override public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getCAKDDMKAIMD() { @java.lang.Override public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getStatus() {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(cAKDDMKAIMD_); emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(status_);
return result == null ? emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.UNRECOGNIZED : result; return result == null ? emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.UNRECOGNIZED : result;
} }
public static final int JHFNDBIHLNB_FIELD_NUMBER = 8; public static final int SEED_ID_FIELD_NUMBER = 8;
private int jHFNDBIHLNB_; private int seedId_;
/** /**
* <code>uint32 JHFNDBIHLNB = 8;</code> * <code>uint32 seed_id = 8;</code>
* @return The jHFNDBIHLNB. * @return The seedId.
*/ */
@java.lang.Override @java.lang.Override
public int getJHFNDBIHLNB() { public int getSeedId() {
return jHFNDBIHLNB_; return seedId_;
} }
public static final int END_TIME_FIELD_NUMBER = 14; public static final int END_TIME_FIELD_NUMBER = 14;
@ -262,15 +262,15 @@ public final class HomePlantSubFieldDataOuterClass {
return endTime_; return endTime_;
} }
public static final int KHFGOPCOAGM_FIELD_NUMBER = 3; public static final int GATHER_POINT_TYPE_FIELD_NUMBER = 3;
private int kHFGOPCOAGM_; private int gatherPointType_;
/** /**
* <code>uint32 KHFGOPCOAGM = 3;</code> * <code>uint32 gather_point_type = 3;</code>
* @return The kHFGOPCOAGM. * @return The gatherPointType.
*/ */
@java.lang.Override @java.lang.Override
public int getKHFGOPCOAGM() { public int getGatherPointType() {
return kHFGOPCOAGM_; return gatherPointType_;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
@ -288,8 +288,8 @@ public final class HomePlantSubFieldDataOuterClass {
public void writeTo(com.google.protobuf.CodedOutputStream output) public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException { throws java.io.IOException {
getSerializedSize(); getSerializedSize();
if (kHFGOPCOAGM_ != 0) { if (gatherPointType_ != 0) {
output.writeUInt32(3, kHFGOPCOAGM_); output.writeUInt32(3, gatherPointType_);
} }
if (getEntityIdListList().size() > 0) { if (getEntityIdListList().size() > 0) {
output.writeUInt32NoTag(50); output.writeUInt32NoTag(50);
@ -298,11 +298,11 @@ public final class HomePlantSubFieldDataOuterClass {
for (int i = 0; i < entityIdList_.size(); i++) { for (int i = 0; i < entityIdList_.size(); i++) {
output.writeUInt32NoTag(entityIdList_.getInt(i)); output.writeUInt32NoTag(entityIdList_.getInt(i));
} }
if (cAKDDMKAIMD_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) { if (status_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) {
output.writeEnum(7, cAKDDMKAIMD_); output.writeEnum(7, status_);
} }
if (jHFNDBIHLNB_ != 0) { if (seedId_ != 0) {
output.writeUInt32(8, jHFNDBIHLNB_); output.writeUInt32(8, seedId_);
} }
if (endTime_ != 0) { if (endTime_ != 0) {
output.writeFixed32(14, endTime_); output.writeFixed32(14, endTime_);
@ -316,9 +316,9 @@ public final class HomePlantSubFieldDataOuterClass {
if (size != -1) return size; if (size != -1) return size;
size = 0; size = 0;
if (kHFGOPCOAGM_ != 0) { if (gatherPointType_ != 0) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(3, kHFGOPCOAGM_); .computeUInt32Size(3, gatherPointType_);
} }
{ {
int dataSize = 0; int dataSize = 0;
@ -334,13 +334,13 @@ public final class HomePlantSubFieldDataOuterClass {
} }
entityIdListMemoizedSerializedSize = dataSize; entityIdListMemoizedSerializedSize = dataSize;
} }
if (cAKDDMKAIMD_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) { if (status_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeEnumSize(7, cAKDDMKAIMD_); .computeEnumSize(7, status_);
} }
if (jHFNDBIHLNB_ != 0) { if (seedId_ != 0) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(8, jHFNDBIHLNB_); .computeUInt32Size(8, seedId_);
} }
if (endTime_ != 0) { if (endTime_ != 0) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
@ -363,13 +363,13 @@ public final class HomePlantSubFieldDataOuterClass {
if (!getEntityIdListList() if (!getEntityIdListList()
.equals(other.getEntityIdListList())) return false; .equals(other.getEntityIdListList())) return false;
if (cAKDDMKAIMD_ != other.cAKDDMKAIMD_) return false; if (status_ != other.status_) return false;
if (getJHFNDBIHLNB() if (getSeedId()
!= other.getJHFNDBIHLNB()) return false; != other.getSeedId()) return false;
if (getEndTime() if (getEndTime()
!= other.getEndTime()) return false; != other.getEndTime()) return false;
if (getKHFGOPCOAGM() if (getGatherPointType()
!= other.getKHFGOPCOAGM()) return false; != other.getGatherPointType()) return false;
if (!unknownFields.equals(other.unknownFields)) return false; if (!unknownFields.equals(other.unknownFields)) return false;
return true; return true;
} }
@ -385,14 +385,14 @@ public final class HomePlantSubFieldDataOuterClass {
hash = (37 * hash) + ENTITY_ID_LIST_FIELD_NUMBER; hash = (37 * hash) + ENTITY_ID_LIST_FIELD_NUMBER;
hash = (53 * hash) + getEntityIdListList().hashCode(); hash = (53 * hash) + getEntityIdListList().hashCode();
} }
hash = (37 * hash) + CAKDDMKAIMD_FIELD_NUMBER; hash = (37 * hash) + STATUS_FIELD_NUMBER;
hash = (53 * hash) + cAKDDMKAIMD_; hash = (53 * hash) + status_;
hash = (37 * hash) + JHFNDBIHLNB_FIELD_NUMBER; hash = (37 * hash) + SEED_ID_FIELD_NUMBER;
hash = (53 * hash) + getJHFNDBIHLNB(); hash = (53 * hash) + getSeedId();
hash = (37 * hash) + END_TIME_FIELD_NUMBER; hash = (37 * hash) + END_TIME_FIELD_NUMBER;
hash = (53 * hash) + getEndTime(); hash = (53 * hash) + getEndTime();
hash = (37 * hash) + KHFGOPCOAGM_FIELD_NUMBER; hash = (37 * hash) + GATHER_POINT_TYPE_FIELD_NUMBER;
hash = (53 * hash) + getKHFGOPCOAGM(); hash = (53 * hash) + getGatherPointType();
hash = (29 * hash) + unknownFields.hashCode(); hash = (29 * hash) + unknownFields.hashCode();
memoizedHashCode = hash; memoizedHashCode = hash;
return hash; return hash;
@ -532,13 +532,13 @@ public final class HomePlantSubFieldDataOuterClass {
super.clear(); super.clear();
entityIdList_ = emptyIntList(); entityIdList_ = emptyIntList();
bitField0_ = (bitField0_ & ~0x00000001); bitField0_ = (bitField0_ & ~0x00000001);
cAKDDMKAIMD_ = 0; status_ = 0;
jHFNDBIHLNB_ = 0; seedId_ = 0;
endTime_ = 0; endTime_ = 0;
kHFGOPCOAGM_ = 0; gatherPointType_ = 0;
return this; return this;
} }
@ -572,10 +572,10 @@ public final class HomePlantSubFieldDataOuterClass {
bitField0_ = (bitField0_ & ~0x00000001); bitField0_ = (bitField0_ & ~0x00000001);
} }
result.entityIdList_ = entityIdList_; result.entityIdList_ = entityIdList_;
result.cAKDDMKAIMD_ = cAKDDMKAIMD_; result.status_ = status_;
result.jHFNDBIHLNB_ = jHFNDBIHLNB_; result.seedId_ = seedId_;
result.endTime_ = endTime_; result.endTime_ = endTime_;
result.kHFGOPCOAGM_ = kHFGOPCOAGM_; result.gatherPointType_ = gatherPointType_;
onBuilt(); onBuilt();
return result; return result;
} }
@ -634,17 +634,17 @@ public final class HomePlantSubFieldDataOuterClass {
} }
onChanged(); onChanged();
} }
if (other.cAKDDMKAIMD_ != 0) { if (other.status_ != 0) {
setCAKDDMKAIMDValue(other.getCAKDDMKAIMDValue()); setStatusValue(other.getStatusValue());
} }
if (other.getJHFNDBIHLNB() != 0) { if (other.getSeedId() != 0) {
setJHFNDBIHLNB(other.getJHFNDBIHLNB()); setSeedId(other.getSeedId());
} }
if (other.getEndTime() != 0) { if (other.getEndTime() != 0) {
setEndTime(other.getEndTime()); setEndTime(other.getEndTime());
} }
if (other.getKHFGOPCOAGM() != 0) { if (other.getGatherPointType() != 0) {
setKHFGOPCOAGM(other.getKHFGOPCOAGM()); setGatherPointType(other.getGatherPointType());
} }
this.mergeUnknownFields(other.unknownFields); this.mergeUnknownFields(other.unknownFields);
onChanged(); onChanged();
@ -755,87 +755,87 @@ public final class HomePlantSubFieldDataOuterClass {
return this; return this;
} }
private int cAKDDMKAIMD_ = 0; private int status_ = 0;
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return The enum numeric value on the wire for cAKDDMKAIMD. * @return The enum numeric value on the wire for status.
*/ */
@java.lang.Override public int getCAKDDMKAIMDValue() { @java.lang.Override public int getStatusValue() {
return cAKDDMKAIMD_; return status_;
} }
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @param value The enum numeric value on the wire for cAKDDMKAIMD to set. * @param value The enum numeric value on the wire for status to set.
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder setCAKDDMKAIMDValue(int value) { public Builder setStatusValue(int value) {
cAKDDMKAIMD_ = value; status_ = value;
onChanged(); onChanged();
return this; return this;
} }
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return The cAKDDMKAIMD. * @return The status.
*/ */
@java.lang.Override @java.lang.Override
public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getCAKDDMKAIMD() { public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getStatus() {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(cAKDDMKAIMD_); emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(status_);
return result == null ? emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.UNRECOGNIZED : result; return result == null ? emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.UNRECOGNIZED : result;
} }
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @param value The cAKDDMKAIMD to set. * @param value The status to set.
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder setCAKDDMKAIMD(emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus value) { public Builder setStatus(emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus value) {
if (value == null) { if (value == null) {
throw new NullPointerException(); throw new NullPointerException();
} }
cAKDDMKAIMD_ = value.getNumber(); status_ = value.getNumber();
onChanged(); onChanged();
return this; return this;
} }
/** /**
* <code>.HomePlantFieldStatus CAKDDMKAIMD = 7;</code> * <code>.HomePlantFieldStatus status = 7;</code>
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder clearCAKDDMKAIMD() { public Builder clearStatus() {
cAKDDMKAIMD_ = 0; status_ = 0;
onChanged(); onChanged();
return this; return this;
} }
private int jHFNDBIHLNB_ ; private int seedId_ ;
/** /**
* <code>uint32 JHFNDBIHLNB = 8;</code> * <code>uint32 seed_id = 8;</code>
* @return The jHFNDBIHLNB. * @return The seedId.
*/ */
@java.lang.Override @java.lang.Override
public int getJHFNDBIHLNB() { public int getSeedId() {
return jHFNDBIHLNB_; return seedId_;
} }
/** /**
* <code>uint32 JHFNDBIHLNB = 8;</code> * <code>uint32 seed_id = 8;</code>
* @param value The jHFNDBIHLNB to set. * @param value The seedId to set.
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder setJHFNDBIHLNB(int value) { public Builder setSeedId(int value) {
jHFNDBIHLNB_ = value; seedId_ = value;
onChanged(); onChanged();
return this; return this;
} }
/** /**
* <code>uint32 JHFNDBIHLNB = 8;</code> * <code>uint32 seed_id = 8;</code>
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder clearJHFNDBIHLNB() { public Builder clearSeedId() {
jHFNDBIHLNB_ = 0; seedId_ = 0;
onChanged(); onChanged();
return this; return this;
} }
@ -871,33 +871,33 @@ public final class HomePlantSubFieldDataOuterClass {
return this; return this;
} }
private int kHFGOPCOAGM_ ; private int gatherPointType_ ;
/** /**
* <code>uint32 KHFGOPCOAGM = 3;</code> * <code>uint32 gather_point_type = 3;</code>
* @return The kHFGOPCOAGM. * @return The gatherPointType.
*/ */
@java.lang.Override @java.lang.Override
public int getKHFGOPCOAGM() { public int getGatherPointType() {
return kHFGOPCOAGM_; return gatherPointType_;
} }
/** /**
* <code>uint32 KHFGOPCOAGM = 3;</code> * <code>uint32 gather_point_type = 3;</code>
* @param value The kHFGOPCOAGM to set. * @param value The gatherPointType to set.
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder setKHFGOPCOAGM(int value) { public Builder setGatherPointType(int value) {
kHFGOPCOAGM_ = value; gatherPointType_ = value;
onChanged(); onChanged();
return this; return this;
} }
/** /**
* <code>uint32 KHFGOPCOAGM = 3;</code> * <code>uint32 gather_point_type = 3;</code>
* @return This builder for chaining. * @return This builder for chaining.
*/ */
public Builder clearKHFGOPCOAGM() { public Builder clearGatherPointType() {
kHFGOPCOAGM_ = 0; gatherPointType_ = 0;
onChanged(); onChanged();
return this; return this;
} }
@ -969,12 +969,12 @@ public final class HomePlantSubFieldDataOuterClass {
static { static {
java.lang.String[] descriptorData = { java.lang.String[] descriptorData = {
"\n\033HomePlantSubFieldData.proto\032\032HomePlant" + "\n\033HomePlantSubFieldData.proto\032\032HomePlant" +
"FieldStatus.proto\"\227\001\n\025HomePlantSubFieldD" + "FieldStatus.proto\"\224\001\n\025HomePlantSubFieldD" +
"ata\022\026\n\016entity_id_list\030\006 \003(\r\022*\n\013CAKDDMKAI" + "ata\022\026\n\016entity_id_list\030\006 \003(\r\022%\n\006status\030\007 " +
"MD\030\007 \001(\0162\025.HomePlantFieldStatus\022\023\n\013JHFND" + "\001(\0162\025.HomePlantFieldStatus\022\017\n\007seed_id\030\010 " +
"BIHLNB\030\010 \001(\r\022\020\n\010end_time\030\016 \001(\007\022\023\n\013KHFGOP" + "\001(\r\022\020\n\010end_time\030\016 \001(\007\022\031\n\021gather_point_ty" +
"COAGM\030\003 \001(\rB\033\n\031emu.grasscutter.net.proto" + "pe\030\003 \001(\rB\033\n\031emu.grasscutter.net.protob\006p" +
"b\006proto3" "roto3"
}; };
descriptor = com.google.protobuf.Descriptors.FileDescriptor descriptor = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData, .internalBuildGeneratedFileFrom(descriptorData,
@ -986,7 +986,7 @@ public final class HomePlantSubFieldDataOuterClass {
internal_static_HomePlantSubFieldData_fieldAccessorTable = new internal_static_HomePlantSubFieldData_fieldAccessorTable = new
com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
internal_static_HomePlantSubFieldData_descriptor, internal_static_HomePlantSubFieldData_descriptor,
new java.lang.String[] { "EntityIdList", "CAKDDMKAIMD", "JHFNDBIHLNB", "EndTime", "KHFGOPCOAGM", }); new java.lang.String[] { "EntityIdList", "Status", "SeedId", "EndTime", "GatherPointType", });
emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.getDescriptor(); emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.getDescriptor();
} }

View File

@ -1,8 +1,32 @@
# Handbook Data # Handbook Data
Use Grasscutter's dumpers to generate the data to put here. Use Grasscutter's dumpers to generate the data to put here.
# Generating Data
When you have Grasscutter set up, you can use the following commands to generate the data:
- Commands - `grasscutter.jar -dump=commands,en-us`
- Items - `grasscutter.jar -dump=items,EN`
- Avatars - `grasscutter.jar -dump=avatars,EN`
- Quests - `grasscutter.jar -dump=quests,EN`
- Entities - `grasscutter.jar -dump=entities,en-us`
- Areas - `grasscutter.jar -dump=areas,EN`
- Scenes - `grasscutter.jar -dump=scenes,en-us`
Grasscutter being "set up" means:
- A Java runtime is installed
- Resources are provided in the working directory
## Language Locales
You can replace `en-us` or `EN` using the language locale which matches the format.
| Grasscutter Language Locale | Handbook Language Locale |
|-----------------------------|--------------------------|
| en-us | EN |
## Files Required ## Files Required
- `mainquests.csv' - `mainquests.csv`
- `commands.json` - `commands.json`
- `entities.csv` - `entities.csv`
- `avatars.csv` - `avatars.csv`

View File

@ -158,6 +158,8 @@ public final class Grasscutter {
// Generate handbooks. // Generate handbooks.
Tools.createGmHandbooks(false); Tools.createGmHandbooks(false);
// Generate gacha mappings.
Tools.generateGachaMappings();
} }
// Start servers. // Start servers.

View File

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

View File

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

View File

@ -33,7 +33,7 @@ public final class EnterDungeonCommand implements CommandHandler {
targetPlayer targetPlayer
.getServer() .getServer()
.getDungeonSystem() .getDungeonSystem()
.enterDungeon(targetPlayer.getSession().getPlayer(), 0, dungeonId); .enterDungeon(targetPlayer.getSession().getPlayer(), 0, dungeonId, true);
if (!result) { if (!result) {
CommandHandler.sendMessage( CommandHandler.sendMessage(

View File

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

View File

@ -0,0 +1,124 @@
package emu.grasscutter.command.commands;
import emu.grasscutter.command.*;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.scene.SceneTagData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import java.util.*;
import lombok.val;
@Command(
label = "setSceneTag",
aliases = {"tag"},
usage = {"<add|remove|unlockall|reset> <sceneTagId>"},
permission = "player.setscenetag",
permissionTargeted = "player.setscenetag.others")
public final class SetSceneTagCommand implements CommandHandler {
private final Int2ObjectMap<SceneTagData> sceneTagData = GameData.getSceneTagDataMap();
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.isEmpty()) {
sendUsageMessage(sender);
return;
}
val actionStr = args.get(0).toLowerCase();
var value = -1;
if (args.size() > 1) {
try {
value = Integer.parseInt(args.get(1));
} catch (NumberFormatException ignored) {
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
return;
}
} else {
if (actionStr.equals("unlockall")) {
unlockAllSceneTags(targetPlayer);
return;
} else if (actionStr.equals("reset") || actionStr.equals("restore")) {
resetAllSceneTags(targetPlayer);
return;
} else {
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
return;
}
}
val userVal = value;
var sceneData =
sceneTagData.values().stream().filter(sceneTag -> sceneTag.getId() == userVal).findFirst();
if (sceneData.isEmpty()) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id");
return;
}
int scene = sceneData.get().getSceneId();
switch (actionStr) {
case "add", "set" -> addSceneTag(targetPlayer, scene, value);
case "remove", "del" -> removeSceneTag(targetPlayer, scene, value);
default -> CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
}
CommandHandler.sendTranslatedMessage(sender, "commands.generic.set_to", value, actionStr);
}
private void addSceneTag(Player targetPlayer, int scene, int value) {
targetPlayer.getProgressManager().addSceneTag(scene, value);
}
private void removeSceneTag(Player targetPlayer, int scene, int value) {
targetPlayer.getProgressManager().delSceneTag(scene, value);
}
private void unlockAllSceneTags(Player targetPlayer) {
var allData = sceneTagData.values();
// Add all SceneTags
allData.stream()
.toList()
.forEach(
sceneTag -> {
targetPlayer
.getSceneTags()
.computeIfAbsent(sceneTag.getSceneId(), k -> new HashSet<>());
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
});
// Remove default SceneTags, as most are "before" or "locked" states
allData.stream()
.filter(SceneTagData::isDefaultValid)
// Only remove for big world as some other scenes only have defaults
.filter(sceneTag -> sceneTag.getSceneId() == 3)
.forEach(
sceneTag -> {
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).remove(sceneTag.getId());
});
this.setSceneTags(targetPlayer);
}
private void resetAllSceneTags(Player targetPlayer) {
targetPlayer.getSceneTags().clear();
// targetPlayer.applyStartingSceneTags(); // private
GameData.getSceneTagDataMap().values().stream()
.filter(SceneTagData::isDefaultValid)
.forEach(
sceneTag -> {
targetPlayer
.getSceneTags()
.computeIfAbsent(sceneTag.getSceneId(), k -> new HashSet<>());
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
});
this.setSceneTags(targetPlayer);
}
private void setSceneTags(Player targetPlayer) {
targetPlayer.sendPacket(new PacketPlayerWorldSceneInfoListNotify(targetPlayer));
}
}

View File

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

View File

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

View File

@ -33,9 +33,11 @@ public class ConfigContainer {
* Lua script require system if performance is a concern. * Lua script require system if performance is a concern.
* Version 12 - 'http.startImmediately' was added to control whether the * Version 12 - 'http.startImmediately' was added to control whether the
* HTTP server should start immediately. * HTTP server should start immediately.
* Version 13 - 'game.useUniquePacketKey' was added to control whether the
* encryption key used for packets is a constant or randomly generated.
*/ */
private static int version() { private static int version() {
return 12; return 13;
} }
/** /**
@ -169,6 +171,9 @@ public class ConfigContainer {
/* This is the port used in the default region. */ /* This is the port used in the default region. */
public int accessPort = 0; public int accessPort = 0;
/* Enabling this will generate a unique packet encryption key for each player. */
public boolean useUniquePacketKey = true;
/* Entities within a certain range will be loaded for the player */ /* Entities within a certain range will be loaded for the player */
public int loadEntitiesForPlayerRange = 300; public int loadEntitiesForPlayerRange = 300;
/* Start in 'unstable-quests', Lua scripts will be enabled by default. */ /* Start in 'unstable-quests', Lua scripts will be enabled by default. */

View File

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

View File

@ -214,6 +214,14 @@ public final class GameData {
private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap = private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap =
new Int2ObjectOpenHashMap<>(); new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CoopChapterData> coopChapterDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CoopPointData> coopPointDataMap =
new Int2ObjectOpenHashMap<>();
@Getter @Getter
private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>();
@ -286,10 +294,18 @@ public final class GameData {
private static final Int2ObjectMap<HomeWorldBgmData> homeWorldBgmDataMap = private static final Int2ObjectMap<HomeWorldBgmData> homeWorldBgmDataMap =
new Int2ObjectOpenHashMap<>(); new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeWorldEventData> homeWorldEventDataMap =
new Int2ObjectOpenHashMap<>();
@Getter @Getter
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;
@ -273,18 +274,20 @@ public class AbilityModifier implements Serializable {
@SerializedName( @SerializedName(
value = "amount", value = "amount",
alternate = {"PDLLIFICICJ", "cdRatio"}) alternate = {"LNFMOCKIAGK", "PDLLIFICICJ", "cdRatio"})
public DynamicFloat amount = DynamicFloat.ZERO; public DynamicFloat amount = DynamicFloat.ZERO;
@SerializedName(value = "amountByTargetCurrentHPRatio") @SerializedName(
value = "amountByTargetCurrentHPRatio",
alternate = {"GMFELAKANEF"})
public DynamicFloat amountByCasterAttackRatio = DynamicFloat.ZERO; public DynamicFloat amountByCasterAttackRatio = DynamicFloat.ZERO;
@SerializedName(value = "unused") @SerializedName(value = "unknown2")
public DynamicFloat amountByCasterCurrentHPRatio = DynamicFloat.ZERO; public DynamicFloat amountByCasterCurrentHPRatio = DynamicFloat.ZERO;
@SerializedName( @SerializedName(
value = "unknown", value = "amountByCasterMaxHPRatio",
alternate = {"HFNJHOGGFKB", "GEJGGCIOLKN"}) alternate = {"PKPBLCNMPIG", "HFNJHOGGFKB", "GEJGGCIOLKN"})
public DynamicFloat amountByCasterMaxHPRatio = DynamicFloat.ZERO; public DynamicFloat amountByCasterMaxHPRatio = DynamicFloat.ZERO;
public DynamicFloat amountByGetDamage = DynamicFloat.ZERO; public DynamicFloat amountByGetDamage = DynamicFloat.ZERO;
@ -292,7 +295,7 @@ public class AbilityModifier implements Serializable {
@SerializedName(value = "amountByTargetMaxHPRatio") @SerializedName(value = "amountByTargetMaxHPRatio")
public DynamicFloat amountByTargetCurrentHPRatio = DynamicFloat.ZERO; public DynamicFloat amountByTargetCurrentHPRatio = DynamicFloat.ZERO;
@SerializedName(value = "amountByCasterMaxHPRatio") @SerializedName(value = "unknown1", alternate = "GGLMMJHNGMO")
public DynamicFloat amountByTargetMaxHPRatio = DynamicFloat.ZERO; public DynamicFloat amountByTargetMaxHPRatio = DynamicFloat.ZERO;
public DynamicFloat limboByTargetMaxHPRatio = DynamicFloat.ZERO; public DynamicFloat limboByTargetMaxHPRatio = DynamicFloat.ZERO;
@ -325,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;
@ -367,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

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
package emu.grasscutter.data.excels;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.GameResource;
import emu.grasscutter.data.ResourceType;
import emu.grasscutter.game.home.suite.event.SuiteEventType;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@ResourceType(name = "HomeWorldEventExcelConfigData.json")
@FieldDefaults(level = AccessLevel.PRIVATE)
@Getter
public class HomeWorldEventData extends GameResource {
@SerializedName(
value = "id",
alternate = {"BBEIIPEFDPE"})
int id;
@SerializedName(
value = "eventType",
alternate = {"JOCKIMECHDP"})
SuiteEventType eventType;
int avatarID;
@SerializedName(
value = "talkId",
alternate = {"IGNJAICDFPD"})
int talkId;
int rewardID;
@SerializedName(
value = "suiteId",
alternate = {"FEHOKMJPOED"})
int suiteId;
}

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

@ -22,6 +22,7 @@ public class DungeonData extends GameResource {
private DungeonInvolveType involveType; private DungeonInvolveType involveType;
@Getter private int limitLevel; @Getter private int limitLevel;
@Getter private int passCond; @Getter private int passCond;
@Getter private int passJumpDungeon;
@Getter private int reviveMaxCount; @Getter private int reviveMaxCount;
@Getter private int settleCountdownTime; @Getter private int settleCountdownTime;
@Getter private int failSettleCountdownTime; @Getter private int failSettleCountdownTime;

View File

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

View File

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

View File

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

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

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

View File

@ -6,6 +6,7 @@ import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.Ability; import emu.grasscutter.game.ability.Ability;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.FightProperty;
import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap;
@AbilityAction(AbilityModifierAction.Type.HealHP) @AbilityAction(AbilityModifierAction.Type.HealHP)
public final class ActionHealHP extends AbilityActionHandler { public final class ActionHealHP extends AbilityActionHandler {
@ -31,24 +32,25 @@ public final class ActionHealHP extends AbilityActionHandler {
if (owner == null) return false; if (owner == null) return false;
ability // Get all properties.
.getAbilitySpecials() var properties = new Object2FloatOpenHashMap<String>();
.forEach((k, v) -> Grasscutter.getLogger().trace(">>> {}: {}", k, v)); // Add entity fight properties.
for (var property : FightProperty.values()) {
var name = property.name();
var value = owner.getFightProperty(property);
properties.put(name, value);
}
// Add ability properties.
properties.putAll(ability.getAbilitySpecials());
var amountByCasterMaxHPRatio = action.amountByCasterMaxHPRatio.get(ability); // Calculate ratios from properties.
var amountByCasterAttackRatio = action.amountByCasterAttackRatio.get(ability); var amountByCasterMaxHPRatio = action.amountByCasterMaxHPRatio.get(properties, 0);
var amountByCasterCurrentHPRatio = action.amountByCasterCurrentHPRatio.get(ability); var amountByCasterAttackRatio = action.amountByCasterAttackRatio.get(properties, 0);
var amountByTargetCurrentHPRatio = action.amountByTargetCurrentHPRatio.get(ability); var amountByCasterCurrentHPRatio = action.amountByCasterCurrentHPRatio.get(properties, 0);
var amountByTargetMaxHPRatio = action.amountByTargetMaxHPRatio.get(ability); var amountByTargetCurrentHPRatio = action.amountByTargetCurrentHPRatio.get(properties, 0);
var amountByTargetMaxHPRatio = action.amountByTargetMaxHPRatio.get(properties, 0);
Grasscutter.getLogger().trace("amountByCasterMaxHPRatio: " + amountByCasterMaxHPRatio); var amountToRegenerate = action.amount.get(properties, 0);
Grasscutter.getLogger().trace("amountByCasterAttackRatio: " + amountByCasterAttackRatio);
Grasscutter.getLogger().trace("amountByCasterCurrentHPRatio: " + amountByCasterCurrentHPRatio);
Grasscutter.getLogger().trace("amountByTargetCurrentHPRatio: " + amountByTargetCurrentHPRatio);
Grasscutter.getLogger().trace("amountByTargetMaxHPRatio: " + amountByTargetMaxHPRatio);
var amountToRegenerate = action.amount.get(ability);
Grasscutter.getLogger().trace("Base amount: " + amountToRegenerate);
amountToRegenerate += amountToRegenerate +=
amountByCasterMaxHPRatio * owner.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); amountByCasterMaxHPRatio * owner.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
@ -57,25 +59,17 @@ public final class ActionHealHP extends AbilityActionHandler {
amountToRegenerate += amountToRegenerate +=
amountByCasterCurrentHPRatio * owner.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); amountByCasterCurrentHPRatio * owner.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
Grasscutter.getLogger().trace("amountToRegenerate: " + amountToRegenerate);
var abilityRatio = 1.0f; var abilityRatio = 1.0f;
Grasscutter.getLogger().trace("Base abilityRatio: " + abilityRatio);
if (!action.ignoreAbilityProperty) if (!action.ignoreAbilityProperty)
abilityRatio += abilityRatio +=
target.getFightProperty(FightProperty.FIGHT_PROP_HEAL_ADD) target.getFightProperty(FightProperty.FIGHT_PROP_HEAL_ADD)
+ target.getFightProperty(FightProperty.FIGHT_PROP_HEALED_ADD); + target.getFightProperty(FightProperty.FIGHT_PROP_HEALED_ADD);
Grasscutter.getLogger().trace("abilityRatio: " + abilityRatio);
Grasscutter.getLogger().trace("Sub-regenerate amount: " + amountToRegenerate);
amountToRegenerate += amountToRegenerate +=
amountByTargetCurrentHPRatio * target.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); amountByTargetCurrentHPRatio * target.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
amountToRegenerate += amountToRegenerate +=
amountByTargetMaxHPRatio * target.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); amountByTargetMaxHPRatio * target.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
Grasscutter.getLogger().trace("Healing {} without ratios", amountToRegenerate);
target.heal( target.heal(
amountToRegenerate * abilityRatio * action.healRatio.get(ability, 1f), amountToRegenerate * abilityRatio * action.healRatio.get(ability, 1f),
action.muteHealEffect); action.muteHealEffect);

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

@ -8,10 +8,9 @@ import emu.grasscutter.game.player.*;
import emu.grasscutter.game.props.*; import emu.grasscutter.game.props.*;
import emu.grasscutter.net.proto.ActivityInfoOuterClass; import emu.grasscutter.net.proto.ActivityInfoOuterClass;
import emu.grasscutter.server.packet.send.PacketActivityScheduleInfoNotify; import emu.grasscutter.server.packet.send.PacketActivityScheduleInfoNotify;
import lombok.Getter;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import lombok.Getter;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Getter @Getter

View File

@ -80,7 +80,7 @@ public class TrialAvatarActivityHandler extends ActivityHandler {
if (!player if (!player
.getServer() .getServer()
.getDungeonSystem() .getDungeonSystem()
.enterDungeon(player, enterPointId, getTrialActivityDungeonId(trialAvatarIndexId))) .enterDungeon(player, enterPointId, getTrialActivityDungeonId(trialAvatarIndexId), true))
return false; return false;
setSelectedTrialAvatarIndex(trialAvatarIndexId); setSelectedTrialAvatarIndex(trialAvatarIndexId);

View File

@ -1,5 +1,7 @@
package emu.grasscutter.game.avatar; package emu.grasscutter.game.avatar;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
@ -30,14 +32,11 @@ import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.helpers.ProtoHelper; import emu.grasscutter.utils.helpers.ProtoHelper;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.*;
import org.bson.types.ObjectId;
import javax.annotation.*;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS; import lombok.*;
import org.bson.types.ObjectId;
@Entity(value = "avatars", useDiscriminator = false) @Entity(value = "avatars", useDiscriminator = false)
public class Avatar { public class Avatar {

View File

@ -38,6 +38,7 @@ public final class DungeonManager {
private boolean ended = false; private boolean ended = false;
private int newestWayPoint = 0; private int newestWayPoint = 0;
@Getter private int startSceneTime = 0; @Getter private int startSceneTime = 0;
@Setter @Getter private boolean towerDungeon = false;
DungeonTrialTeam trialTeam = null; DungeonTrialTeam trialTeam = null;
@ -67,6 +68,10 @@ public final class DungeonManager {
} }
if (isFinishedSuccessfully()) { if (isFinishedSuccessfully()) {
// Set ended now because calling EVENT_DUNGEON_SETTLE
// during finishDungeon() may cause reentrance into
// this function, leading to double settles.
ended = true;
finishDungeon(); finishDungeon();
} }
} }
@ -77,9 +82,14 @@ public final class DungeonManager {
} }
public int getLevelForMonster(int id) { public int getLevelForMonster(int id) {
if (isTowerDungeon()) {
// Tower dungeons have their own level setting in TowerLevelData
return scene.getPlayers().get(0).getTowerManager().getCurrentMonsterLevel();
} else {
// TODO should use levelConfigMap? and how? // TODO should use levelConfigMap? and how?
return dungeonData.getShowLevel(); return dungeonData.getShowLevel();
} }
}
public boolean activateRespawnPoint(int pointId) { public boolean activateRespawnPoint(int pointId) {
val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId); val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId);
@ -282,6 +292,16 @@ public final class DungeonManager {
// Call PlayerFinishDungeonEvent. // Call PlayerFinishDungeonEvent.
new PlayerFinishDungeonEvent(this.getScene().getPlayers(), this.getScene(), this).call(); new PlayerFinishDungeonEvent(this.getScene().getPlayers(), this.getScene(), this).call();
// jump players to next dungeon if available
if (this.dungeonData.getPassJumpDungeon() != 0) {
for (var player : this.getScene().getPlayers()) {
player
.getServer()
.getDungeonSystem()
.enterDungeon(player, 0, this.dungeonData.getPassJumpDungeon(), false);
}
}
} }
public void quitDungeon() { public void quitDungeon() {
@ -313,15 +333,31 @@ public final class DungeonManager {
p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON); p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON);
} }
}); });
var future =
scene scene
.getScriptManager() .getScriptManager()
.callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0)); .callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0));
// Note: There is a possible race condition with calling
// EVENT_DUNGEON_SETTLE here asynchronously:
// 1. EVENT_DUNGEON_SETTLE triggers some Lua-side logic,
// which may happen after 2 (below) finishes.
// 2. Some DungeonSettleListener could be comparing some
// Lua variable before its setting in 1 (above) finishes.
// For safety, ensure all events have finished before returning.
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
} }
public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) { public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) {
if (scene.getDungeonSettleListeners() != null) { if (scene.getDungeonSettleListeners() != null) {
scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason)); scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason));
} }
if (isTowerDungeon()) {
scene.getPlayers().get(0).getTowerManager().onEnd();
}
ended = true; ended = true;
} }

View File

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

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

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() {
@ -80,9 +83,16 @@ public class WorldChallenge {
return; return;
} }
this.progress = true; this.progress = true;
this.startedAt = System.currentTimeMillis(); this.startedAt = getScene().getSceneTimeSeconds();
getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this));
challengeTriggers.forEach(t -> t.onBegin(this)); challengeTriggers.forEach(t -> t.onBegin(this));
var player = scene.getPlayers().get(0);
var dungeonManager = scene.getDungeonManager();
var towerManager = player.getTowerManager();
if (dungeonManager != null && dungeonManager.isTowerDungeon() && towerManager != null) {
towerManager.onBegin();
}
} }
public void done() { public void done() {
@ -136,6 +146,10 @@ public class WorldChallenge {
this.progress = false; this.progress = false;
this.success = success; this.success = success;
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt)); this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt));
// Despawn all leftover mobs in this challenge's SceneGroup
getScene().getScriptManager().removeMonstersInGroup(group);
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
} }
@ -143,6 +157,20 @@ public class WorldChallenge {
return score.incrementAndGet(); return score.incrementAndGet();
} }
public int getGuardEntityHpPercent() {
if (guardEntity == null) {
Grasscutter.getLogger()
.warn(
"getGuardEntityHpPercent: Could not find guardEntity for this challenge = {}", this);
return 100;
}
var curHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId());
var maxHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
int percent = (int) (curHp * 100 / maxHp);
return percent;
}
public void onMonsterDeath(EntityMonster monster) { public void onMonsterDeath(EntityMonster monster) {
if (!inProgress()) { if (!inProgress()) {
return; return;

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

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

View File

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

View File

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

View File

@ -67,6 +67,11 @@ public class EntityAvatar extends GameEntity {
} }
this.initAbilities(); this.initAbilities();
// New EntityAvatar instances are created on every scene transition.
// Ensure that isDead is properly carried over between scenes.
// Otherwise avatars could have 0 HP but not considered dead.
this.checkIfDead();
} }
@Override @Override
@ -88,11 +93,6 @@ public class EntityAvatar extends GameEntity {
return getPlayer().getRotation(); return getPlayer().getRotation();
} }
@Override
public boolean isAlive() {
return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f;
}
@Override @Override
public Int2FloatMap getFightProperties() { public Int2FloatMap getFightProperties() {
return getAvatar().getFightProperties(); return getAvatar().getFightProperties();
@ -137,13 +137,19 @@ public class EntityAvatar extends GameEntity {
@Override @Override
public float heal(float amount, boolean mute) { public float heal(float amount, boolean mute) {
// Do not heal character if they are dead // Do not heal character if they are dead.
if (!this.isAlive()) { var currentHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
if (currentHp <= 0) {
return 0f; return 0f;
} }
float healed = super.heal(amount, mute); // Check if the character hasn't been marked as dead.
if (currentHp > 0 && this.isDead()) {
this.setDead(false);
mute = false;
}
float healed = super.heal(amount, mute);
if (healed > 0f) { if (healed > 0f) {
getScene() getScene()
.broadcastPacket( .broadcastPacket(

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));
@ -256,6 +276,9 @@ public class EntityGadget extends EntityBaseGadget {
var route = this.getScene().getSceneRouteById(configRoute.getRouteId()); var route = this.getScene().getSceneRouteById(configRoute.getRouteId());
if (route != null) { if (route != null) {
var points = route.getPoints(); var points = route.getPoints();
if (configRoute.getStartIndex() == points.length - 1) {
configRoute.setStartIndex(0);
}
val currIndex = configRoute.getStartIndex(); val currIndex = configRoute.getStartIndex();
Position prevpos; Position prevpos;
@ -301,6 +324,9 @@ public class EntityGadget extends EntityBaseGadget {
} }
configRoute.setStartIndex(I); configRoute.setStartIndex(I);
this.position.set(points[I].getPos()); this.position.set(points[I].getPos());
if (I == points.length - 1) {
configRoute.setStarted(false);
}
}, },
(int) time)); (int) time));
} }

View File

@ -8,6 +8,7 @@ import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.VisionTypeOuterClass; import emu.grasscutter.net.proto.VisionTypeOuterClass;
import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify; import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
import emu.grasscutter.server.packet.send.PacketSceneEntityDisappearNotify; import emu.grasscutter.server.packet.send.PacketSceneEntityDisappearNotify;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.Getter; import lombok.Getter;
public class EntityHomeAnimal extends EntityMonster implements Rebornable { public class EntityHomeAnimal extends EntityMonster implements Rebornable {
@ -15,10 +16,10 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
private final Position rebornPos; private final Position rebornPos;
@Getter private final int rebirth; @Getter private final int rebirth;
@Getter private final int rebirthCD; @Getter private final int rebirthCD;
private boolean disappeared; private final AtomicBoolean disappeared = new AtomicBoolean();
public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos) { public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos, Position rot) {
super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, 1); super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, rot, 1);
this.rebornPos = pos.clone(); this.rebornPos = pos.clone();
this.rebirth = data.getIsRebirth(); this.rebirth = data.getIsRebirth();
@ -60,13 +61,13 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
new PacketSceneEntityDisappearNotify( new PacketSceneEntityDisappearNotify(
this, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); this, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE));
this.rebornCDTickCount = this.getRebornCD(); this.rebornCDTickCount = this.getRebornCD();
this.disappeared = true; this.disappeared.set(true);
} }
@Override @Override
public void reborn() { public void reborn() {
if (this.disappeared) { if (this.disappeared.get()) {
this.disappeared = false; this.disappeared.set(false);
this.getPosition().set(this.getRebornPos()); this.getPosition().set(this.getRebornPos());
this.getScene().broadcastPacket(new PacketSceneEntityAppearNotify(this)); this.getScene().broadcastPacket(new PacketSceneEntityAppearNotify(this));
} }
@ -74,6 +75,6 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
@Override @Override
public boolean isInCD() { public boolean isInCD() {
return this.disappeared; return this.disappeared.get();
} }
} }

View File

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

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,12 @@ 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)
@Getter
private boolean isDead = false;
// Lua controller for specific actions // Lua controller for specific actions
@Getter @Setter private EntityController entityController; @Getter @Setter private EntityController entityController;
@ -63,7 +70,7 @@ public abstract class GameEntity {
} }
public boolean isAlive() { public boolean isAlive() {
return true; return !this.isDead;
} }
public LifeState getLifeState() { public LifeState getLifeState() {
@ -106,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())
@ -163,21 +185,29 @@ public abstract class GameEntity {
return; // If the event is canceled, do not damage the entity. return; // If the event is canceled, do not damage the entity.
} }
float effectiveDamage = 0;
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) { if (limbo) {
// Add negative HP to the current HP property. float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage())); float curRatio = curHp / maxHp;
if (curRatio > limboHpThreshold) {
// OK if this hit takes HP below threshold.
effectiveDamage = event.getDamage();
} }
if (effectiveDamage >= curHp && limboHpThreshold > .0f) {
// Don't let entity die while in limbo.
effectiveDamage = curHp - 1;
}
} else if (curHp != Float.POSITIVE_INFINITY && !lockHP
|| lockHP && curHp <= event.getDamage()) {
effectiveDamage = event.getDamage();
}
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -effectiveDamage);
this.lastAttackType = attackType; this.lastAttackType = attackType;
this.checkIfDead();
// Check if dead
boolean isDead = false;
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
isDead = true;
}
this.runLuaCallbacks(event); this.runLuaCallbacks(event);
// Packets // Packets
@ -186,11 +216,22 @@ public abstract class GameEntity {
new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
// Check if dead. // Check if dead.
if (isDead) { if (this.isDead) {
this.getScene().killEntity(this, killerId); this.getScene().killEntity(this, killerId);
} }
} }
public void checkIfDead() {
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
return;
}
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
this.isDead = true;
}
}
/** /**
* Runs the Lua callbacks for {@link EntityDamageEvent}. * Runs the Lua callbacks for {@link EntityDamageEvent}.
* *
@ -330,6 +371,8 @@ public abstract class GameEntity {
if (entityController != null) { if (entityController != null) {
entityController.onDie(this, getLastAttackType()); entityController.onDie(this, getLastAttackType());
} }
this.isDead = true;
} }
/** Invoked when a global ability value is updated. */ /** Invoked when a global ability value is updated. */

View File

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

View File

@ -9,14 +9,16 @@ 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.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -35,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;
@ -55,6 +61,7 @@ public class GameHome {
Set<Integer> unlockedHomeBgmList; Set<Integer> unlockedHomeBgmList;
int enterHomeOption; int enterHomeOption;
Map<Integer, Set<Integer>> finishedTalkIdMap; Map<Integer, Set<Integer>> finishedTalkIdMap;
Set<Integer> finishedRewardEventIdSet;
public static GameHome getByUid(Integer uid) { public static GameHome getByUid(Integer uid) {
var home = DatabaseHelper.getHomeByUid(uid); var home = DatabaseHelper.getHomeByUid(uid);
@ -62,7 +69,9 @@ public class GameHome {
home = GameHome.create(uid); home = GameHome.create(uid);
} }
home.reassignIfNull();
home.fixMainHouseIfOld(); home.fixMainHouseIfOld();
home.syncHomeAvatarCostume();
return home; return home;
} }
@ -79,9 +88,19 @@ public class GameHome {
.mainHouseMap(new ConcurrentHashMap<>()) .mainHouseMap(new ConcurrentHashMap<>())
.unlockedHomeBgmList(new HashSet<>()) .unlockedHomeBgmList(new HashSet<>())
.finishedTalkIdMap(new HashMap<>()) .finishedTalkIdMap(new HashMap<>())
.finishedRewardEventIdSet(new HashSet<>())
.build(); .build();
} }
// avoid NPE caused by database remover.
private void reassignIfNull() {
this.getSceneMap().values().stream()
.map(HomeSceneItem::getBlockItems)
.map(Map::values)
.flatMap(Collection::stream)
.forEach(HomeBlockItem::reassignIfNull);
}
// Data fixer. // Data fixer.
private void fixMainHouseIfOld() { private void fixMainHouseIfOld() {
if (this.getMainHouseMap() == null) { if (this.getMainHouseMap() == null) {
@ -97,6 +116,18 @@ public class GameHome {
this.save(); this.save();
} }
private void syncHomeAvatarCostume() {
Stream.of(this.sceneMap, this.mainHouseMap)
.map(ConcurrentHashMap::values)
.flatMap(Collection::stream)
.map(HomeSceneItem::getBlockItems)
.map(Map::values)
.flatMap(Collection::stream)
.map(HomeBlockItem::getDeployNPCList)
.flatMap(Collection::stream)
.forEach(npc -> npc.setCostumeId(this.getPlayer().getCostumeFrom(npc.getAvatarId())));
}
public void save() { public void save() {
DatabaseHelper.saveHome(this); DatabaseHelper.saveHome(this);
} }
@ -113,12 +144,12 @@ public class GameHome {
if (defaultItem != null) { if (defaultItem != null) {
Grasscutter.getLogger() Grasscutter.getLogger()
.info("Set player {} home {} to initial setting", ownerUid, sceneId); .info("Set player {} home {} to initial setting", ownerUid, sceneId);
return HomeSceneItem.parseFrom(defaultItem, sceneId);
} else { } else {
// Realm res missing bricks account, use default realm data to allow main house // Realm res missing bricks account, use default realm data to allow main house
defaultItem = GameData.getHomeworldDefaultSaveData().get(2001); defaultItem = GameData.getHomeworldDefaultSaveData().get(2001);
return HomeSceneItem.parseFrom(defaultItem, sceneId);
} }
return HomeSceneItem.parseFrom(defaultItem, sceneId);
}); });
} }
@ -149,10 +180,13 @@ public class GameHome {
this.getMainHouseMap().remove(outdoor); // delete main house in current scene. this.getMainHouseMap().remove(outdoor); // delete main house in current scene.
this.getMainHouseItem(outdoor); // put new main house with default arrangement. this.getMainHouseItem(outdoor); // put new main house with default arrangement.
this.save(); this.save();
this.getPlayer().getCurHomeWorld().getModuleManager().refreshMainHouse();
} }
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));
@ -160,10 +194,41 @@ public class GameHome {
player.getSession().send(new PacketHomeMarkPointNotify(player)); player.getSession().send(new PacketHomeMarkPointNotify(player));
player.getSession().send(new PacketHomeAvatarTalkFinishInfoNotify(player)); player.getSession().send(new PacketHomeAvatarTalkFinishInfoNotify(player));
player.getSession().send(new PacketHomeAllUnlockedBgmIdListNotify(player)); player.getSession().send(new PacketHomeAllUnlockedBgmIdListNotify(player));
player.getSession().send(new PacketHomeAvatarRewardEventNotify(player));
player.getSession().send(new PacketHomeAvatarAllFinishRewardNotify(player));
checkAccumulatedResources(player); checkAccumulatedResources(player);
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(
@ -209,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<>();
} }
@ -218,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();
@ -226,6 +290,20 @@ public class GameHome {
.toList(); .toList();
} }
public boolean onClaimAvatarRewards(int eventId) {
if (this.finishedRewardEventIdSet == null) {
this.finishedRewardEventIdSet = new HashSet<>();
}
var success = this.finishedRewardEventIdSet.add(eventId);
this.save();
return success;
}
public boolean isRewardEventFinished(int eventId) {
return this.finishedRewardEventIdSet != null && this.finishedRewardEventIdSet.contains(eventId);
}
public boolean addUnlockedHomeBgm(int homeBgmId) { public boolean addUnlockedHomeBgm(int homeBgmId) {
if (!getUnlockedHomeBgmList().add(homeBgmId)) return false; if (!getUnlockedHomeBgmList().add(homeBgmId)) return false;
@ -404,7 +482,7 @@ public class GameHome {
newCoin = storedCoin + owedCoin; newCoin = storedCoin + owedCoin;
} }
// Ensure max is not exceeded // Ensure max is not exceeded
storedCoin = (maxCoin >= newCoin) ? newCoin : maxCoin; storedCoin = Math.min(maxCoin, newCoin);
} }
// Update fetter exp // Update fetter exp
@ -416,7 +494,7 @@ public class GameHome {
newFetter = storedFetterExp + owedFetter; newFetter = storedFetterExp + owedFetter;
} }
// Ensure max is not exceeded // Ensure max is not exceeded
storedFetterExp = (maxFetter >= newFetter) ? newFetter : maxFetter; storedFetterExp = Math.min(maxFetter, newFetter);
} }
save(); save();

View File

@ -2,6 +2,8 @@ package emu.grasscutter.game.home;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.data.binout.HomeworldDefaultSaveData; import emu.grasscutter.data.binout.HomeworldDefaultSaveData;
import emu.grasscutter.game.home.suite.HomeSuiteItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.HomeBlockArrangementInfoOuterClass.HomeBlockArrangementInfo; import emu.grasscutter.net.proto.HomeBlockArrangementInfoOuterClass.HomeBlockArrangementInfo;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -19,6 +21,7 @@ public class HomeBlockItem {
List<HomeFurnitureItem> persistentFurnitureList; List<HomeFurnitureItem> persistentFurnitureList;
List<HomeAnimalItem> deployAnimalList; List<HomeAnimalItem> deployAnimalList;
List<HomeNPCItem> deployNPCList; List<HomeNPCItem> deployNPCList;
List<HomeSuiteItem> suiteList;
public static HomeBlockItem parseFrom(HomeworldDefaultSaveData.HomeBlock homeBlock) { public static HomeBlockItem parseFrom(HomeworldDefaultSaveData.HomeBlock homeBlock) {
// create from default setting // create from default setting
@ -37,10 +40,11 @@ public class HomeBlockItem {
.toList()) .toList())
.deployAnimalList(List.of()) .deployAnimalList(List.of())
.deployNPCList(List.of()) .deployNPCList(List.of())
.suiteList(List.of())
.build(); .build();
} }
public void update(HomeBlockArrangementInfo homeBlockArrangementInfo) { public void update(HomeBlockArrangementInfo homeBlockArrangementInfo, Player owner) {
this.blockId = homeBlockArrangementInfo.getBlockId(); this.blockId = homeBlockArrangementInfo.getBlockId();
this.deployFurnitureList = this.deployFurnitureList =
@ -60,7 +64,12 @@ public class HomeBlockItem {
this.deployNPCList = this.deployNPCList =
homeBlockArrangementInfo.getDeployNpcListList().stream() homeBlockArrangementInfo.getDeployNpcListList().stream()
.map(HomeNPCItem::parseFrom) .map(homeNpcData -> HomeNPCItem.parseFrom(homeNpcData, owner))
.toList();
this.suiteList =
homeBlockArrangementInfo.getFurnitureSuiteListList().stream()
.map(HomeSuiteItem::parseFrom)
.toList(); .toList();
} }
@ -81,15 +90,20 @@ public class HomeBlockItem {
this.persistentFurnitureList.forEach(f -> proto.addPersistentFurnitureList(f.toProto())); this.persistentFurnitureList.forEach(f -> proto.addPersistentFurnitureList(f.toProto()));
this.deployAnimalList.forEach(f -> proto.addDeployAnimalList(f.toProto())); this.deployAnimalList.forEach(f -> proto.addDeployAnimalList(f.toProto()));
this.deployNPCList.forEach(f -> proto.addDeployNpcList(f.toProto())); this.deployNPCList.forEach(f -> proto.addDeployNpcList(f.toProto()));
this.suiteList.forEach(f -> proto.addFurnitureSuiteList(f.toProto()));
return proto.build(); return proto.build();
} }
// TODO add more types (farm field and suite) // TODO implement farm field.
public List<? extends HomeMarkPointProtoFactory> getMarkPointProtoFactories() { public List<? extends HomeMarkPointProtoFactory> getMarkPointProtoFactories() {
this.reassignIfNull(); this.reassignIfNull();
return Stream.of(this.deployFurnitureList, this.persistentFurnitureList, this.deployNPCList) return Stream.of(
this.deployFurnitureList,
this.persistentFurnitureList,
this.deployNPCList,
this.suiteList)
.flatMap(Collection::stream) .flatMap(Collection::stream)
.toList(); .toList();
} }
@ -107,5 +121,8 @@ public class HomeBlockItem {
if (this.deployNPCList == null) { if (this.deployNPCList == null) {
this.deployNPCList = List.of(); this.deployNPCList = List.of();
} }
if (this.suiteList == null) {
this.suiteList = List.of();
}
} }
} }

View File

@ -0,0 +1,261 @@
package emu.grasscutter.game.home;
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.HomeAvatarSummonEvent;
import emu.grasscutter.game.home.suite.event.SuiteEventType;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify;
import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonAllEventNotify;
import emu.grasscutter.utils.Either;
import java.util.*;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class HomeModuleManager {
final Player homeOwner;
final HomeWorld homeWorld;
final GameHome home;
final int moduleId;
@Nullable final HomeScene outdoor;
@Nullable HomeScene indoor;
final List<HomeAvatarRewardEvent> rewardEvents;
final List<HomeAvatarSummonEvent> summonEvents;
public HomeModuleManager(HomeWorld homeWorld) {
this.homeOwner = homeWorld.getHost();
this.homeWorld = homeWorld;
this.home = homeWorld.getHome();
this.moduleId = this.homeOwner.getCurrentRealmId();
this.outdoor = homeWorld.getSceneById(homeWorld.getActiveOutdoorSceneId());
this.refreshMainHouse();
this.rewardEvents = Lists.newArrayList();
this.summonEvents = Collections.synchronizedList(Lists.newArrayList());
}
public void tick() {
if (this.moduleId == 0) {
return;
}
if (this.outdoor != null) {
this.outdoor.onTick();
}
if (this.indoor != null) {
this.indoor.onTick();
}
this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
}
public void refreshMainHouse() {
if (this.moduleId == 0) {
return;
}
this.indoor = this.homeWorld.getSceneById(this.homeWorld.getActiveIndoorSceneId());
}
public void onUpdateArrangement() {
this.fireAllAvatarRewardEvents();
this.cancelSummonEventsIfAvatarLeave();
}
private void fireAllAvatarRewardEvents() {
this.rewardEvents.clear();
var allBlockItems =
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
.filter(Objects::nonNull)
.map(HomeSceneItem::getBlockItems)
.map(Map::values)
.flatMap(Collection::stream)
.toList();
var suites =
allBlockItems.stream()
.map(HomeBlockItem::getSuiteList)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.distinct()
.toList();
allBlockItems.stream()
.map(HomeBlockItem::getDeployNPCList)
.flatMap(Collection::stream)
.forEach(
avatar -> {
suites.forEach(
suite -> {
var data =
SuiteEventType.HOME_AVATAR_REWARD_EVENT.getEventDataFrom(
avatar.getAvatarId(), suite.getSuiteId());
if (data == null || this.home.isRewardEventFinished(data.getId())) {
return;
}
this.rewardEvents.add(
new HomeAvatarRewardEvent(
homeOwner,
data.getId(),
data.getRewardID(),
data.getAvatarID(),
data.getSuiteId(),
suite.getGuid()));
});
});
if (this.summonEvents != null) {
var suiteIdList = this.rewardEvents.stream().map(HomeAvatarRewardEvent::getSuiteId).toList();
this.summonEvents.removeIf(event -> suiteIdList.contains(event.getSuiteId()));
}
}
private void cancelSummonEventsIfAvatarLeave() {
var avatars =
Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
.filter(Objects::nonNull)
.map(HomeSceneItem::getBlockItems)
.map(Map::values)
.flatMap(Collection::stream)
.map(HomeBlockItem::getDeployNPCList)
.flatMap(Collection::stream)
.map(HomeNPCItem::getAvatarId)
.toList();
this.summonEvents.removeIf(event -> !avatars.contains(event.getAvatarId()));
}
public Either<List<GameItem>, Integer> claimAvatarRewards(int eventId) {
if (this.rewardEvents.isEmpty()) {
return Either.right(Retcode.RET_FAIL_VALUE);
}
var event = this.rewardEvents.remove(0);
if (event.getEventId() != eventId) {
return Either.right(Retcode.RET_FAIL_VALUE);
}
if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
return Either.right(Retcode.RET_FAIL_VALUE);
}
return Either.left(event.giveRewards());
}
public Either<HomeAvatarSummonEvent, Integer> fireAvatarSummonEvent(
Player owner, int avatarId, int guid, int suiteId) {
HomeSuiteItem targetSuite = null;
if (owner.getScene() instanceof HomeScene homeScene) {
targetSuite =
homeScene.getSceneItem().getBlockItems().values().stream()
.map(HomeBlockItem::getSuiteList)
.flatMap(Collection::stream)
.filter(suite -> suite.getGuid() == guid)
.findFirst()
.orElse(null);
}
if (this.isInRewardEvent(avatarId)) {
return Either.right(Retcode.RET_DUPLICATE_AVATAR_VALUE);
}
if (this.rewardEvents.stream().anyMatch(event -> event.getGuid() == guid)) {
return Either.right(Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
}
this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
if (targetSuite == null) {
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
}
var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
if (eventData == null) {
return Either.right(Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
}
var event =
new HomeAvatarSummonEvent(
owner, eventData.getId(), eventData.getRewardID(), avatarId, suiteId, guid);
this.summonEvents.add(event);
owner.sendPacket(new PacketHomeAvatarSummonAllEventNotify(owner));
return Either.left(event);
}
public void onFinishSummonEvent(int eventId) {
this.summonEvents.removeIf(event -> event.getEventId() == eventId);
}
public HomeAvatarRewardEventNotify toRewardEventProto() {
var notify = HomeAvatarRewardEventNotify.newBuilder();
if (!this.rewardEvents.isEmpty()) {
notify.setRewardEvent(this.rewardEvents.get(0).toProto()).setIsEventTrigger(true);
notify.addAllPendingList(
this.rewardEvents.subList(1, this.rewardEvents.size()).stream()
.map(HomeAvatarRewardEvent::toProto)
.toList());
}
return notify.build();
}
public HomeAvatarSummonAllEventNotify toSummonEventProto() {
return HomeAvatarSummonAllEventNotify.newBuilder()
.addAllSummonEventList(
this.summonEvents.stream().map(HomeAvatarSummonEvent::toProto).toList())
.build();
}
public boolean isInRewardEvent(int avatarId) {
return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
}
@Nullable public HomeSceneItem getOutdoorSceneItem() {
return this.outdoor == null ? null : this.outdoor.getSceneItem();
}
@Nullable public HomeSceneItem getIndoorSceneItem() {
return this.indoor == null ? null : this.indoor.getSceneItem();
}
public void onSetModule() {
if (this.moduleId == 0) {
return;
}
if (this.outdoor != null) {
this.outdoor.addEntities(this.getOutdoorSceneItem().getAnimals(this.outdoor));
}
if (this.indoor != null) {
this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
}
this.fireAllAvatarRewardEvents();
}
public void onRemovedModule() {
if (this.moduleId == 0) {
return;
}
if (this.outdoor != null) {
this.outdoor.getEntities().clear();
}
if (this.indoor != null) {
this.indoor.getEntities().clear();
}
}
}

View File

@ -2,6 +2,7 @@ package emu.grasscutter.game.home;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Position;
import emu.grasscutter.net.proto.HomeMarkPointFurnitureDataOuterClass; import emu.grasscutter.net.proto.HomeMarkPointFurnitureDataOuterClass;
import emu.grasscutter.net.proto.HomeMarkPointNPCDataOuterClass; import emu.grasscutter.net.proto.HomeMarkPointNPCDataOuterClass;
@ -23,11 +24,12 @@ public class HomeNPCItem implements HomeMarkPointProtoFactory {
Position spawnRot; Position spawnRot;
int costumeId; int costumeId;
public static HomeNPCItem parseFrom(HomeNpcDataOuterClass.HomeNpcData homeNpcData) { public static HomeNPCItem parseFrom(HomeNpcDataOuterClass.HomeNpcData homeNpcData, Player owner) {
return HomeNPCItem.of() return HomeNPCItem.of()
.avatarId(homeNpcData.getAvatarId()) .avatarId(homeNpcData.getAvatarId())
.spawnPos(new Position(homeNpcData.getSpawnPos())) .spawnPos(new Position(homeNpcData.getSpawnPos()))
.spawnRot(new Position(homeNpcData.getSpawnRot())) .spawnRot(new Position(homeNpcData.getSpawnRot()))
.costumeId(owner.getCostumeFrom(homeNpcData.getAvatarId()))
.build(); .build();
} }

View File

@ -1,9 +1,12 @@
package emu.grasscutter.game.home; package emu.grasscutter.game.home;
import emu.grasscutter.data.excels.scene.SceneData; import emu.grasscutter.data.excels.scene.SceneData;
import emu.grasscutter.game.entity.EntityHomeAnimal;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.entity.Rebornable;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.VisionTypeOuterClass;
import emu.grasscutter.server.packet.send.PacketSceneTimeNotify; import emu.grasscutter.server.packet.send.PacketSceneTimeNotify;
public class HomeScene extends Scene { public class HomeScene extends Scene {
@ -40,10 +43,31 @@ public class HomeScene extends Scene {
.forEach(gameEntity -> gameEntity.onTick(this.getSceneTimeSeconds())); .forEach(gameEntity -> gameEntity.onTick(this.getSceneTimeSeconds()));
this.finishLoading(); this.finishLoading();
this.checkPlayerRespawn();
if (this.tickCount++ % 10 == 0) this.broadcastPacket(new PacketSceneTimeNotify(this)); if (this.tickCount++ % 10 == 0) this.broadcastPacket(new PacketSceneTimeNotify(this));
} }
public void onEnterEditModeFinish() {
this.removeEntities(
this.getEntities().values().stream()
.filter(gameEntity -> gameEntity instanceof EntityHomeAnimal)
.toList(),
VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE);
}
public void onLeaveEditMode() {
this.addEntities(this.getSceneItem().getAnimals(this));
}
@Override
public void killEntity(GameEntity target, int attackerId) {
if (target instanceof Rebornable rebornable) {
rebornable.onAiKillSelf(); // Teapot animals will not die. They will revive!
return;
}
super.killEntity(target, attackerId);
}
@Override @Override
public void checkNpcGroup() {} public void checkNpcGroup() {}

View File

@ -6,16 +6,18 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.HomeworldDefaultSaveData; import emu.grasscutter.data.binout.HomeworldDefaultSaveData;
import emu.grasscutter.game.entity.EntityHomeAnimal; import emu.grasscutter.game.entity.EntityHomeAnimal;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Position;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.HomeSceneArrangementInfoOuterClass.HomeSceneArrangementInfo; import emu.grasscutter.net.proto.HomeSceneArrangementInfoOuterClass.HomeSceneArrangementInfo;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.*; import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
@Entity @Entity
@ -49,14 +51,14 @@ public class HomeSceneItem {
.build(); .build();
} }
public void update(HomeSceneArrangementInfo arrangementInfo) { public void update(HomeSceneArrangementInfo arrangementInfo, Player owner) {
for (var blockItem : arrangementInfo.getBlockArrangementInfoListList()) { for (var blockItem : arrangementInfo.getBlockArrangementInfoListList()) {
var block = this.blockItems.get(blockItem.getBlockId()); var block = this.blockItems.get(blockItem.getBlockId());
if (block == null) { if (block == null) {
Grasscutter.getLogger().warn("Could not found the Home Block {}", blockItem.getBlockId()); Grasscutter.getLogger().warn("Could not found the Home Block {}", blockItem.getBlockId());
continue; continue;
} }
block.update(blockItem); block.update(blockItem, owner);
this.blockItems.put(blockItem.getBlockId(), block); this.blockItems.put(blockItem.getBlockId(), block);
} }
@ -84,17 +86,13 @@ public class HomeSceneItem {
} }
@Nullable public Position getTeleportPointPos(int guid) { @Nullable public Position getTeleportPointPos(int guid) {
var pos = new AtomicReference<Position>(); return this.getBlockItems().values().stream()
this.getBlockItems().values().stream()
.map(HomeBlockItem::getDeployFurnitureList) .map(HomeBlockItem::getDeployFurnitureList)
.flatMap(Collection::stream) .flatMap(Collection::stream)
.filter(homeFurnitureItem -> homeFurnitureItem.getGuid() == guid) .filter(homeFurnitureItem -> homeFurnitureItem.getGuid() == guid)
.map(HomeFurnitureItem::getSpawnPos) .map(HomeFurnitureItem::getSpawnPos)
.findFirst() .findFirst()
.ifPresent(pos::set); .orElse(null);
return pos.get();
} }
public List<EntityHomeAnimal> getAnimals(Scene scene) { public List<EntityHomeAnimal> getAnimals(Scene scene) {
@ -109,7 +107,8 @@ public class HomeSceneItem {
return new EntityHomeAnimal( return new EntityHomeAnimal(
scene, scene,
GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()), GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()),
homeAnimalItem.getSpawnPos()); homeAnimalItem.getSpawnPos(),
homeAnimalItem.getSpawnRot());
}) })
.toList(); .toList();
} }

View File

@ -3,38 +3,70 @@ 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;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.PacketDelTeamEntityNotify;
import emu.grasscutter.server.packet.send.PacketPlayerChatNotify;
import emu.grasscutter.server.packet.send.PacketPlayerGameTimeNotify;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
@Getter
public class HomeWorld extends World { public class HomeWorld extends World {
@Getter private final GameHome home; private final GameHome home;
private HomeModuleManager moduleManager;
public HomeWorld(GameServer server, Player owner) { public HomeWorld(GameServer server, Player owner) {
super(server, owner); super(server, owner);
this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid()); this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid());
server.registerHomeWorld(this); this.refreshModuleManager();
} }
@Override @Override
public void registerScene(Scene scene) { public boolean onTick() {
this.addAnimalsToScene((HomeScene) scene); if (this.moduleManager == null) {
super.registerScene(scene); return false;
}
this.moduleManager.tick();
if (this.getTickCount() % 10 == 0) {
this.getPlayers().forEach(p -> p.sendPacket(new PacketPlayerGameTimeNotify(p)));
} }
@Override if (this.isInHome(this.getHost()) && this.getTickCount() % 60 == 0) {
public void deregisterScene(Scene scene) { this.getHost().updatePlayerGameTime(this.getCurrentWorldTime());
super.deregisterScene(scene);
} }
private void addAnimalsToScene(HomeScene scene) { this.tickCount++;
scene.getSceneItem().getAnimals(scene).forEach(scene::addEntity); return false;
}
public void refreshModuleManager() {
if (this.moduleManager != null) {
this.moduleManager.onRemovedModule();
}
this.moduleManager = new HomeModuleManager(this);
this.moduleManager.onSetModule();
}
public int getActiveOutdoorSceneId() {
return this.getHost().getCurrentRealmId() + 2000;
}
public int getActiveIndoorSceneId() {
return this.isRealmIdValid()
? this.getSceneById(this.getActiveOutdoorSceneId()).getSceneItem().getRoomSceneId()
: -1;
}
public boolean isRealmIdValid() {
return this.getSceneById(this.getHost().getCurrentRealmId() + 2000) != null;
} }
@Override @Override
@ -115,11 +147,13 @@ public class HomeWorld extends World {
player.setWorld(null); player.setWorld(null);
// Remove from scene // Remove from scene
Scene scene = this.getSceneById(player.getSceneId()); var scene = this.getSceneById(player.getSceneId());
if (scene != null) {
scene.removePlayer(player); scene.removePlayer(player);
}
// Info packet for other players // Info packet for other players
if (this.getPlayers().size() > 0) { if (!this.getPlayers().isEmpty()) {
this.updatePlayerInfos(player); this.updatePlayerInfos(player);
} }
@ -135,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;
@ -188,6 +222,12 @@ public class HomeWorld extends World {
return this.getPlayers().contains(player); return this.getPlayers().contains(player);
} }
public void ifHost(Player hostOrGuest, Consumer<Player> ifHost) {
if (this.getHost().equals(hostOrGuest)) {
ifHost.accept(hostOrGuest);
}
}
public void sendPacketToHostIfOnline(BasePacket basePacket) { public void sendPacketToHostIfOnline(BasePacket basePacket) {
if (this.getHost().isOnline()) { if (this.getHost().isOnline()) {
this.getHost().sendPacket(basePacket); this.getHost().sendPacket(basePacket);

View File

@ -5,10 +5,7 @@ import emu.grasscutter.game.props.EnterReason;
import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Position;
import emu.grasscutter.game.world.World; import emu.grasscutter.game.world.World;
import emu.grasscutter.game.world.data.TeleportProperties; import emu.grasscutter.game.world.data.TeleportProperties;
import emu.grasscutter.net.proto.EnterTypeOuterClass; import emu.grasscutter.net.proto.*;
import emu.grasscutter.net.proto.OtherPlayerEnterHomeNotifyOuterClass;
import emu.grasscutter.net.proto.PlayerApplyEnterHomeResultNotifyOuterClass;
import emu.grasscutter.net.proto.RetcodeOuterClass;
import emu.grasscutter.server.event.player.PlayerEnterHomeEvent; import emu.grasscutter.server.event.player.PlayerEnterHomeEvent;
import emu.grasscutter.server.event.player.PlayerLeaveHomeEvent; import emu.grasscutter.server.event.player.PlayerLeaveHomeEvent;
import emu.grasscutter.server.event.player.PlayerTeleportEvent; import emu.grasscutter.server.event.player.PlayerTeleportEvent;
@ -142,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);
@ -215,6 +216,10 @@ public class HomeWorldMPSystem extends BaseGameSystem {
player.setCurHomeWorld(myHome); player.setCurHomeWorld(myHome);
myHome.getHome().onOwnerLogin(player); myHome.getHome().onOwnerLogin(player);
player.sendPacket(
new PacketPlayerQuitFromHomeNotify(
PlayerQuitFromHomeNotifyOuterClass.PlayerQuitFromHomeNotify.QuitReason
.BACK_TO_MY_WORLD));
player.sendPacket( player.sendPacket(
new PacketPlayerEnterSceneNotify( new PacketPlayerEnterSceneNotify(
player, player,
@ -263,6 +268,9 @@ public class HomeWorldMPSystem extends BaseGameSystem {
victim.setCurHomeWorld(myHome); victim.setCurHomeWorld(myHome);
myHome.getHome().onOwnerLogin(victim); myHome.getHome().onOwnerLogin(victim);
victim.sendPacket(
new PacketPlayerQuitFromHomeNotify(
PlayerQuitFromHomeNotifyOuterClass.PlayerQuitFromHomeNotify.QuitReason.KICK_BY_HOST));
victim.sendPacket( victim.sendPacket(
new PacketPlayerEnterSceneNotify( new PacketPlayerEnterSceneNotify(
victim, victim,

View File

@ -0,0 +1,82 @@
package emu.grasscutter.game.home.suite;
import dev.morphia.annotations.Entity;
import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
import emu.grasscutter.game.home.SpecialFurnitureType;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.net.proto.HomeFurnitureSuiteDataOuterClass;
import emu.grasscutter.net.proto.HomeMarkPointFurnitureDataOuterClass;
import emu.grasscutter.net.proto.HomeMarkPointSuiteDataOuterClass;
import java.util.List;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.Nullable;
@Entity
@Builder(builderMethodName = "of")
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class HomeSuiteItem implements HomeMarkPointProtoFactory {
public static final int SUITE_FURNITURE_ID = 377101;
int guid;
int suiteId;
Position pos;
List<Integer> includedFurnitureIndexList;
boolean isAllowSummon;
public static HomeSuiteItem parseFrom(
HomeFurnitureSuiteDataOuterClass.HomeFurnitureSuiteData data) {
return HomeSuiteItem.of()
.guid(data.getGuid())
.suiteId(data.getSuiteId())
.pos(new Position(data.getSpawnPos()))
.includedFurnitureIndexList(data.getIncludedFurnitureIndexListList())
.isAllowSummon(data.getIsAllowSummon())
.build();
}
public HomeFurnitureSuiteDataOuterClass.HomeFurnitureSuiteData toProto() {
return HomeFurnitureSuiteDataOuterClass.HomeFurnitureSuiteData.newBuilder()
.setSuiteId(this.suiteId)
.setGuid(this.guid)
.setIsAllowSummon(this.isAllowSummon)
.addAllIncludedFurnitureIndexList(this.includedFurnitureIndexList)
.setSpawnPos(this.pos.toProto())
.build();
}
@Nullable @Override
public HomeMarkPointFurnitureDataOuterClass.HomeMarkPointFurnitureData toMarkPointProto() {
return HomeMarkPointFurnitureDataOuterClass.HomeMarkPointFurnitureData.newBuilder()
.setFurnitureId(SUITE_FURNITURE_ID)
.setPos(this.pos.toProto())
.setFurnitureType(this.getType().getValue())
.setGuid(this.guid)
.setSuiteData(
HomeMarkPointSuiteDataOuterClass.HomeMarkPointSuiteData.newBuilder()
.setSuiteId(this.suiteId)
.build())
.build();
}
@Override
public SpecialFurnitureType getType() {
return SpecialFurnitureType.FurnitureSuite;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HomeSuiteItem that = (HomeSuiteItem) o;
return suiteId == that.suiteId;
}
@Override
public int hashCode() {
return Objects.hash(suiteId);
}
}

View File

@ -0,0 +1,54 @@
package emu.grasscutter.game.home.suite.event;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.utils.Utils;
import java.util.List;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public abstract class HomeAvatarEvent {
final Player homeOwner;
final int eventId;
final int rewardId;
final int avatarId;
final int suiteId;
final int guid;
final int randomPos;
public HomeAvatarEvent(
Player homeOwner, int eventId, int rewardId, int avatarId, int suiteId, int guid) {
this.homeOwner = homeOwner;
this.eventId = eventId;
this.rewardId = rewardId;
this.avatarId = avatarId;
this.suiteId = suiteId;
this.guid = guid;
this.randomPos = this.generateRandomPos();
}
public int generateRandomPos() {
return Utils.randomRange(1, 97);
}
public List<GameItem> giveRewards() {
return List.of();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HomeAvatarEvent that = (HomeAvatarEvent) o;
return eventId == that.eventId;
}
@Override
public int hashCode() {
return Objects.hash(eventId);
}
}

View File

@ -0,0 +1,37 @@
package emu.grasscutter.game.home.suite.event;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.HomeAvatarRewardEventInfoOuterClass;
import java.util.List;
public class HomeAvatarRewardEvent extends HomeAvatarEvent {
public HomeAvatarRewardEvent(
Player homeOwner, int eventId, int rewardId, int avatarId, int suiteId, int guid) {
super(homeOwner, eventId, rewardId, avatarId, suiteId, guid);
}
public HomeAvatarRewardEventInfoOuterClass.HomeAvatarRewardEventInfo toProto() {
return HomeAvatarRewardEventInfoOuterClass.HomeAvatarRewardEventInfo.newBuilder()
.setAvatarId(this.getAvatarId())
.setEventId(this.getEventId())
.setGuid(this.getGuid())
.setSuiteId(this.getSuiteId())
.setRandomPosition(this.getRandomPos())
.build();
}
@Override
public List<GameItem> giveRewards() {
var data = GameData.getRewardDataMap().get(this.getRewardId());
if (data == null) {
return List.of();
}
var rewards = data.getRewardItemList().stream().map(GameItem::new).toList();
this.getHomeOwner().getInventory().addItems(rewards, ActionReason.HomeAvatarEventReward);
return rewards;
}
}

View File

@ -0,0 +1,37 @@
package emu.grasscutter.game.home.suite.event;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.HomeAvatarSummonEventInfoOuterClass;
import emu.grasscutter.utils.Utils;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class HomeAvatarSummonEvent extends HomeAvatarEvent {
public static final int TIME_LIMIT_SECS = 240;
final int eventOverTime;
public HomeAvatarSummonEvent(
Player homeOwner, int eventId, int rewardId, int avatarId, int suiteId, int guid) {
super(homeOwner, eventId, rewardId, avatarId, suiteId, guid);
this.eventOverTime = Utils.getCurrentSeconds() + TIME_LIMIT_SECS;
}
public HomeAvatarSummonEventInfoOuterClass.HomeAvatarSummonEventInfo toProto() {
return HomeAvatarSummonEventInfoOuterClass.HomeAvatarSummonEventInfo.newBuilder()
.setAvatarId(this.getAvatarId())
.setEventId(this.getEventId())
.setGuid(this.getGuid())
.setSuitId(this.getSuiteId())
.setRandomPosition(this.getRandomPos())
.setEventOverTime(this.eventOverTime)
.build();
}
public boolean isTimeOver() {
return Utils.getCurrentSeconds() > this.eventOverTime;
}
}

View File

@ -0,0 +1,21 @@
package emu.grasscutter.game.home.suite.event;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.HomeWorldEventData;
import javax.annotation.Nullable;
public enum SuiteEventType {
HOME_AVATAR_REWARD_EVENT,
HOME_AVATAR_SUMMON_EVENT;
@Nullable public HomeWorldEventData getEventDataFrom(int avatarId, int suiteId) {
return GameData.getHomeWorldEventDataMap().values().stream()
.filter(
data ->
data.getEventType() == this
&& data.getAvatarID() == avatarId
&& data.getSuiteId() == suiteId)
.findFirst()
.orElse(null);
}
}

View File

@ -216,14 +216,14 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
} }
/** /**
* Checks to see if the player has the item in their inventory. This is exact. * Checks to see if the player has the item in their inventory. This is not exact.
* *
* @param items A map of item game IDs to their count. * @param items A map of item game IDs to their count.
* @return True if the player has the items, false otherwise. * @return True if the player has the items, false otherwise.
*/ */
public boolean hasAllItems(Collection<ItemParam> items) { public boolean hasAllItems(Collection<ItemParam> items) {
for (var item : items) { for (var item : items) {
if (!this.hasItem(item.getItemId(), item.getCount(), true)) return false; if (!this.hasItem(item.getItemId(), item.getCount(), false)) return false;
} }
return true; return true;

View File

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

View File

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

View File

@ -116,6 +116,7 @@ public class Player implements PlayerHook, FieldFetch {
@Getter private Map<Integer, ActiveCookCompoundData> activeCookCompounds; @Getter private Map<Integer, ActiveCookCompoundData> activeCookCompounds;
@Getter private Map<Integer, Integer> questGlobalVariables; @Getter private Map<Integer, Integer> questGlobalVariables;
@Getter private Map<Integer, Integer> openStates; @Getter private Map<Integer, Integer> openStates;
@Getter private Map<Integer, Set<Integer>> sceneTags;
@Getter @Setter private Map<Integer, Set<Integer>> unlockedSceneAreas; @Getter @Setter private Map<Integer, Set<Integer>> unlockedSceneAreas;
@Getter @Setter private Map<Integer, Set<Integer>> unlockedScenePoints; @Getter @Setter private Map<Integer, Set<Integer>> unlockedScenePoints;
@Getter @Setter private List<Integer> chatEmojiIdList; @Getter @Setter private List<Integer> chatEmojiIdList;
@ -175,6 +176,7 @@ public class Player implements PlayerHook, FieldFetch {
@Getter @Setter private Set<Date> moonCardGetTimes; @Getter @Setter private Set<Date> moonCardGetTimes;
@Transient @Getter private boolean paused; @Transient @Getter private boolean paused;
@Transient @Getter @Setter private Future<?> queuedTeleport;
@Transient @Getter @Setter private int enterSceneToken; @Transient @Getter @Setter private int enterSceneToken;
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE; @Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
@Transient private boolean hasSentLoginPackets; @Transient private boolean hasSentLoginPackets;
@ -244,6 +246,7 @@ public class Player implements PlayerHook, FieldFetch {
this.unlockedRecipies = new HashMap<>(); this.unlockedRecipies = new HashMap<>();
this.questGlobalVariables = new HashMap<>(); this.questGlobalVariables = new HashMap<>();
this.openStates = new HashMap<>(); this.openStates = new HashMap<>();
this.sceneTags = new HashMap<>();
this.unlockedSceneAreas = new HashMap<>(); this.unlockedSceneAreas = new HashMap<>();
this.unlockedScenePoints = new HashMap<>(); this.unlockedScenePoints = new HashMap<>();
this.chatEmojiIdList = new ArrayList<>(); this.chatEmojiIdList = new ArrayList<>();
@ -295,6 +298,7 @@ public class Player implements PlayerHook, FieldFetch {
this.codex = new PlayerCodex(this); this.codex = new PlayerCodex(this);
this.applyProperties(); this.applyProperties();
this.applyStartingSceneTags();
this.getFlyCloakList().add(140001); this.getFlyCloakList().add(140001);
this.getNameCardList().add(210001); this.getNameCardList().add(210001);
@ -587,6 +591,20 @@ public class Player implements PlayerHook, FieldFetch {
this.getProperty(PlayerProperty.PROP_DIVE_MAX_STAMINA)); this.getProperty(PlayerProperty.PROP_DIVE_MAX_STAMINA));
} }
/**
* Applies all default scenetags to the player.
*/
private void applyStartingSceneTags() {
GameData.getSceneTagDataMap().values().stream()
.filter(sceneTag -> sceneTag.isDefaultValid())
.forEach(sceneTag -> {
if (this.getSceneTags().get(sceneTag.getSceneId()) == null) {
this.getSceneTags().put(sceneTag.getSceneId(), new HashSet<>());
}
this.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
});
}
/** /**
* Applies a property to the player if it doesn't exist in the database. * Applies a property to the player if it doesn't exist in the database.
* *
@ -950,6 +968,13 @@ public class Player implements PlayerHook, FieldFetch {
this.sendPacket(new PacketAvatarGainCostumeNotify(costumeId)); this.sendPacket(new PacketAvatarGainCostumeNotify(costumeId));
} }
public int getCostumeFrom(int avatarId) {
var avatars = this.getAvatars();
avatars.loadFromDatabase();
var avatar = avatars.getAvatarById(avatarId);
return avatar == null ? 0 : avatar.getCostume();
}
public void addPersonalLine(int personalLineId) { public void addPersonalLine(int personalLineId) {
this.getPersonalLineList().add(personalLineId); this.getPersonalLineList().add(personalLineId);
session.getPlayer().getQuestManager().queueEvent(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK, personalLineId); session.getPlayer().getQuestManager().queueEvent(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK, personalLineId);
@ -1354,14 +1379,6 @@ public class Player implements PlayerHook, FieldFetch {
this.getPlayerProgress().setPlayer(this); // Add reference to the player. this.getPlayerProgress().setPlayer(this); // Add reference to the player.
} }
/**
* Invoked when the player selects their avatar.
*/
public void onPlayerBorn() {
Grasscutter.getThreadPool().submit(
this.getQuestManager()::onPlayerBorn);
}
public void onLogin() { public void onLogin() {
// Quest - Commented out because a problem is caused if you log out while this quest is active // Quest - Commented out because a problem is caused if you log out while this quest is active
/* /*
@ -1377,6 +1394,10 @@ public class Player implements PlayerHook, FieldFetch {
} }
*/ */
// Ensure the player has valid scenetags, allows old accounts to work
if (this.getSceneTags().isEmpty() || this.getSceneTags() == null) {
this.applyStartingSceneTags();
}
if (GameHome.HOME_SCENE_IDS.contains(this.getSceneId())) { if (GameHome.HOME_SCENE_IDS.contains(this.getSceneId())) {
this.setSceneId(this.prevScene <= 0 ? 3 : this.prevScene); // if the player in home, make the player go back. this.setSceneId(this.prevScene <= 0 ? 3 : this.prevScene); // if the player in home, make the player go back.
@ -1510,6 +1531,31 @@ public class Player implements PlayerHook, FieldFetch {
getServer().getPlayers().values().removeIf(player1 -> player1 == this); getServer().getPlayers().values().removeIf(player1 -> player1 == this);
} }
public void unfreezeUnlockedScenePoints(int sceneId) {
// Unfreeze previously unlocked scene points. For example,
// the first weapon mats domain needs some script interaction
// to unlock. It needs to be unfrozen when GetScenePointReq
// comes in to be interactable again.
GameData.getScenePointEntryMap().values().stream()
.filter(scenePointEntry ->
// Note: Only DungeonEntry scene points need to be unfrozen
scenePointEntry.getPointData().getType().equals("DungeonEntry")
// groupLimit says this scene point needs to be unfrozen
&& scenePointEntry.getPointData().isGroupLimit())
.forEach(scenePointEntry -> {
// If this is a previously unlocked scene point,
// send unfreeze packet.
val pointId = scenePointEntry.getPointData().getId();
if (unlockedScenePoints.get(sceneId).contains(pointId)) {
this.sendPacket(new PacketUnfreezeGroupLimitNotify(pointId, sceneId));
}
});
}
public void unfreezeUnlockedScenePoints() {
unlockedScenePoints.keySet().forEach(sceneId -> unfreezeUnlockedScenePoints(sceneId));
}
public int getLegendaryKey() { public int getLegendaryKey() {
return this.getProperty(PlayerProperty.PROP_PLAYER_LEGENDARY_KEY); return this.getProperty(PlayerProperty.PROP_PLAYER_LEGENDARY_KEY);
} }

View File

@ -12,6 +12,7 @@ import emu.grasscutter.game.quest.enums.*;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.scripts.data.ScriptArgs; import emu.grasscutter.scripts.data.ScriptArgs;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -32,12 +33,9 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
public static final Set<Integer> IGNORED_OPEN_STATES = public static final Set<Integer> IGNORED_OPEN_STATES =
Set.of( Set.of(
1404, // OPEN_STATE_MENGDE_INFUSEDCRYSTAL, causes quest 'Mine Craft' to be given to the 1404 // OPEN_STATE_MENGDE_INFUSEDCRYSTAL, causes quest 'Mine Craft' to be given to the
// player at the start of the game. // player at the start of the game.
// This should be removed when city reputation is implemented. // This should be removed when city reputation is implemented.
57 // OPEN_STATE_PERSONAL_LINE, causes the prompt for showing character hangout quests to
// be permanently shown.
// This should be removed when character story quests are implemented.
); );
// Set of open states that are set per default for all accounts. Can be overwritten by an entry in // Set of open states that are set per default for all accounts. Can be overwritten by an entry in
// `map`. // `map`.
@ -102,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);
@ -313,4 +311,28 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
player.save(); player.save();
player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount); player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount);
} }
/******************************************************************************************************************
******************************************************************************************************************
* SCENETAGS
******************************************************************************************************************
*****************************************************************************************************************/
public void addSceneTag(int sceneId, int sceneTagId) {
player.getSceneTags().computeIfAbsent(sceneId, k -> new HashSet<>()).add(sceneTagId);
player.sendPacket(new PacketPlayerWorldSceneInfoListNotify(player));
}
public void delSceneTag(int sceneId, int sceneTagId) {
// Sanity check
if (player.getSceneTags().get(sceneId) == null) {
// Can't delete something that doesn't exist
return;
}
player.getSceneTags().get(sceneId).remove(sceneTagId);
player.sendPacket(new PacketPlayerWorldSceneInfoListNotify(player));
}
public boolean checkSceneTag(int sceneId, int sceneTagId) {
return player.getSceneTags().get(sceneId).contains(sceneTagId);
}
} }

View File

@ -425,6 +425,30 @@ public final class TeamManager extends BasePlayerDataManager {
this.getPlayer().sendPacket(responsePacket); this.getPlayer().sendPacket(responsePacket);
} }
// Ensure new selected character index is alive.
// If not, change to another alive one or revive.
checkCurrentAvatarIsAlive(currentEntity);
}
public void checkCurrentAvatarIsAlive(EntityAvatar currentEntity) {
if (currentEntity == null) {
currentEntity = this.getCurrentAvatarEntity();
}
// Ensure currently selected character is still alive
if (!this.getActiveTeam().get(this.currentCharacterIndex).isAlive()) {
// Character died in a dungeon challenge...
int replaceIndex = getDeadAvatarReplacement();
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
this.currentCharacterIndex = replaceIndex;
} else {
// Team wiped in dungeon...
// Revive and change to first avatar.
this.currentCharacterIndex = 0;
this.reviveAvatar(this.getCurrentAvatarEntity().getAvatar());
}
}
// Check if character changed // Check if character changed
var newAvatarEntity = this.getCurrentAvatarEntity(); var newAvatarEntity = this.getCurrentAvatarEntity();
if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) { if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) {
@ -700,15 +724,16 @@ public final class TeamManager extends BasePlayerDataManager {
this.updateTeamEntities(null); this.updateTeamEntities(null);
} }
public void cleanTemporaryTeam() { public boolean cleanTemporaryTeam() {
// check if using temporary team // check if using temporary team
if (useTemporarilyTeamIndex < 0) { if (useTemporarilyTeamIndex < 0) {
return; return false;
} }
this.useTemporarilyTeamIndex = -1; this.useTemporarilyTeamIndex = -1;
this.temporaryTeam = null; this.temporaryTeam = null;
this.updateTeamEntities(null); this.updateTeamEntities(null);
return true;
} }
public synchronized void setCurrentTeam(int teamId) { public synchronized void setCurrentTeam(int teamId) {
@ -798,10 +823,7 @@ public final class TeamManager extends BasePlayerDataManager {
public void onAvatarDie(long dieGuid) { public void onAvatarDie(long dieGuid) {
EntityAvatar deadAvatar = this.getCurrentAvatarEntity(); EntityAvatar deadAvatar = this.getCurrentAvatarEntity();
if (deadAvatar == null || deadAvatar.getId() != dieGuid) return;
if (deadAvatar.isAlive() || deadAvatar.getId() != dieGuid) {
return;
}
PlayerDieType dieType = deadAvatar.getKilledType(); PlayerDieType dieType = deadAvatar.getKilledType();
int killedBy = deadAvatar.getKilledBy(); int killedBy = deadAvatar.getKilledBy();
@ -813,20 +835,13 @@ public final class TeamManager extends BasePlayerDataManager {
// TODO: Perhaps find a way to get vanilla experience? // TODO: Perhaps find a way to get vanilla experience?
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
} else { } else {
// Replacement avatar // Find replacement avatar
EntityAvatar replacement = null; int replaceIndex = getDeadAvatarReplacement();
int replaceIndex = -1; if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
// Set index and spawn replacement member
for (int i = 0; i < this.getActiveTeam().size(); i++) { this.setCurrentCharacterIndex(replaceIndex);
EntityAvatar entity = this.getActiveTeam().get(i); this.getPlayer().getScene().addEntity(this.getActiveTeam().get(replaceIndex));
if (entity.isAlive()) { } else {
replaceIndex = i;
replacement = entity;
break;
}
}
if (replacement == null) {
// No more living team members... // No more living team members...
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
// Invoke player team death event. // Invoke player team death event.
@ -834,10 +849,6 @@ public final class TeamManager extends BasePlayerDataManager {
new PlayerTeamDeathEvent( new PlayerTeamDeathEvent(
this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex())); this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex()));
event.call(); event.call();
} else {
// Set index and spawn replacement member
this.setCurrentCharacterIndex(replaceIndex);
this.getPlayer().getScene().addEntity(replacement);
} }
} }
@ -845,6 +856,20 @@ public final class TeamManager extends BasePlayerDataManager {
this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0)); this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0));
} }
public int getDeadAvatarReplacement() {
int replaceIndex = -1;
for (int i = 0; i < this.getActiveTeam().size(); i++) {
EntityAvatar entity = this.getActiveTeam().get(i);
if (entity.isAlive()) {
replaceIndex = i;
break;
}
}
return replaceIndex;
}
public boolean reviveAvatar(Avatar avatar) { public boolean reviveAvatar(Avatar avatar) {
for (EntityAvatar entity : this.getActiveTeam()) { for (EntityAvatar entity : this.getActiveTeam()) {
if (entity.getAvatar() == avatar) { if (entity.getAvatar() == avatar) {

View File

@ -177,7 +177,8 @@ public enum ActionReason {
ChannellerSlabLoopDungeonFirstPassReward(1090), ChannellerSlabLoopDungeonFirstPassReward(1090),
ChannellerSlabLoopDungeonScoreReward(1091), ChannellerSlabLoopDungeonScoreReward(1091),
HomeLimitedShopBuy(1092), HomeLimitedShopBuy(1092),
HomeCoinCollect(1093); HomeCoinCollect(1093),
HomeAvatarEventReward(1100);
private static final Int2ObjectMap<ActionReason> map = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<ActionReason> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, ActionReason> stringMap = new HashMap<>(); private static final Map<String, ActionReason> stringMap = new HashMap<>();

View File

@ -16,11 +16,10 @@ import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest;
import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest; import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import lombok.*;
import org.bson.types.ObjectId;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.*;
import org.bson.types.ObjectId;
@Entity(value = "quests", useDiscriminator = false) @Entity(value = "quests", useDiscriminator = false)
public class GameMainQuest { public class GameMainQuest {
@ -170,23 +169,6 @@ public class GameMainQuest {
this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED; this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED;
} }
/*
* We also need to check for unfinished childQuests in this MainQuest
* force them to complete and send a packet about this to the user,
* because at some points there are special "invisible" child quests that control
* some situations.
*
* For example, subQuest 35312 is responsible for the event of leaving the territory
* of the island with a statue and automatically returns the character back,
* quest 35311 completes the main quest line 353 and starts 35501 from
* new MainQuest 355 but if 35312 is not completed after the completion
* of the main quest 353 - the character will not be able to leave place
* (return again and again)
*/
// this.getChildQuests().values().stream()
// .filter(p -> p.state != QuestState.QUEST_STATE_FINISHED)
// .forEach(GameQuest::finish);
this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this)); this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this));
this.getOwner().getSession().send(new PacketCodexDataUpdateNotify(this)); this.getOwner().getSession().send(new PacketCodexDataUpdateNotify(this));
@ -329,11 +311,12 @@ public class GameMainQuest {
} }
/** /**
* Checks if the quest has a teleport position. Returns true if it does and adds the target position * Checks if the quest has a teleport position. Returns true if it does and adds the target
* and rotation to the list. * position and rotation to the list.
* *
* @param subId The sub-quest ID. * @param subId The sub-quest ID.
* @param posAndRot A list which will contain the position and rotation if the quest has a teleport. * @param posAndRot A list which will contain the position and rotation if the quest has a
* teleport.
* @return True if the quest has a teleport position. False otherwise. * @return True if the quest has a teleport position. False otherwise.
*/ */
public boolean hasTeleportPosition(int subId, List<Position> posAndRot) { public boolean hasTeleportPosition(int subId, List<Position> posAndRot) {
@ -383,46 +366,6 @@ public class GameMainQuest {
} }
} }
public void tryAcceptSubQuests(QuestCond condType, String paramStr, int... params) {
try {
List<GameQuest> subQuestsWithCond =
getChildQuests().values().stream()
.filter(
p ->
p.getState() == QuestState.QUEST_STATE_UNSTARTED
|| p.getState() == QuestState.UNFINISHED)
.filter(
p ->
p.getQuestData().getAcceptCond().stream()
.anyMatch(
q ->
condType == QuestCond.QUEST_COND_NONE || q.getType() == condType))
.toList();
var questSystem = owner.getServer().getQuestSystem();
for (GameQuest subQuestWithCond : subQuestsWithCond) {
var acceptCond = subQuestWithCond.getQuestData().getAcceptCond();
int[] accept = new int[acceptCond.size()];
for (int i = 0; i < subQuestWithCond.getQuestData().getAcceptCond().size(); i++) {
var condition = acceptCond.get(i);
boolean result =
questSystem.triggerCondition(
getOwner(), subQuestWithCond.getQuestData(), condition, paramStr, params);
accept[i] = result ? 1 : 0;
}
boolean shouldAccept =
LogicType.calculate(subQuestWithCond.getQuestData().getAcceptCondComb(), accept);
if (shouldAccept) subQuestWithCond.start();
}
this.save();
} catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while trying to accept quest.", e);
}
}
public void tryFailSubQuests(QuestContent condType, String paramStr, int... params) { public void tryFailSubQuests(QuestContent condType, String paramStr, int... params) {
try { try {
List<GameQuest> subQuestsWithCond = List<GameQuest> subQuestsWithCond =
@ -437,7 +380,7 @@ public class GameMainQuest {
for (GameQuest subQuestWithCond : subQuestsWithCond) { for (GameQuest subQuestWithCond : subQuestsWithCond) {
val failCond = subQuestWithCond.getQuestData().getFailCond(); val failCond = subQuestWithCond.getQuestData().getFailCond();
for (int i = 0; i < subQuestWithCond.getQuestData().getFailCond().size(); i++) { for (int i = 0; i < failCond.size(); i++) {
val condition = failCond.get(i); val condition = failCond.get(i);
if (condition.getType() == condType) { if (condition.getType() == condType) {
boolean result = boolean result =
@ -445,7 +388,7 @@ public class GameMainQuest {
.getServer() .getServer()
.getQuestSystem() .getQuestSystem()
.triggerContent(subQuestWithCond, condition, paramStr, params); .triggerContent(subQuestWithCond, condition, paramStr, params);
subQuestWithCond.getFailProgressList()[i] = result ? 1 : 0; subQuestWithCond.setFailProgress(i, result ? 1 : 0);
if (result) { if (result) {
getOwner().getSession().send(new PacketQuestProgressUpdateNotify(subQuestWithCond)); getOwner().getSession().send(new PacketQuestProgressUpdateNotify(subQuestWithCond));
} }

View File

@ -158,6 +158,13 @@ public class GameQuest {
public boolean clearProgress(boolean notifyDelete) { public boolean clearProgress(boolean notifyDelete) {
// TODO improve // TODO improve
var oldState = state; var oldState = state;
if (questData.getAcceptCond() != null && questData.getAcceptCond().size() != 0) {
this.getMainQuest()
.getQuestManager()
.getAcceptProgressLists()
.put(this.getSubQuestId(), new int[questData.getAcceptCond().size()]);
}
if (questData.getFinishCond() != null && questData.getFinishCond().size() != 0) { if (questData.getFinishCond() != null && questData.getFinishCond().size() != 0) {
for (var condition : questData.getFinishCond()) { for (var condition : questData.getFinishCond()) {
if (condition.getType() == QuestContent.QUEST_CONTENT_LUA_NOTIFY) { if (condition.getType() == QuestContent.QUEST_CONTENT_LUA_NOTIFY) {

View File

@ -1,5 +1,8 @@
package emu.grasscutter.game.quest; package emu.grasscutter.game.quest;
import static emu.grasscutter.GameConstants.DEBUG;
import static emu.grasscutter.config.Configuration.*;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*; import emu.grasscutter.data.binout.*;
@ -12,21 +15,18 @@ import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.*;
import javax.annotation.Nonnull;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import static emu.grasscutter.GameConstants.DEBUG; import lombok.*;
import static emu.grasscutter.config.Configuration.*;
public final class QuestManager extends BasePlayerManager { public final class QuestManager extends BasePlayerManager {
@Getter private final Player player; @Getter private final Player player;
@Getter private final Int2ObjectMap<GameMainQuest> mainQuests; @Getter private final Int2ObjectMap<GameMainQuest> mainQuests;
@Getter private Int2ObjectMap<int[]> acceptProgressLists;
@Getter private final List<Integer> loggedQuests; @Getter private final List<Integer> loggedQuests;
private long lastHourCheck = 0; private long lastHourCheck = 0;
@ -53,6 +53,7 @@ public final class QuestManager extends BasePlayerManager {
this.player = player; this.player = player;
this.mainQuests = new Int2ObjectOpenHashMap<>(); this.mainQuests = new Int2ObjectOpenHashMap<>();
this.loggedQuests = new ArrayList<>(); this.loggedQuests = new ArrayList<>();
this.acceptProgressLists = new Int2ObjectOpenHashMap<>();
if (DEBUG) { if (DEBUG) {
this.loggedQuests.addAll( this.loggedQuests.addAll(
@ -100,21 +101,16 @@ public final class QuestManager extends BasePlayerManager {
* Attempts to add the giving action. * Attempts to add the giving action.
* *
* @param givingId The giving action ID. * @param givingId The giving action ID.
* @throws IllegalStateException If the giving action is already active.
*/ */
public void addGiveItemAction(int givingId) throws IllegalStateException { public void addGiveItemAction(int givingId) throws IllegalStateException {
var progress = this.player.getPlayerProgress(); var progress = this.player.getPlayerProgress();
var givings = progress.getItemGivings(); var givings = progress.getItemGivings();
// Check if the action is already present. // Check if the action is not present.
if (givings.containsKey(givingId)) { if (!givings.containsKey(givingId)) {
throw new IllegalStateException("Giving action " + givingId + " is already active.");
}
// Add the action.
givings.put(givingId, ItemGiveRecord.resolve(givingId)); givings.put(givingId, ItemGiveRecord.resolve(givingId));
// Save the givings.
player.save(); player.save();
}
this.sendGivingRecords(); this.sendGivingRecords();
} }
@ -225,14 +221,11 @@ public final class QuestManager extends BasePlayerManager {
this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords())); this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords()));
} }
public void onPlayerBorn() { public void onLogin() {
if (this.isQuestingEnabled()) { if (this.isQuestingEnabled()) {
this.enableQuests(); this.enableQuests();
this.sendGivingRecords(); this.sendGivingRecords();
} }
}
public void onLogin() {
List<GameMainQuest> activeQuests = getActiveMainQuests(); List<GameMainQuest> activeQuests = getActiveMainQuests();
List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size()); List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size());
@ -302,6 +295,17 @@ public final class QuestManager extends BasePlayerManager {
} }
public void enableQuests() { public void enableQuests() {
GameData.getBeginCondQuestMap()
.keySet()
.forEach(
x -> {
if (x.contains("QUEST_COND_STATE_NOT_EQUAL"))
this.triggerEvent(
QuestCond.QUEST_COND_STATE_NOT_EQUAL, null, Integer.parseInt(x.substring(26)));
if (x.contains("QUEST_COND_STATE_EQUAL"))
this.triggerEvent(
QuestCond.QUEST_COND_STATE_EQUAL, null, Integer.parseInt(x.substring(22)));
});
this.triggerEvent(QuestCond.QUEST_COND_NONE, null, 0); this.triggerEvent(QuestCond.QUEST_COND_NONE, null, 0);
this.triggerEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, null, 1); this.triggerEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, null, 1);
} }
@ -485,8 +489,6 @@ public final class QuestManager extends BasePlayerManager {
eventExecutor.submit(() -> triggerEvent(condType, paramStr, params)); eventExecutor.submit(() -> triggerEvent(condType, paramStr, params));
} }
// QUEST_EXEC are handled directly by each subQuest
public void triggerEvent(QuestCond condType, String paramStr, int... params) { public void triggerEvent(QuestCond condType, String paramStr, int... params) {
Grasscutter.getLogger().trace("Trigger Event {}, {}, {}", condType, paramStr, params); Grasscutter.getLogger().trace("Trigger Event {}, {}, {}", condType, paramStr, params);
var potentialQuests = GameData.getQuestDataByConditions(condType, params[0], paramStr); var potentialQuests = GameData.getQuestDataByConditions(condType, params[0], paramStr);
@ -503,15 +505,19 @@ public final class QuestManager extends BasePlayerManager {
return; return;
} }
val acceptCond = questData.getAcceptCond(); val acceptCond = questData.getAcceptCond();
int[] accept = new int[acceptCond.size()]; acceptProgressLists.putIfAbsent(questData.getId(), new int[acceptCond.size()]);
for (int i = 0; i < acceptCond.size(); i++) { for (int i = 0; i < acceptCond.size(); i++) {
val condition = acceptCond.get(i); val condition = acceptCond.get(i);
if (condition.getType() == condType) {
boolean result = boolean result =
questSystem.triggerCondition(owner, questData, condition, paramStr, params); questSystem.triggerCondition(owner, questData, condition, paramStr, params);
accept[i] = result ? 1 : 0; acceptProgressLists.get(questData.getId())[i] = result ? 1 : 0;
}
} }
boolean shouldAccept = LogicType.calculate(questData.getAcceptCondComb(), accept); boolean shouldAccept =
LogicType.calculate(
questData.getAcceptCondComb(), acceptProgressLists.get(questData.getId()));
if (this.loggedQuests.contains(questData.getId())) { if (this.loggedQuests.contains(questData.getId())) {
Grasscutter.getLogger() Grasscutter.getLogger()
.debug( .debug(
@ -523,7 +529,7 @@ public final class QuestManager extends BasePlayerManager {
Arrays.stream(params) Arrays.stream(params)
.mapToObj(String::valueOf) .mapToObj(String::valueOf)
.collect(Collectors.joining(", "))); .collect(Collectors.joining(", ")));
for (var i = 0; i < accept.length; i++) { for (var i = 0; i < acceptCond.size(); i++) {
var condition = acceptCond.get(i); var condition = acceptCond.get(i);
Grasscutter.getLogger() Grasscutter.getLogger()
.debug( .debug(
@ -533,14 +539,13 @@ public final class QuestManager extends BasePlayerManager {
.filter(value -> value > 0) .filter(value -> value > 0)
.mapToObj(String::valueOf) .mapToObj(String::valueOf)
.collect(Collectors.joining(", ")), .collect(Collectors.joining(", ")),
accept[i] == 1 ? "success" : "failure"); acceptProgressLists.get(questData.getId())[i] == 1 ? "success" : "failure");
} }
} }
if (shouldAccept) { if (shouldAccept) {
GameQuest quest = owner.getQuestManager().addQuest(questData); GameQuest quest = owner.getQuestManager().addQuest(questData);
Grasscutter.getLogger() Grasscutter.getLogger().debug("Added quest {}", questData.getSubId());
.debug("Added quest {} result {}", questData.getSubId(), quest != null);
} }
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,11 @@ import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.quest.enums.QuestContent;
@QuestValueContent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING) @QuestValueContent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING)
public final class ContentFinishGivingItem extends BaseContent { public final class ContentFinishItemGiving extends BaseContent {
@Override @Override
public boolean execute( public boolean execute(
GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) { GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
return condition.getParam()[0] == params[0] && condition.getParam()[1] == params[1]; return condition.getParam()[0] == params[0]
&& (condition.getParam()[1] == 0 || condition.getParam()[1] == params[1]);
} }
} }

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