diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..8457bf7ac --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: "Build" +on: + push: + branches: + - "stable" +jobs: + Build-Server-Jar: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: '8' + - name: Run Gradle + run: .\gradlew.bat && .\gradlew jar + - name: Upload build + uses: actions/upload-artifact@v3 + with: + name: Grasscutter + path: grasscutter.jar + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..fbab9b375 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Grasscutter-Protos"] + path = Grasscutter-Protos + url = https://github.com/Melledy/Grasscutter-Protos diff --git a/Grasscutter-Protos b/Grasscutter-Protos new file mode 160000 index 000000000..0537e9cc4 --- /dev/null +++ b/Grasscutter-Protos @@ -0,0 +1 @@ +Subproject commit 0537e9cc4c7856a7c6f88bbbaa908a80c4ee677e diff --git a/README.md b/README.md index 11f694599..853c37ae3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ # Grasscutter -A WIP server emulator for Genshin Impact 2.3-2.6 +A WIP server reimplementation for *some anime game* 2.3-2.6 +**Documentation**: [Grasscutter Wiki](https://github.com/Melledy/Grasscutter/wiki/) **Note**: For support please join the [Discord server](https://discord.gg/T5vZU6UyeG). - # Current features * Logging in -* Spawning monsters via console * Combat +* Spawning monsters via console * Inventory features (recieving items/characters, upgrading items/characters, etc) -* Co-op does work, but movement is kind of buggy and some player ults do not spawn properly -* Friends list * Gacha system - +* Friends list +* Co-op *partially* work # Quick setup guide -* For more information, we now have [Grasscutter Wiki](https://github.com/Melledy/Grasscutter/wiki/) page ! +### Note +* If you update from an older version, delete `config.json` for regeneration + ### Prerequisites * JDK-8u202 ([mirror link](https://mirrors.huaweicloud.com/java/jdk/8u202-b08/) since Oracle required an account to download old builds) * Mongodb (recommended 4.0+) @@ -22,13 +23,13 @@ A WIP server emulator for Genshin Impact 2.3-2.6 ### Starting up Grasscutter server (Assuming you are on Windows) 1. Setup compile environment `gradlew.bat` 2. Compile Grasscutter with `gradlew jar` -3. Create a folder named `resources` in your Grasscutter directory, bring your `BinOutput` and `ExcelBinOutput` folders into it *(Check the wiki for more details where to get those.)* -4. Run Grasscutter with `java -jar grasscutter.jar`. Make sure mongodb is running as well. +3. Create a folder named `resources` in your Grasscutter directory, bring your `BinOutput` and `ExcelBinOutput` folders into it *(Check the wiki for more details how to get those.)* +4. Run Grasscutter with `java -jar grasscutter.jar`. Make sure mongodb service is running as well. ### Connecting with the client -½. Create an account using command below +½. Create an account using *server console command* below 1. Run a proxy daemon: (choose either one) - - mitmdump: `mitmdump -s proxy.py --ssl-insecure` + - mitmdump: `mitmdump -s proxy.py -k` - Fiddler Classic: Run Fiddler Classic, turn on `Decrypt https traffic` in setting and change the default port there (Tools -> Options -> Connections) to anything other than `8888`, and load [this script](https://github.lunatic.moe/fiddlerscript). - [Hosts file](https://github.com/Melledy/Grasscutter/wiki/Running#traffic-route-map) 2. Trust CA certificate: @@ -39,35 +40,40 @@ A WIP server emulator for Genshin Impact 2.3-2.6 * or you can use `run.cmd` to start Server & Proxy daemon with one click # Grasscutter commands -### Server console commands +There is a dummy user named "Server" in every player's friends list that you can message to use commands. Commands also work in other chat rooms, such as private/team chats. `account create [username] {playerid}` - Creates an account with the specified username and the in-game uid for that account. The playerid parameter is optional and will be auto generated if not set. -### In-Game commands -There is a dummy user named "Server" in every player's friends list that you can message to use commands. Commands also work in other chat rooms, such as private/team chats. +`spawn [monster id] [level] [amount]` -`!spawn [monster id] [level] [amount]` +`give [item id] [amount]` -`!give [item id] [amount]` +`givechar [avatar id] [level]` -`!givechar [avatar id] [level]` +`drop [item id] [amount]` -`!drop [item id] [amount]` +`killall` -`!killall` +`setworldlevel [level]` - Relog to see effects properly -`!setworldlevel [level]` - Relog to see effects properly +`godmode` - Prevents you from taking damage -`!godmode` - Prevents you from taking damage +`resetconst` - Resets the constellation level on your current active character, will need to relog after using the command to see any changes. -`!resetconst` - Resets the constellation level on your current active character, will need to relog after using the command to see any changes. +`setstats [stats] [amount]` - Changes the current character's specified stat. -`!sethp [hp]` +`clearartifacts` - Deletes all unequipped and unlocked level 0 artifacts, **including yellow rarity ones** from your inventory -`!clearartifacts` - Deletes all unequipped and unlocked level 0 artifacts, **including yellow rarity ones** from your inventory +`pos` - Gets your current coordinate. + +`weather [weather id] [climate id]` - Changes the current weather. + +*More commands will be updated in the [wiki](https://github.com/Melledy/Grasscutter/wiki/).* + +### Bonus +When you want to teleport to somewhere, use the ingame marking function on Map, click Confirm. You will see your character falling from a very high destination, exact location that you marked. # Quick Troubleshooting -* If compiling wasnt successful, please check your JDK installation (must be JDK 8 and validated JDK's bin PATH variable) -* My client doesn't connect, doesn't login, 4206, etc... - Mostly your proxy daemon setup is the issue, if using Fiddler make sure it running on another port except 8888 +* If compiling wasn't successful, please check your JDK installation (must be JDK 8 and validated JDK's bin PATH variable) +* My client doesn't connect, doesn't login, 4206, etc... - Mostly your proxy daemon setup is *the issue*, if using Fiddler make sure it running on another port except 8888 * Startup sequence: Mongodb > Grasscutter > Proxy daemon (mitmdump, fiddler, etc.) > Client -* If `4206` error constantly prompt up, try to use [jdk-8u202-b08](https://mirrors.huaweicloud.com/java/jdk/8u202-b08/) instead of other versions of JDK diff --git a/build.gradle b/build.gradle index c7c9eae79..97477456a 100644 --- a/build.gradle +++ b/build.gradle @@ -23,19 +23,21 @@ repositories { } dependencies { - compile fileTree(dir: 'lib', include: '*.jar') + implementation fileTree(dir: 'lib', include: ['*.jar']) - compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' - compile group: 'ch.qos.logback', name: 'logback-core', version: '1.2.6' - compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.6' - compile group: 'io.netty', name: 'netty-all', version: '4.1.69.Final' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' + implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.6' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.6' + implementation group: 'io.netty', name: 'netty-all', version: '4.1.69.Final' - compile group: 'com.google.code.gson', name: 'gson', version: '2.8.8' - compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.1' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' + implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.1' - compile group: 'org.reflections', name: 'reflections', version: '0.9.12' + implementation group: 'org.reflections', name: 'reflections', version: '0.9.12' - compile group: 'dev.morphia.morphia', name: 'core', version: '1.6.1' + implementation group: 'dev.morphia.morphia', name: 'core', version: '1.6.1' + + implementation group: 'org.greenrobot', name: 'eventbus-java', version: '3.3.1' } application { @@ -51,9 +53,11 @@ jar { jar.baseName = 'grasscutter' from { - configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + duplicatesStrategy = DuplicatesStrategy.INCLUDE + from('src/main/java') { include '*.xml' } diff --git a/data/Banners.json b/data/Banners.json index 163319f5e..a4f724ac9 100644 --- a/data/Banners.json +++ b/data/Banners.json @@ -17,39 +17,24 @@ "gachaType": 301, "scheduleId": 903, "bannerType": "EVENT", - "prefabPath": "GachaShowPanel_A076", - "previewPrefabPath": "UI_Tab_GachaShowPanel_A076", - "titlePath": "UI_GACHA_SHOW_PANEL_A076_TITLE", + "prefabPath": "GachaShowPanel_A079", + "previewPrefabPath": "UI_Tab_GachaShowPanel_A079", + "titlePath": "UI_GACHA_SHOW_PANEL_A048_TITLE", "costItem": 223, "beginTime": 0, "endTime": 1924992000, "sortId": 9998, "maxItemType": 1, - "rateUpItems1": [1066], - "rateUpItems2": [1023, 1043, 1064] - }, - { - "gachaType": 400, - "scheduleId": 913, - "bannerType": "EVENT", - "prefabPath": "GachaShowPanel_A077", - "previewPrefabPath": "UI_Tab_GachaShowPanel_A077", - "titlePath": "UI_Tab_GachaShowPanel_A077", - "costItem": 223, - "beginTime": 0, - "endTime": 1924992000, - "sortId": 9998, - "maxItemType": 1, - "rateUpItems1": [1022], - "rateUpItems2": [1023, 1043, 1064] + "rateUpItems1": [1002], + "rateUpItems2": [1053, 1020, 1045] }, { "gachaType": 302, - "scheduleId": 923, + "scheduleId": 913, "bannerType": "WEAPON", - "prefabPath": "GachaShowPanel_A078", - "previewPrefabPath": "UI_Tab_GachaShowPanel_A078", - "titlePath": "UI_GACHA_SHOW_PANEL_A078_TITLE", + "prefabPath": "GachaShowPanel_A080", + "previewPrefabPath": "UI_Tab_GachaShowPanel_A080", + "titlePath": "UI_GACHA_SHOW_PANEL_A021_TITLE", "costItem": 223, "beginTime": 0, "endTime": 1924992000, @@ -58,7 +43,7 @@ "eventChance": 75, "softPity": 80, "hardPity": 80, - "rateUpItems1": [11510, 15503], - "rateUpItems2": [11402, 12403, 13401, 14402, 15405] + "rateUpItems1": [11509, 12504], + "rateUpItems2": [11401, 12402, 13407, 14401, 15401] } -] \ No newline at end of file +] diff --git a/data/query_cur_region.txt b/data/query_cur_region.txt index 427c13f45..ddef1aff6 100644 --- a/data/query_cur_region.txt +++ b/data/query_cur_region.txt @@ -1 +1 @@ -GpgdCgw0Ny44OS4xNTIuNDcQ1awBGitodHRwczovL29zdXNhb2FzZXJ2ZXIueXVhbnNoZW4uY29tL3JlY2hhcmdlOgNVU0FCOWh0dHBzOi8vYXV0b3BhdGNoaGsueXVhbnNoZW4uY29tL2NsaWVudF9nYW1lX3Jlcy8yLjZfbGl2ZUo8aHR0cHM6Ly9hdXRvcGF0Y2hoay55dWFuc2hlbi5jb20vY2xpZW50X2Rlc2lnbl9kYXRhLzIuNl9saXZlUpIBaHR0cHM6Ly93ZWJzdGF0aWMtc2VhLmhveW92ZXJzZS5jb20veXMvZXZlbnQvaW0tc2VydmljZS9pbmRleC5odG1sP2ltX291dD1mYWxzZSZzaWduX3R5cGU9MiZhdXRoX2FwcGlkPWltX2NjcyZhdXRoa2V5X3Zlcj0xJndpbl9kaXJlY3Rpb249cG9ydHJhaXRiCDIuNl9saXZlcNnsmgOQAdnsmgOaAVx7InJlbW90ZU5hbWUiOiAiZGF0YV92ZXJzaW9ucyIsICJtZDUiOiAiMWE0NDVhZTQ4ZmJkYmUwMzgzMTJiZTg2OGYyODEzZDIiLCAiZmlsZVNpemUiOiA0NDE1faIBW3sicmVtb3RlTmFtZSI6ICJkYXRhX3ZlcnNpb25zIiwgIm1kNSI6ICIxNzU0MmM3YmJhYzQ5YjkxMWRlMjlhOTYyNzU0YmQ2MSIsICJmaWxlU2l6ZSI6IDUxNH2yAYEGCL23mQMa4AV7InJlbW90ZU5hbWUiOiAicmVzX3ZlcnNpb25zX2V4dGVybmFsIiwgIm1kNSI6ICI5ZjA5MmNhMTMwNjdjYWU0MzEzNDkzZWVkM2QzZTlhZSIsICJmaWxlU2l6ZSI6IDUyNjk2Nn0NCnsicmVtb3RlTmFtZSI6ICJyZXNfdmVyc2lvbnNfbWVkaXVtIiwgIm1kNSI6ICI3Yzk0MmU3MDRhODA0YzIxMjJmYzYzZWU5MjhlNzE0OCIsICJmaWxlU2l6ZSI6IDI4NTU1MH0NCnsicmVtb3RlTmFtZSI6ICJyZXNfdmVyc2lvbnNfc3RyZWFtaW5nIiwgIm1kNSI6ICJlMzVmNmQ4NTNkMDRjZTAyMmFjN2MzNmQ0M2Y0MGU3YyIsICJmaWxlU2l6ZSI6IDEyMjAzNn0NCnsicmVtb3RlTmFtZSI6ICJyZWxlYXNlX3Jl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 \ No newline at end of file +GpgdCgo4LjIwOS42Ni4xENWsARosaHR0cHM6Ly9vc2V1cm9vYXNlcnZlci55dWFuc2hlbi5jb20vcmVjaGFyZ2U6BGV1cm9COWh0dHBzOi8vYXV0b3BhdGNoaGsueXVhbnNoZW4uY29tL2NsaWVudF9nYW1lX3Jlcy8yLjZfbGl2ZUo8aHR0cHM6Ly9hdXRvcGF0Y2hoay55dWFuc2hlbi5jb20vY2xpZW50X2Rlc2lnbl9kYXRhLzIuNl9saXZlUpIBaHR0cHM6Ly93ZWJzdGF0aWMtc2VhLmhveW92ZXJzZS5jb20veXMvZXZlbnQvaW0tc2VydmljZS9pbmRleC5odG1sP2ltX291dD1mYWxzZSZzaWduX3R5cGU9MiZhdXRoX2FwcGlkPWltX2NjcyZhdXRoa2V5X3Zlcj0xJndpbl9kaXJlY3Rpb249cG9ydHJhaXRiCDIuNl9saXZlcNnsmgOQAdnsmgOaAVx7InJlbW90ZU5hbWUiOiAiZGF0YV92ZXJzaW9ucyIsICJtZDUiOiAiMWE0NDVhZTQ4ZmJkYmUwMzgzMTJiZTg2OGYyODEzZDIiLCAiZmlsZVNpemUiOiA0NDE1faIBW3sicmVtb3RlTmFtZSI6ICJkYXRhX3ZlcnNpb25zIiwgIm1kNSI6ICIxNzU0MmM3YmJhYzQ5YjkxMWRlMjlhOTYyNzU0YmQ2MSIsICJmaWxlU2l6ZSI6IDUxNH2yAYEGCL23mQMa4AV7InJlbW90ZU5hbWUiOiAicmVzX3ZlcnNpb25zX2V4dGVybmFsIiwgIm1kNSI6ICI5ZjA5MmNhMTMwNjdjYWU0MzEzNDkzZWVkM2QzZTlhZSIsICJmaWxlU2l6ZSI6IDUyNjk2Nn0NCnsicmVtb3RlTmFtZSI6ICJyZXNfdmVyc2lvbnNfbWVkaXVtIiwgIm1kNSI6ICI3Yzk0MmU3MDRhODA0YzIxMjJmYzYzZWU5MjhlNzE0OCIsICJmaWxlU2l6ZSI6IDI4NTU1MH0NCnsicmVtb3RlTmFtZSI6ICJyZXNfdmVyc2lvbnNfc3RyZWFtaW5nIiwgIm1kNSI6ICJlMzVmNmQ4NTNkMDRjZTAyMmFjN2MzNmQ0M2Y0MGU3YyIsICJmaWxlU2l6ZSI6IDEyMjAzNn0NCnsicmVtb3RlTmFtZSI6ICJyZWxlYXNlX3J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 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f04d6a20a..aa991fcea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/proxy.py b/proxy.py index 1961c90dc..86145cde6 100644 --- a/proxy.py +++ b/proxy.py @@ -16,53 +16,59 @@ # - mitmdump from mitmproxy # # @author MlgmXyysd -# @version 1.0 +# @version 1.1 # ## -from mitmproxy import ctx, http +from mitmproxy import http +from proxy_config import USE_SSL +from proxy_config import REMOTE_HOST +from proxy_config import REMOTE_PORT class MlgmXyysd_Genshin_Impact_Proxy: + LIST_DOMAINS = [ + "api-os-takumi.mihoyo.com", + "hk4e-api-os-static.mihoyo.com", + "hk4e-sdk-os.mihoyo.com", + "dispatchosglobal.yuanshen.com", + "osusadispatch.yuanshen.com", + "account.mihoyo.com", + "log-upload-os.mihoyo.com", + "dispatchcntest.yuanshen.com", + "devlog-upload.mihoyo.com", + "webstatic.mihoyo.com", + "log-upload.mihoyo.com", + "hk4e-sdk.mihoyo.com", + "api-beta-sdk.mihoyo.com", + "api-beta-sdk-os.mihoyo.com", + "cnbeta01dispatch.yuanshen.com", + "dispatchcnglobal.yuanshen.com", + "cnbeta02dispatch.yuanshen.com", + "sdk-os-static.mihoyo.com", + "webstatic-sea.mihoyo.com", + "webstatic-sea.hoyoverse.com", + "hk4e-sdk-os-static.hoyoverse.com", + "sdk-os-static.hoyoverse.com", + "api-account-os.hoyoverse.com", + "hk4e-sdk-os.hoyoverse.com", + "overseauspider.yuanshen.com", + "gameapi-account.mihoyo.com", + "minor-api.mihoyo.com", + "public-data-api.mihoyo.com", + "uspider.yuanshen.com", + "sdk-static.mihoyo.com" + ] + def request(self, flow: http.HTTPFlow) -> None: - - # This can also be replaced with another IP address. - REMOTE_HOST = "localhost" - - LIST_DOMAINS = [ - "api-os-takumi.mihoyo.com", - "hk4e-api-os-static.mihoyo.com", - "hk4e-sdk-os.mihoyo.com", - "dispatchosglobal.yuanshen.com", - "osusadispatch.yuanshen.com", - "account.mihoyo.com", - "log-upload-os.mihoyo.com", - "dispatchcntest.yuanshen.com", - "devlog-upload.mihoyo.com", - "webstatic.mihoyo.com", - "log-upload.mihoyo.com", - "hk4e-sdk.mihoyo.com", - "api-beta-sdk.mihoyo.com", - "api-beta-sdk-os.mihoyo.com", - "cnbeta01dispatch.yuanshen.com", - "dispatchcnglobal.yuanshen.com", - "cnbeta02dispatch.yuanshen.com", - "sdk-os-static.mihoyo.com", - "webstatic-sea.mihoyo.com", - "webstatic-sea.hoyoverse.com", - "hk4e-sdk-os-static.hoyoverse.com", - "sdk-os-static.hoyoverse.com", - "api-account-os.hoyoverse.com", - "hk4e-sdk-os.hoyoverse.com" - ] - - if flow.request.url.startswith("http://overseauspider.yuanshen.com:8888/log"): - ctx.log.info("Block overseauspider.yuanshen.com") - flow.response = http.HTTPResponse.make(404) - elif flow.request.host in LIST_DOMAINS: - ctx.log.info("Redirect " + flow.request.host) + if flow.request.host in self.LIST_DOMAINS: + if USE_SSL: + flow.request.scheme = "https" + else: + flow.request.scheme = "http" flow.request.host = REMOTE_HOST + flow.request.port = REMOTE_PORT addons = [ MlgmXyysd_Genshin_Impact_Proxy() -] \ No newline at end of file +] diff --git a/proxy_config.py b/proxy_config.py new file mode 100644 index 000000000..5025a974e --- /dev/null +++ b/proxy_config.py @@ -0,0 +1,4 @@ +# This can also be replaced with another IP address. +USE_SSL = True +REMOTE_HOST = "127.0.0.1" +REMOTE_PORT = 443 \ No newline at end of file diff --git a/run.bat b/run.bat deleted file mode 100644 index 36530cf77..000000000 --- a/run.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off - - - -::This will not work if your java is in a different location, plugin as necessary -::this just saves you from changing your PATH -"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" -jar ./grasscutter.jar \ No newline at end of file diff --git a/run.cmd b/run.cmd deleted file mode 100644 index 36a20d811..000000000 --- a/run.cmd +++ /dev/null @@ -1,115 +0,0 @@ -@rem -@rem Copyright (C) 2002-2022 MlgmXyysd All Rights Reserved. -@rem - -@if "%DEBUG%" == "" echo off -pushd %~dp0 -title Grasscutter -call :LOG [INFO] Grasscutter -call :LOG [INFO] Initializing... - -@rem This will not work if your java or mitmproxy is in a different location, plugin as necessary -@rem this just saves you from changing your PATH -set JAVA_PATH=C:\Program Files\Java\jdk1.8.0_202\ -set MITMPROXY_PATH=%~dp0 -set PROXY_SCRIPT=proxy -@rem TODO: MongoDB integration -set SERVER_PATH=%~dp0 - -@rem mitmproxy not found, server only -if not exist "%MITMPROXY_PATH%mitmdump.exe" ( - call :LOG [WARN] mitmproxy not found, server only mode. - goto :SERVER -) -@rem proxy script not found, server only -if not exist "%PROXY_SCRIPT%.py" ( - if not exist "%PROXY_SCRIPT%.pyc" ( - call :LOG [WARN] Missing proxy script or compiled proxy script, server only mode. - goto :SERVER - ) else set PROXY_SCRIPT=%PROXY_SCRIPT%.pyc -) else set PROXY_SCRIPT=%PROXY_SCRIPT%.py - -:PROXY -@rem UAC Administrator privileges ->nul 2>&1 reg query "HKU\S-1-5-19" || ( - call :LOG [WARN] Currently running with non Administrator privileges, raising... - echo set UAC = CreateObject^("Shell.Application"^) > "%temp%\UAC.vbs" - echo UAC.ShellExecute "%~f0", "%1", "", "runas", 1 >> "%temp%\UAC.vbs" - "%temp%\UAC.vbs" - del /f /q "%temp%\UAC.vbs" >nul 2>nul - exit /b -) - -set PROXY=true -@rem Store original proxy settings -for /f "tokens=2*" %%a in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable 2^>nul') do set "ORIG_PROXY_ENABLE=%%b" -for /f "tokens=2*" %%a in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer 2^>nul') do set "ORIG_PROXY_SERVER=%%b" - -call :LOG [INFO] Starting proxy daemon... -@rem TODO: External proxy when ORIG_PROXY_ENABLE == 0x1 -start /min "" "%MITMPROXY_PATH%mitmdump.exe" -s %PROXY_SCRIPT% --ssl-insecure - -@rem CA certificate for possible HTTPS scheme -call :LOG [INFO] Waiting for CA certificate generation... -set CA_CERT_FILE="%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer" - -set /A TIMEOUT_COUNT=0 - -:CERT_CA_CHECK -if not exist %CA_CERT_FILE% ( - timeout /T 1 >nul 2>nul - set /A TIMEOUT_COUNT+=1 - goto CERT_CA_CHECK -) -:EXTRA_TIMEOUT -if %TIMEOUT_COUNT% LEQ 2 ( - timeout /T 1 >nul 2>nul - set /A TIMEOUT_COUNT+=1 - goto EXTRA_TIMEOUT -) -call :LOG [INFO] Adding CA certificate to store... -certutil -addstore root %CA_CERT_FILE% >nul 2>nul - -call :LOG [INFO] Setting up network proxy... -reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f >nul 2>nul -reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "127.0.0.1:8080" /f >nul 2>nul - -:SERVER -if not exist "%JAVA_PATH%bin\java.exe" ( - call :LOG [ERROR] Java not found. - goto :EXIT -) -if not exist "%SERVER_PATH%grasscutter.jar" ( - call :LOG [ERROR] Server jar not found. - goto :EXIT -) -call :LOG [INFO] Starting server... -"%JAVA_PATH%bin\java.exe" -jar "%SERVER_PATH%grasscutter.jar" -call :LOG [INFO] Server stopped - -:EXIT -if "%PROXY%" == "" ( - call :LOG [INFO] Proxy not started, no need to clean up. -) else ( - call :LOG [INFO] Restoring network settings... - - reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d "%ORIG_PROXY_ENABLE%" /f >nul 2>nul - reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "%ORIG_PROXY_SERVER%" /f >nul 2>nul - - call :LOG [INFO] Shutting down proxy daemon... - taskkill /t /f /im mitmdump.exe >nul 2>nul - - call :LOG [INFO] Removing CA certificate... - - for /F "tokens=2" %%s in ('certutil -dump %CA_CERT_FILE% ^| findstr ^"^sha1^"') do ( - set SERIAL=%%s - ) - - certutil -delstore root %SERIAL% >nul 2>nul -) - -call :LOG [INFO] See you again :) -goto :EOF - -:LOG -echo [%time:~0,8%] %* \ No newline at end of file diff --git a/src/deprecated/java/emu/grasscutter/commands/PlayerCommands.java b/src/deprecated/java/emu/grasscutter/commands/PlayerCommands.java deleted file mode 100644 index 2e8be354d..000000000 --- a/src/deprecated/java/emu/grasscutter/commands/PlayerCommands.java +++ /dev/null @@ -1,307 +0,0 @@ -package emu.grasscutter.commands; - -import java.lang.reflect.Modifier; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; - -import emu.grasscutter.data.GenshinData; -import emu.grasscutter.data.def.ItemData; -import emu.grasscutter.data.def.MonsterData; -import emu.grasscutter.game.GenshinPlayer; -import emu.grasscutter.game.avatar.GenshinAvatar; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntityItem; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.GenshinEntity; -import emu.grasscutter.game.inventory.GenshinItem; -import emu.grasscutter.game.inventory.ItemType; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; -import emu.grasscutter.utils.Position; - -public class PlayerCommands { - private static HashMap list = new HashMap<>(); - - static { - try { - // Look for classes - for (Class cls : PlayerCommands.class.getDeclaredClasses()) { - // Get non abstract classes - if (!Modifier.isAbstract(cls.getModifiers())) { - Command commandAnnotation = cls.getAnnotation(Command.class); - PlayerCommand command = (PlayerCommand) cls.newInstance(); - - if (commandAnnotation != null) { - command.setLevel(commandAnnotation.gmLevel()); - for (String alias : commandAnnotation.aliases()) { - if (alias.length() == 0) { - continue; - } - - String commandName = "!" + alias; - list.put(commandName, command); - commandName = "/" + alias; - list.put(commandName, command); - } - } - - String commandName = "!" + cls.getSimpleName().toLowerCase(); - list.put(commandName, command); - commandName = "/" + cls.getSimpleName().toLowerCase(); - list.put(commandName, command); - } - - } - } catch (Exception e) { - - } - } - - public static void handle(GenshinPlayer player, String msg) { - String[] split = msg.split(" "); - - // End if invalid - if (split.length == 0) { - return; - } - - // - String first = split[0].toLowerCase(); - PlayerCommand c = PlayerCommands.list.get(first); - - if (c != null) { - // Level check - if (player.getGmLevel() < c.getLevel()) { - return; - } - // Execute - int len = Math.min(first.length() + 1, msg.length()); - c.execute(player, msg.substring(len)); - } - } - - public static abstract class PlayerCommand { - // GM level required to use this command - private int level; - protected int getLevel() { return this.level; } - protected void setLevel(int minLevel) { this.level = minLevel; } - - // Main - public abstract void execute(GenshinPlayer player, String raw); - } - - // ================ Commands ================ - - @Command(aliases = {"g", "item", "additem"}, helpText = "/give [item id] [count] - Gives {count} amount of {item id}") - public static class Give extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - String[] split = raw.split(" "); - int itemId = 0, count = 1; - - try { - itemId = Integer.parseInt(split[0]); - } catch (Exception e) { - itemId = 0; - } - - try { - count = Math.max(Math.min(Integer.parseInt(split[1]), Integer.MAX_VALUE), 1); - } catch (Exception e) { - count = 1; - } - - // Give - ItemData itemData = GenshinData.getItemDataMap().get(itemId); - GenshinItem item; - - if (itemData == null) { - player.dropMessage("Error: Item data not found"); - return; - } - - if (itemData.isEquip()) { - List items = new LinkedList<>(); - for (int i = 0; i < count; i++) { - item = new GenshinItem(itemData); - items.add(item); - } - player.getInventory().addItems(items); - player.sendPacket(new PacketItemAddHintNotify(items, ActionReason.SubfieldDrop)); - } else { - item = new GenshinItem(itemData, count); - player.getInventory().addItem(item); - player.sendPacket(new PacketItemAddHintNotify(item, ActionReason.SubfieldDrop)); - } - } - } - - @Command(aliases = {"d"}, helpText = "/drop [item id] [count] - Drops {count} amount of {item id}") - public static class Drop extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - String[] split = raw.split(" "); - int itemId = 0, count = 1; - - try { - itemId = Integer.parseInt(split[0]); - } catch (Exception e) { - itemId = 0; - } - - try { - count = Math.max(Math.min(Integer.parseInt(split[1]), Integer.MAX_VALUE), 1); - } catch (Exception e) { - count = 1; - } - - // Give - ItemData itemData = GenshinData.getItemDataMap().get(itemId); - - if (itemData == null) { - player.dropMessage("Error: Item data not found"); - return; - } - - if (itemData.isEquip()) { - float range = (5f + (.1f * count)); - for (int i = 0; i < count; i++) { - Position pos = player.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); - EntityItem entity = new EntityItem(player.getWorld(), player, itemData, pos, 1); - player.getWorld().addEntity(entity); - } - } else { - EntityItem entity = new EntityItem(player.getWorld(), player, itemData, player.getPos().clone().addY(3f), count); - player.getWorld().addEntity(entity); - } - } - } - - @Command(helpText = "/spawn [monster id] [count] - Creates {count} amount of {item id}") - public static class Spawn extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - String[] split = raw.split(" "); - int monsterId = 0, count = 1, level = 1; - - try { - monsterId = Integer.parseInt(split[0]); - } catch (Exception e) { - monsterId = 0; - } - - try { - level = Math.max(Math.min(Integer.parseInt(split[1]), 200), 1); - } catch (Exception e) { - level = 1; - } - - try { - count = Math.max(Math.min(Integer.parseInt(split[2]), 1000), 1); - } catch (Exception e) { - count = 1; - } - - // Give - MonsterData monsterData = GenshinData.getMonsterDataMap().get(monsterId); - - if (monsterData == null) { - player.dropMessage("Error: Monster data not found"); - return; - } - - float range = (5f + (.1f * count)); - for (int i = 0; i < count; i++) { - Position pos = player.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); - EntityMonster entity = new EntityMonster(player.getWorld(), monsterData, pos, level); - player.getWorld().addEntity(entity); - } - } - } - - @Command(helpText = "/killall") - public static class KillAll extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - List toRemove = new LinkedList<>(); - for (GenshinEntity entity : player.getWorld().getEntities().values()) { - if (entity instanceof EntityMonster) { - toRemove.add(entity); - } - } - toRemove.forEach(e -> player.getWorld().killEntity(e, 0)); - } - } - - @Command(helpText = "/resetconst - Resets all constellations for the currently active character") - public static class ResetConst extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - EntityAvatar entity = player.getTeamManager().getCurrentAvatarEntity(); - - if (entity == null) { - return; - } - - GenshinAvatar avatar = entity.getAvatar(); - - avatar.getTalentIdList().clear(); - avatar.setCoreProudSkillLevel(0); - avatar.recalcStats(); - avatar.save(); - - player.dropMessage("Constellations for " + entity.getAvatar().getAvatarData().getName() + " have been reset. Please relogin to see changes."); - } - } - - @Command(helpText = "/godmode - Prevents you from taking damage") - public static class Godmode extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - player.setGodmode(!player.hasGodmode()); - player.dropMessage("Godmode is now " + (player.hasGodmode() ? "ON" : "OFF")); - } - } - - @Command(helpText = "/sethp [hp]") - public static class Sethp extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - String[] split = raw.split(" "); - int hp = 0; - - try { - hp = Math.max(Integer.parseInt(split[0]), 1); - } catch (Exception e) { - hp = 1; - } - - EntityAvatar entity = player.getTeamManager().getCurrentAvatarEntity(); - - if (entity == null) { - return; - } - - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, hp); - entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); - } - } - - @Command(aliases = {"clearart"}, helpText = "/clearartifacts") - public static class ClearArtifacts extends PlayerCommand { - @Override - public void execute(GenshinPlayer player, String raw) { - List toRemove = new LinkedList<>(); - for (GenshinItem item : player.getInventory().getItems().values()) { - if (item.getItemType() == ItemType.ITEM_RELIQUARY && item.getLevel() == 1 && item.getExp() == 0 && !item.isLocked() && !item.isEquipped()) { - toRemove.add(item); - } - } - - player.getInventory().removeItems(toRemove); - } - } -} diff --git a/src/deprecated/java/emu/grasscutter/commands/ServerCommands.java b/src/deprecated/java/emu/grasscutter/commands/ServerCommands.java deleted file mode 100644 index 2039d5227..000000000 --- a/src/deprecated/java/emu/grasscutter/commands/ServerCommands.java +++ /dev/null @@ -1,171 +0,0 @@ -package emu.grasscutter.commands; - -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.stream.Collectors; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GenshinData; -import emu.grasscutter.data.def.ItemData; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.GenshinPlayer; -import emu.grasscutter.game.inventory.GenshinItem; -import emu.grasscutter.utils.Crypto; -import emu.grasscutter.utils.Utils; - -public class ServerCommands { - private static HashMap list = new HashMap<>(); - - static { - try { - // Look for classes - for (Class cls : ServerCommands.class.getDeclaredClasses()) { - // Get non abstract classes - if (!Modifier.isAbstract(cls.getModifiers())) { - String commandName = cls.getSimpleName().toLowerCase(); - list.put(commandName, (ServerCommand) cls.newInstance()); - } - - } - } catch (Exception e) { - - } - } - - public static void handle(String msg) { - String[] split = msg.split(" "); - - // End if invalid - if (split.length == 0) { - return; - } - - // - String first = split[0].toLowerCase(); - ServerCommand c = ServerCommands.list.get(first); - - if (c != null) { - // Execute - int len = Math.min(first.length() + 1, msg.length()); - c.execute(msg.substring(len)); - } - } - - public static abstract class ServerCommand { - public abstract void execute(String raw); - } - - // ================ Commands ================ - - public static class Reload extends ServerCommand { - @Override - public void execute(String raw) { - Grasscutter.getLogger().info("Reloading config."); - Grasscutter.loadConfig(); - Grasscutter.getDispatchServer().loadQueries(); - Grasscutter.getLogger().info("Reload complete."); - } - } - - public static class sendMsg extends ServerCommand { - @Override - public void execute(String raw) { - List split = Arrays.asList(raw.split(" ")); - - if (split.size() < 2) { - Grasscutter.getLogger().error("Invalid amount of args"); - return; - } - - String playerID = split.get(0); - String message = split.stream().skip(1).collect(Collectors.joining(" ")); - - - emu.grasscutter.game.Account account = DatabaseHelper.getAccountByPlayerId(Integer.parseInt(playerID)); - if (account != null) { - GenshinPlayer player = Grasscutter.getGameServer().getPlayerById(Integer.parseInt(playerID)); - if(player != null) { - player.dropMessage(message); - Grasscutter.getLogger().info(String.format("Successfully sent message to %s: %s", playerID, message)); - } else { - Grasscutter.getLogger().error("Player not online"); - } - } else { - Grasscutter.getLogger().error(String.format("Player %s does not exist", playerID)); - } - } - } - - public static class Account extends ServerCommand { - @Override - public void execute(String raw) { - String[] split = raw.split(" "); - - if (split.length < 2) { - Grasscutter.getLogger().error("Invalid amount of args"); - return; - } - - String command = split[0].toLowerCase(); - String username = split[1]; - - switch (command) { - case "create": - if (split.length < 2) { - Grasscutter.getLogger().error("Invalid amount of args"); - return; - } - - int reservedId = 0; - try { - reservedId = Integer.parseInt(split[2]); - } catch (Exception e) { - reservedId = 0; - } - - emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithId(username, reservedId); - if (account != null) { - Grasscutter.getLogger().info("Account created" + (reservedId > 0 ? " with an id of " + reservedId : "")); - } else { - Grasscutter.getLogger().error("Account already exists"); - } - break; - case "delete": - boolean success = DatabaseHelper.deleteAccount(username); - - if (success) { - Grasscutter.getLogger().info("Account deleted"); - } - break; - /* - case "setpw": - case "setpass": - case "setpassword": - if (split.length < 3) { - Grasscutter.getLogger().error("Invalid amount of args"); - return; - } - - account = DatabaseHelper.getAccountByName(username); - - if (account == null) { - Grasscutter.getLogger().error("No account found!"); - return; - } - - token = split[2]; - token = PasswordHelper.hashPassword(token); - - account.setPassword(token); - DatabaseHelper.saveAccount(account); - - Grasscutter.getLogger().info("Password set"); - break; - */ - } - } - } -} diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 983524223..267ab67f2 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -1,48 +1,63 @@ package emu.grasscutter; +import java.util.ArrayList; + public final class Config { - public String DispatchServerIp = "127.0.0.1"; - public String DispatchServerPublicIp = ""; - public int DispatchServerPort = 443; - public String DispatchServerKeystorePath = "./keystore.p12"; - public String DispatchServerKeystorePassword = ""; - public Boolean UseSSL = true; - - public String GameServerName = "Test"; - public String GameServerIp = "127.0.0.1"; - public String GameServerPublicIp = ""; - public int GameServerPort = 22102; - - public int UploadLogPort = 80; - + public String DatabaseUrl = "mongodb://localhost:27017"; public String DatabaseCollection = "grasscutter"; - + public String RESOURCE_FOLDER = "./resources/"; public String DATA_FOLDER = "./data/"; public String PACKETS_FOLDER = "./packets/"; public String DUMPS_FOLDER = "./dumps/"; public String KEY_FOLDER = "./keys/"; - public boolean LOG_PACKETS = false; - - public GameRates Game = new GameRates(); - public ServerOptions ServerOptions = new ServerOptions(); - - public GameRates getGameRates() { - return Game; + + public String RunMode = "HYBRID"; // HYBRID, DISPATCH_ONLY, GAME_ONLY + public GameServerOptions GameServer = new GameServerOptions(); + public DispatchServerOptions DispatchServer = new DispatchServerOptions(); + + public GameServerOptions getGameServerOptions() { + return GameServer; + } + + public DispatchServerOptions getDispatchOptions() { return DispatchServer; } + + public static class DispatchServerOptions { + public String Ip = "0.0.0.0"; + public String PublicIp = "127.0.0.1"; + public int Port = 443; + public String KeystorePath = "./keystore.p12"; + public String KeystorePassword = ""; + public Boolean UseSSL = true; + + public boolean AutomaticallyCreateAccounts = false; + + public RegionInfo[] GameServers = {}; + + public RegionInfo[] getGameServers() { + return GameServers; + } + + public static class RegionInfo { + public String Name = "os_usa"; + public String Title = "Test"; + public String Ip = "127.0.0.1"; + public int Port = 22102; + } } - public ServerOptions getServerOptions() { - return ServerOptions; - } - - public static class GameRates { - public float ADVENTURE_EXP_RATE = 1.0f; - public float MORA_RATE = 1.0f; - public float DOMAIN_DROP_RATE = 1.0f; - } - - public static class ServerOptions { + public static class GameServerOptions { + public String Name = "Test"; + public String Ip = "0.0.0.0"; + public String PublicIp = "127.0.0.1"; + public int Port = 22102; + + public String DispatchServerDatabaseUrl = "mongodb://localhost:27017"; + public String DispatchServerDatabaseCollection = "grasscutter"; + + public boolean LOG_PACKETS = false; + public int InventoryLimitWeapon = 2000; public int InventoryLimitRelic = 2000; public int InventoryLimitMaterial = 2000; @@ -51,7 +66,18 @@ public final class Config { public int MaxAvatarsInTeam = 4; public int MaxAvatarsInTeamMultiplayer = 4; public int MaxEntityLimit = 1000; // Max entity limit per world. // TODO: Enforce later. + public boolean WatchGacha = false; public int[] WelcomeEmotes = {2007, 1002, 4010}; public String WelcomeMotd = "Welcome to Grasscutter emu"; + + public GameRates Game = new GameRates(); + + public GameRates getGameRates() { return Game; } + + public static class GameRates { + public float ADVENTURE_EXP_RATE = 1.0f; + public float MORA_RATE = 1.0f; + public float DOMAIN_DROP_RATE = 1.0f; + } } } diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index c9ccb551c..abcdc3557 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -7,7 +7,7 @@ import java.io.FileWriter; import java.io.InputStreamReader; import java.net.InetSocketAddress; -import emu.grasscutter.commands.CommandMap; +import emu.grasscutter.command.CommandMap; import emu.grasscutter.utils.Utils; import org.reflections.Reflections; import org.slf4j.LoggerFactory; @@ -73,11 +73,26 @@ public final class Grasscutter { DatabaseManager.initialize(); // Start servers. - dispatchServer = new DispatchServer(); - dispatchServer.start(); - - gameServer = new GameServer(new InetSocketAddress(getConfig().GameServerIp, getConfig().GameServerPort)); - gameServer.start(); + if(getConfig().RunMode.equalsIgnoreCase("HYBRID")) { + dispatchServer = new DispatchServer(); + dispatchServer.start(); + + gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + gameServer.start(); + } else if(getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { + dispatchServer = new DispatchServer(); + dispatchServer.start(); + } else if(getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + gameServer.start(); + } else { + getLogger().error("Invalid server run mode. " + getConfig().RunMode); + getLogger().error("Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter..."); + getLogger().error("Shutting down..."); + System.exit(1); + } + + // Open console. startConsole(); @@ -86,8 +101,10 @@ public final class Grasscutter { public static void loadConfig() { try (FileReader file = new FileReader(configFile)) { config = gson.fromJson(file, Config.class); + saveConfig(); } catch (Exception e) { - Grasscutter.config = new Config(); saveConfig(); + Grasscutter.config = new Config(); + saveConfig(); } } @@ -104,9 +121,14 @@ public final class Grasscutter { try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) { while ((input = br.readLine()) != null) { try { + if(getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { + getLogger().error("Commands are not supported in dispatch only mode"); + return; + } CommandMap.getInstance().invoke(null, input); } catch (Exception e) { - Grasscutter.getLogger().error("Command error: " + e.getMessage()); + Grasscutter.getLogger().error("Command error: "); + e.printStackTrace(); } } } catch (Exception e) { diff --git a/src/main/java/emu/grasscutter/command/Command.java b/src/main/java/emu/grasscutter/command/Command.java new file mode 100644 index 000000000..d8a57e1a8 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/Command.java @@ -0,0 +1,17 @@ +package emu.grasscutter.command; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Command { + String label() default ""; + + String usage() default "No usage specified"; + + String description() default "No description specified"; + + String[] aliases() default {}; + + String permission() default ""; +} diff --git a/src/main/java/emu/grasscutter/command/CommandHandler.java b/src/main/java/emu/grasscutter/command/CommandHandler.java new file mode 100644 index 000000000..037e1ed9d --- /dev/null +++ b/src/main/java/emu/grasscutter/command/CommandHandler.java @@ -0,0 +1,30 @@ +package emu.grasscutter.command; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +public interface CommandHandler { + /** + * Send a message to the target. + * + * @param player The player to send the message to, or null for the server console. + * @param message The message to send. + */ + static void sendMessage(GenshinPlayer player, String message) { + if (player == null) { + Grasscutter.getLogger().info(message); + } else { + player.dropMessage(message); + } + } + + /** + * Called when a player/console invokes a command. + * @param sender The player/console that invoked the command. + * @param args The arguments to the command. + */ + default void execute(GenshinPlayer sender, List args) { + } +} diff --git a/src/main/java/emu/grasscutter/commands/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java similarity index 75% rename from src/main/java/emu/grasscutter/commands/CommandMap.java rename to src/main/java/emu/grasscutter/command/CommandMap.java index e9ca260e7..007fafe17 100644 --- a/src/main/java/emu/grasscutter/commands/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -1,4 +1,4 @@ -package emu.grasscutter.commands; +package emu.grasscutter.command; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.Account; @@ -7,73 +7,89 @@ import org.reflections.Reflections; import java.util.*; -@SuppressWarnings("UnusedReturnValue") +@SuppressWarnings({"UnusedReturnValue", "unused"}) public final class CommandMap { + private final Map commands = new HashMap<>(); + private final Map annotations = new HashMap<>(); + public CommandMap() { + this(false); + } + + public CommandMap(boolean scan) { + if (scan) this.scan(); + } + public static CommandMap getInstance() { return Grasscutter.getGameServer().getCommandMap(); } - - private final Map commands = new HashMap<>(); - private final Map annotations = new HashMap<>(); /** * Register a command handler. - * @param label The command label. + * + * @param label The command label. * @param command The command handler. * @return Instance chaining. */ public CommandMap registerCommand(String label, CommandHandler command) { Grasscutter.getLogger().debug("Registered command: " + label); - + // Get command data. Command annotation = command.getClass().getAnnotation(Command.class); this.annotations.put(label, annotation); this.commands.put(label, command); - + // Register aliases. - if(annotation.aliases().length > 0) { + if (annotation.aliases().length > 0) { for (String alias : annotation.aliases()) { this.commands.put(alias, command); this.annotations.put(alias, annotation); } - } return this; + } + return this; } /** * Removes a registered command handler. + * * @param label The command label. * @return Instance chaining. */ public CommandMap unregisterCommand(String label) { Grasscutter.getLogger().debug("Unregistered command: " + label); CommandHandler handler = this.commands.get(label); - if(handler == null) return this; - + if (handler == null) return this; + Command annotation = handler.getClass().getAnnotation(Command.class); this.annotations.remove(label); this.commands.remove(label); - + // Unregister aliases. - if(annotation.aliases().length > 0) { + if (annotation.aliases().length > 0) { for (String alias : annotation.aliases()) { this.commands.remove(alias); this.annotations.remove(alias); } } - + return this; } /** * Returns a list of all registered commands. + * * @return All command handlers as a list. */ - public List getHandlers() { + public List getHandlersAsList() { return new LinkedList<>(this.commands.values()); } + public HashMap getHandlers() { + return new LinkedHashMap<>(this.commands); + } + /** * Returns a handler by label/alias. + * * @param label The command label. * @return The command handler. */ @@ -83,59 +99,44 @@ public final class CommandMap { /** * Invoke a command handler with the given arguments. - * @param player The player invoking the command or null for the server console. + * + * @param player The player invoking the command or null for the server console. * @param rawMessage The messaged used to invoke the command. */ public void invoke(GenshinPlayer player, String rawMessage) { rawMessage = rawMessage.trim(); if(rawMessage.length() == 0) { - CommandHandler.sendMessage(player, "No command specified."); + CommandHandler.sendMessage(player, "No command specified."); return; } - + // Remove prefix if present. - if(!Character.isLetter(rawMessage.charAt(0))) + if (!Character.isLetter(rawMessage.charAt(0))) rawMessage = rawMessage.substring(1); - + // Parse message. String[] split = rawMessage.split(" "); List args = new LinkedList<>(Arrays.asList(split)); String label = args.remove(0); - + // Get command handler. CommandHandler handler = this.commands.get(label); - if(handler == null) { - CommandHandler.sendMessage(player, "Unknown command: " + label); return; + if (handler == null) { + CommandHandler.sendMessage(player, "Unknown command: " + label); + return; } - + // Check for permission. - if(player != null) { + if (player != null) { String permissionNode = this.annotations.get(label).permission(); Account account = player.getAccount(); - List permissions = account.getPermissions(); - if(!permissions.contains("*") && !permissions.contains(permissionNode)) { - CommandHandler.sendMessage(player, "You do not have permission to run this command."); return; + if(!permissionNode.isEmpty() && !account.hasPermission(permissionNode)) { + CommandHandler.sendMessage(player, "You do not have permission to run this command."); + return; } } - - // Execution power check. - Command.Execution executionPower = this.annotations.get(label).execution(); - if(player == null && executionPower == Command.Execution.PLAYER) { - CommandHandler.sendMessage(null, "Run this command in-game."); return; - } else if (player != null && executionPower == Command.Execution.CONSOLE) { - CommandHandler.sendMessage(player, "This command can only be run from the console."); return; - } - + // Invoke execute method for handler. - if(player == null) handler.execute(args); - else handler.execute(player, args); - } - - public CommandMap() { - this(false); - } - - public CommandMap(boolean scan) { - if(scan) this.scan(); + handler.execute(player, args); } /** diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java new file mode 100644 index 000000000..26570e0e8 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -0,0 +1,62 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "account", usage = "account [uid]", + description = "Modify user accounts") +public final class AccountCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender != null) { + CommandHandler.sendMessage(sender, "This command can only be run from the console."); + return; + } + + if (args.size() < 2) { + CommandHandler.sendMessage(null, "Usage: account [uid]"); + return; + } + + String action = args.get(0); + String username = args.get(1); + + switch (action) { + default: + CommandHandler.sendMessage(null, "Usage: account [uid]"); + return; + case "create": + int uid = 0; + if (args.size() > 2) { + try { + uid = Integer.parseInt(args.get(2)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(null, "Invalid UID."); + return; + } + } + + emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithId(username, uid); + if (account == null) { + CommandHandler.sendMessage(null, "Account already exists."); + return; + } else { + CommandHandler.sendMessage(null, "Account created with UID " + account.getPlayerUid() + "."); + account.addPermission("*"); // Grant the player superuser permissions. + account.save(); // Save account to database. + } + return; + case "delete": + if (DatabaseHelper.deleteAccount(username)) { + CommandHandler.sendMessage(null, "Account deleted."); + } else { + CommandHandler.sendMessage(null, "Account not found."); + } + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java new file mode 100644 index 000000000..4922f16f8 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java @@ -0,0 +1,29 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "broadcast", usage = "broadcast ", + description = "Sends a message to all the players", aliases = {"b"}, permission = "server.broadcast") +public final class BroadcastCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (args.size() < 1) { + CommandHandler.sendMessage(sender, "Usage: broadcast "); + return; + } + + String message = String.join(" ", args.subList(0, args.size())); + + for (GenshinPlayer p : Grasscutter.getGameServer().getPlayers().values()) { + CommandHandler.sendMessage(p, message); + } + + CommandHandler.sendMessage(sender, "Message sent."); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java new file mode 100644 index 000000000..eb2e998f2 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -0,0 +1,42 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "changescene", usage = "changescene ", + description = "Changes your scene", aliases = {"scene"}, permission = "player.changescene") +public final class ChangeSceneCommand implements CommandHandler { + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + if (args.size() < 1) { + CommandHandler.sendMessage(sender, "Usage: changescene "); + return; + } + + try { + int sceneId = Integer.parseInt(args.get(0)); + + if (sceneId == sender.getSceneId()) { + CommandHandler.sendMessage(sender, "You are already in that scene"); + return; + } + + boolean result = sender.getWorld().transferPlayerToScene(sender, sceneId, sender.getPos()); + CommandHandler.sendMessage(sender, "Changed to scene " + sceneId); + + if (!result) { + CommandHandler.sendMessage(sender, "Scene does not exist"); + } + } catch (Exception e) { + CommandHandler.sendMessage(sender, "Usage: changescene "); + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/ClearArtifactsCommand.java b/src/main/java/emu/grasscutter/command/commands/ClearArtifactsCommand.java new file mode 100644 index 000000000..258e1e1d5 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/ClearArtifactsCommand.java @@ -0,0 +1,30 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.inventory.Inventory; +import emu.grasscutter.game.inventory.ItemType; + +import java.util.List; + +@Command(label = "clearartifacts", usage = "clearartifacts", + description = "Deletes all unequipped and unlocked level 0 artifacts, including yellow rarity ones from your inventory", + aliases = {"clearart"}, permission = "player.clearartifacts") +public final class ClearArtifactsCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; // TODO: clear player's artifacts from console or other players + } + + Inventory playerInventory = sender.getInventory(); + playerInventory.getItems().values().stream() + .filter(item -> item.getItemType() == ItemType.ITEM_RELIQUARY) + .filter(item -> item.getLevel() == 1 && item.getExp() == 0) + .filter(item -> !item.isLocked() && !item.isEquipped()) + .forEach(item -> playerInventory.removeItem(item, item.getCount())); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/DropCommand.java b/src/main/java/emu/grasscutter/command/commands/DropCommand.java new file mode 100644 index 000000000..e2d7644a0 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/DropCommand.java @@ -0,0 +1,56 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.utils.Position; + +import java.util.List; + +@Command(label = "drop", usage = "drop [amount]", + description = "Drops an item near you", aliases = {"d", "dropitem"}, permission = "server.drop") +public final class DropCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + if (args.size() < 1) { + CommandHandler.sendMessage(sender, "Usage: drop [amount]"); + return; + } + + try { + int item = Integer.parseInt(args.get(0)); + int amount = 1; + if (args.size() > 1) amount = Integer.parseInt(args.get(1)); + + ItemData itemData = GenshinData.getItemDataMap().get(item); + if (itemData == null) { + CommandHandler.sendMessage(sender, "Invalid item id."); + return; + } + + if (itemData.isEquip()) { + float range = (5f + (.1f * amount)); + for (int i = 0; i < amount; i++) { + Position pos = sender.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); + EntityItem entity = new EntityItem(sender.getScene(), sender, itemData, pos, 1); + sender.getScene().addEntity(entity); + } + } else { + EntityItem entity = new EntityItem(sender.getScene(), sender, itemData, sender.getPos().clone().addY(3f), amount); + sender.getScene().addEntity(entity); + } + CommandHandler.sendMessage(sender, String.format("Dropped %s of %s.", amount, item)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid item or player ID."); + } + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java new file mode 100644 index 000000000..bfe0f51c0 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -0,0 +1,93 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.def.AvatarData; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.avatar.GenshinAvatar; + +import java.util.List; + +@Command(label = "givechar", usage = "givechar [level]", + description = "Gives the player a specified character", aliases = {"givec"}, permission = "player.givechar") +public final class GiveCharCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + int target, avatarId, level = 1, ascension; + + if (sender == null && args.size() < 2) { + CommandHandler.sendMessage(null, "Usage: givechar [amount]"); + return; + } + + switch (args.size()) { + default: + CommandHandler.sendMessage(sender, "Usage: givechar [level]"); + return; + case 2: + try { + target = Integer.parseInt(args.get(0)); + if (Grasscutter.getGameServer().getPlayerByUid(target) == null && sender != null) { + target = sender.getUid(); + level = Integer.parseInt(args.get(1)); + avatarId = Integer.parseInt(args.get(0)); + } else { + avatarId = Integer.parseInt(args.get(1)); + } + } catch (NumberFormatException ignored) { + // TODO: Parse from avatar name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid avatar or player ID."); + return; + } + break; + case 3: + try { + target = Integer.parseInt(args.get(0)); + if (Grasscutter.getGameServer().getPlayerByUid(target) == null) { + CommandHandler.sendMessage(sender, "Invalid player ID."); + return; + } + + avatarId = Integer.parseInt(args.get(1)); + level = Integer.parseInt(args.get(2)); + } catch (NumberFormatException ignored) { + // TODO: Parse from avatar name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid avatar or player ID."); + return; + } + break; + } + + GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found."); + return; + } + + AvatarData avatarData = GenshinData.getAvatarDataMap().get(avatarId); + if (avatarData == null) { + CommandHandler.sendMessage(sender, "Invalid avatar id."); + return; + } + + // Calculate ascension level. + if (level <= 40) { + ascension = (int) Math.ceil(level / 20f); + } else { + ascension = (int) Math.ceil(level / 10f) - 3; + } + + GenshinAvatar avatar = new GenshinAvatar(avatarId); + avatar.setLevel(level); + avatar.setPromoteLevel(ascension); + + // This will handle stats and talents + avatar.recalcStats(); + + targetPlayer.addAvatar(avatar); + CommandHandler.sendMessage(sender, String.format("Given %s to %s.", avatarId, target)); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java new file mode 100644 index 000000000..a533130bc --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -0,0 +1,113 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.inventory.GenshinItem; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; + +import java.util.LinkedList; +import java.util.List; + +@Command(label = "give", usage = "give [player] [amount]", + description = "Gives an item to you or the specified player", aliases = {"g", "item", "giveitem"}, permission = "player.give") +public final class GiveCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + int target, item, amount = 1; + + if (sender == null && args.size() < 2) { + CommandHandler.sendMessage(null, "Usage: give [amount]"); + return; + } + + switch (args.size()) { + default: // *No args* + CommandHandler.sendMessage(sender, "Usage: give [player] [amount]"); + return; + case 1: // + try { + item = Integer.parseInt(args.get(0)); + target = sender.getUid(); + } catch (NumberFormatException ignored) { + // TODO: Parse from item name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid item id."); + return; + } + break; + case 2: // [amount] | [player] + try { + target = Integer.parseInt(args.get(0)); + + if (Grasscutter.getGameServer().getPlayerByUid(target) == null && sender != null) { + target = sender.getUid(); + item = Integer.parseInt(args.get(0)); + amount = Integer.parseInt(args.get(1)); + } else { + item = Integer.parseInt(args.get(1)); + } + } catch (NumberFormatException ignored) { + // TODO: Parse from item name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid item or player ID."); + return; + } + break; + case 3: // [player] [amount] + try { + target = Integer.parseInt(args.get(0)); + + if (Grasscutter.getGameServer().getPlayerByUid(target) == null) { + CommandHandler.sendMessage(sender, "Invalid player ID."); + return; + } + + item = Integer.parseInt(args.get(1)); + amount = Integer.parseInt(args.get(2)); + } catch (NumberFormatException ignored) { + // TODO: Parse from item name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid item or player ID."); + return; + } + break; + } + + GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); + + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found."); + return; + } + + ItemData itemData = GenshinData.getItemDataMap().get(item); + if (itemData == null) { + CommandHandler.sendMessage(sender, "Invalid item id."); + return; + } + + this.item(targetPlayer, itemData, amount); + + CommandHandler.sendMessage(sender, String.format("Given %s of %s to %s.", amount, item, target)); + } + + private void item(GenshinPlayer player, ItemData itemData, int amount) { + if (itemData.isEquip()) { + List items = new LinkedList<>(); + for (int i = 0; i < amount; i++) { + items.add(new GenshinItem(itemData)); + } + player.getInventory().addItems(items); + player.sendPacket(new PacketItemAddHintNotify(items, ActionReason.SubfieldDrop)); + } else { + GenshinItem genshinItem = new GenshinItem(itemData); + genshinItem.setCount(amount); + player.getInventory().addItem(genshinItem); + player.sendPacket(new PacketItemAddHintNotify(genshinItem, ActionReason.SubfieldDrop)); + } + } +} + diff --git a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java new file mode 100644 index 000000000..e5635dc9f --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java @@ -0,0 +1,22 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "godmode", usage = "godmode [playerId]", + description = "Prevents you from taking damage", permission = "player.godmode") +public final class GodModeCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; // TODO: toggle player's godmode statue from console or other players + } + sender.setGodmode(!sender.inGodmode()); + sender.dropMessage("Godmode is now " + (sender.inGodmode() ? "enabled" : "disabled") + "."); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/HealCommand.java b/src/main/java/emu/grasscutter/command/commands/HealCommand.java new file mode 100644 index 000000000..511e52812 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/HealCommand.java @@ -0,0 +1,36 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; + +import java.util.List; + +@Command(label = "heal", usage = "heal|h", + description = "Heal all characters in your current team.", aliases = {"h"}, permission = "player.heal") +public class HealCommand implements CommandHandler { + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + sender.getTeamManager().getActiveTeam().forEach(entity -> { + boolean isAlive = entity.isAlive(); + entity.setFightProperty( + FightProperty.FIGHT_PROP_CUR_HP, + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) + ); + entity.getWorld().broadcastPacket(new PacketAvatarFightPropUpdateNotify(entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + if (!isAlive) { + entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + } + }); + CommandHandler.sendMessage(sender, "All characters are healed."); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java new file mode 100644 index 000000000..abe78e7d3 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java @@ -0,0 +1,91 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.command.CommandMap; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.*; + +@Command(label = "help", usage = "help [command]", + description = "Sends the help message or shows information about a specified command") +public final class HelpCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer player, List args) { + if (args.size() < 1) { + HashMap handlers = CommandMap.getInstance().getHandlers(); + List annotations = new ArrayList<>(); + for (String key : handlers.keySet()) { + Command annotation = handlers.get(key).getClass().getAnnotation(Command.class); + + if (!Arrays.asList(annotation.aliases()).contains(key)) { + if (player != null && !Objects.equals(annotation.permission(), "") && !player.getAccount().hasPermission(annotation.permission())) + continue; + annotations.add(annotation); + } + } + + SendAllHelpMessage(player, annotations); + } else { + String command = args.get(0); + CommandHandler handler = CommandMap.getInstance().getHandler(command); + StringBuilder builder = new StringBuilder(player == null ? "\nHelp - " : "Help - ").append(command).append(": \n"); + if (handler == null) { + builder.append("No command found."); + } else { + Command annotation = handler.getClass().getAnnotation(Command.class); + + builder.append(" ").append(annotation.description()).append("\n"); + builder.append(" Usage: ").append(annotation.usage()); + if (annotation.aliases().length >= 1) { + builder.append("\n").append(" Aliases: "); + for (String alias : annotation.aliases()) { + builder.append(alias).append(" "); + } + } + if (player != null && !Objects.equals(annotation.permission(), "") && !player.getAccount().hasPermission(annotation.permission())) { + builder.append("\n Warning: You do not have permission to run this command."); + } + } + + CommandHandler.sendMessage(player, builder.toString()); + } + } + + void SendAllHelpMessage(GenshinPlayer player, List annotations) { + if (player == null) { + StringBuilder builder = new StringBuilder("\nAvailable commands:\n"); + annotations.forEach(annotation -> { + builder.append(annotation.label()).append("\n"); + builder.append(" ").append(annotation.description()).append("\n"); + builder.append(" Usage: ").append(annotation.usage()); + if (annotation.aliases().length >= 1) { + builder.append("\n").append(" Aliases: "); + for (String alias : annotation.aliases()) { + builder.append(alias).append(" "); + } + } + + builder.append("\n"); + }); + + CommandHandler.sendMessage(null, builder.toString()); + } else { + CommandHandler.sendMessage(player, "Available commands:"); + annotations.forEach(annotation -> { + StringBuilder builder = new StringBuilder(annotation.label()).append("\n"); + builder.append(" ").append(annotation.description()).append("\n"); + builder.append(" Usage: ").append(annotation.usage()); + if (annotation.aliases().length >= 1) { + builder.append("\n").append(" Aliases: "); + for (String alias : annotation.aliases()) { + builder.append(alias).append(" "); + } + } + + CommandHandler.sendMessage(player, builder.toString()); + }); + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/KickCommand.java b/src/main/java/emu/grasscutter/command/commands/KickCommand.java new file mode 100644 index 000000000..61e9d759b --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/KickCommand.java @@ -0,0 +1,31 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "kick", usage = "kick ", + description = "Kicks the specified player from the server (WIP)", permission = "server.kick") +public final class KickCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + int target = Integer.parseInt(args.get(0)); + + GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found."); + return; + } + + if (sender != null) { + CommandHandler.sendMessage(sender, String.format("Player [%s:%s] has kicked player [%s:%s]", sender.getAccount().getPlayerUid(), sender.getAccount().getUsername(), target, targetPlayer.getAccount().getUsername())); + } + CommandHandler.sendMessage(sender, String.format("Kicking player [%s:%s]", target, targetPlayer.getAccount().getUsername())); + + targetPlayer.getSession().close(); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java new file mode 100644 index 000000000..436691b57 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -0,0 +1,64 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.GenshinScene; +import emu.grasscutter.game.entity.EntityMonster; + +import java.util.List; + +@Command(label = "killall", usage = "killall [playerUid] [sceneId]", + description = "Kill all entities", permission = "server.killall") +public final class KillAllCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + GenshinScene scene; + GenshinPlayer genshinPlayer; + + try { + switch (args.size()) { + case 0: // *No args* + if (sender == null) { + CommandHandler.sendMessage(null, "Usage: killall [playerUid] [sceneId]"); + return; + } + scene = sender.getScene(); + break; + case 1: // [playerUid] + genshinPlayer = Grasscutter.getGameServer().getPlayerByUid(Integer.parseInt(args.get(0))); + if (genshinPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found or offline."); + return; + } + scene = genshinPlayer.getScene(); + break; + case 2: // [playerUid] [sceneId] + genshinPlayer = Grasscutter.getGameServer().getPlayerByUid(Integer.parseInt(args.get(0))); + if (genshinPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found or offline."); + return; + } + GenshinScene genshinScene = sender.getWorld().getSceneById(Integer.parseInt(args.get(1))); + if (genshinScene == null) { + CommandHandler.sendMessage(sender, "Scene not found in player world"); + return; + } + scene = genshinScene; + break; + default: + CommandHandler.sendMessage(sender, "Usage: killall [playerUid] [sceneId]"); + return; + } + + scene.getEntities().values().stream() + .filter(entity -> entity instanceof EntityMonster) + .forEach(entity -> scene.killEntity(entity, 0)); + CommandHandler.sendMessage(sender, "Killing all monsters in scene " + scene.getId()); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid arguments."); + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/ListCommand.java b/src/main/java/emu/grasscutter/command/commands/ListCommand.java new file mode 100644 index 000000000..6afca4a6d --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/ListCommand.java @@ -0,0 +1,33 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; +import java.util.Map; + +@Command(label = "list", description = "List online players") +public class ListCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + Map playersMap = Grasscutter.getGameServer().getPlayers(); + + CommandHandler.sendMessage(sender, String.format("There are %s player(s) online:", playersMap.size())); + + if (playersMap.size() != 0) { + StringBuilder playerSet = new StringBuilder(); + + for (Map.Entry entry : playersMap.entrySet()) { + playerSet.append(entry.getValue().getNickname()); + playerSet.append(", "); + } + + String players = playerSet.toString(); + + CommandHandler.sendMessage(sender, players.substring(0, players.length() - 2)); + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java new file mode 100644 index 000000000..7218bf2cd --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java @@ -0,0 +1,50 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "permission", usage = "permission ", + description = "Grants or removes a permission for a user", permission = "*") +public final class PermissionCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (args.size() < 3) { + CommandHandler.sendMessage(sender, "Usage: permission "); + return; + } + + String action = args.get(0); + String username = args.get(1); + String permission = args.get(2); + + Account account = Grasscutter.getGameServer().getAccountByName(username); + if (account == null) { + CommandHandler.sendMessage(sender, "Account not found."); + return; + } + + switch (action) { + default: + CommandHandler.sendMessage(sender, "Usage: permission "); + break; + case "add": + if (account.addPermission(permission)) { + CommandHandler.sendMessage(sender, "Permission added."); + } else CommandHandler.sendMessage(sender, "They already have this permission!"); + break; + case "remove": + if (account.removePermission(permission)) { + CommandHandler.sendMessage(sender, "Permission removed."); + } else CommandHandler.sendMessage(sender, "They don't have this permission!"); + break; + } + + account.save(); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java new file mode 100644 index 000000000..639301c7f --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java @@ -0,0 +1,22 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "position", usage = "position", aliases = {"pos"}, + description = "Get coordinates.") +public final class PositionCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + sender.dropMessage(String.format("Coord: %.3f, %.3f, %.3f", sender.getPos().getX(), sender.getPos().getY(), sender.getPos().getZ())); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java new file mode 100644 index 000000000..b145aaf94 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -0,0 +1,22 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "reload", usage = "reload", + description = "Reload server config", permission = "server.reload") +public final class ReloadCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + CommandHandler.sendMessage(sender, "Reloading config."); + Grasscutter.loadConfig(); + Grasscutter.getGameServer().getGachaManager().load(); + Grasscutter.getDispatchServer().loadQueries(); + CommandHandler.sendMessage(sender, "Reload complete."); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java new file mode 100644 index 000000000..77f16f9b8 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java @@ -0,0 +1,45 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.avatar.GenshinAvatar; +import emu.grasscutter.game.entity.EntityAvatar; + +import java.util.List; + +@Command(label = "resetconst", usage = "resetconst [all]", + description = "Resets the constellation level on your current active character, will need to relog after using the command to see any changes.", + aliases = {"resetconstellation"}, permission = "player.resetconstellation") +public final class ResetConstCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + if (args.size() > 0 && args.get(0).equalsIgnoreCase("all")) { + sender.getAvatars().forEach(this::resetConstellation); + sender.dropMessage("Reset all avatars' constellations."); + } else { + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + if (entity == null) { + return; + } + + GenshinAvatar avatar = entity.getAvatar(); + this.resetConstellation(avatar); + + sender.dropMessage("Constellations for " + avatar.getAvatarData().getName() + " have been reset. Please relog to see changes."); + } + } + + private void resetConstellation(GenshinAvatar avatar) { + avatar.getTalentIdList().clear(); + avatar.setCoreProudSkillLevel(0); + avatar.recalcStats(); + avatar.save(); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/RestartCommand.java b/src/main/java/emu/grasscutter/command/commands/RestartCommand.java new file mode 100644 index 000000000..cf40fa9bd --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/RestartCommand.java @@ -0,0 +1,16 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "restart", usage = "restart - Restarts the current session") +public final class RestartCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + sender.getSession().close(); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java new file mode 100644 index 000000000..061009c19 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -0,0 +1,37 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "say", usage = "say ", description = "Sends a message to a player as the server", + aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage") +public final class SendMessageCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (args.size() < 2) { + CommandHandler.sendMessage(null, "Usage: sendmessage "); + return; + } + + try { + int target = Integer.parseInt(args.get(0)); + String message = String.join(" ", args.subList(1, args.size())); + + GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found."); + return; + } + + CommandHandler.sendMessage(targetPlayer, message); + CommandHandler.sendMessage(sender, "Message sent."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid player ID."); + } + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java new file mode 100644 index 000000000..e3efdb0d5 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -0,0 +1,239 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; + +import java.util.List; + +@Command(label = "setstats", usage = "setstats|stats ", + description = "Set fight property for your current active character", aliases = {"stats"}, permission = "player.setstats") +public final class SetStatsCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + if (args.size() < 2){ + CommandHandler.sendMessage(sender, "Usage: setstats|stats "); + return; + } + + String stat = args.get(0); + switch (stat) { + default: + CommandHandler.sendMessage(sender, "Usage: /setstats|stats for basic stats"); + CommandHandler.sendMessage(sender, "Usage: /stats for elemental bonus"); + return; + case "hp": + try { + int health = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, health); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + CommandHandler.sendMessage(sender, "HP set to " + health + "."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid HP value."); + return; + } + break; + case "def": + try { + int def = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_DEFENSE)); + CommandHandler.sendMessage(sender, "DEF set to " + def + "."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid DEF value."); + return; + } + break; + case "atk": + try { + int atk = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_ATTACK)); + CommandHandler.sendMessage(sender, "ATK set to " + atk + "."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid ATK value."); + return; + } + break; + case "em": + try { + int em = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + entity.setFightProperty(FightProperty.FIGHT_PROP_ELEMENT_MASTERY, em); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_ELEMENT_MASTERY)); + CommandHandler.sendMessage(sender, "Elemental Mastery set to " + em + "."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid EM value."); + return; + } + break; + case "er": + try { + float er = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float erecharge = er / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, erecharge); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY)); + float iger = erecharge * 100; + CommandHandler.sendMessage(sender, "Energy recharge set to " + iger + "%."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid ER value."); + return; + } + break; + case "crate": + try { + float cr = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float crate = cr / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL, crate); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CRITICAL)); + float igcrate = crate * 100; + CommandHandler.sendMessage(sender, "Crit Rate set to " + igcrate + "%."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Crit Rate value."); + return; + } + break; + case "cdmg": + try { + float cdmg = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float cdamage = cdmg / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, cdamage); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CRITICAL_HURT)); + float igcdmg = cdamage * 100; + CommandHandler.sendMessage(sender, "Crit DMG set to " + igcdmg + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Crit DMG value."); + return; + } + break; + case "epyro": + try { + float epyro = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float pyro = epyro / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_FIRE_ADD_HURT, pyro); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_FIRE_ADD_HURT)); + float igpyro = pyro * 100; + CommandHandler.sendMessage(sender, "Pyro DMG Bonus set to " + igpyro + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Pyro DMG Bonus value."); + return; + } + break; + case "ecryo": + try { + float ecryo = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float cryo = ecryo / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_ICE_ADD_HURT, cryo); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_ICE_ADD_HURT)); + float igcyro = cryo * 100; + CommandHandler.sendMessage(sender, "Cyro DMG Bonus set to " + igcyro + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Cryo DMG Bonus value."); + return; + } + break; + case "ehydro": + try { + float ehydro = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float hydro = ehydro / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_WATER_ADD_HURT, hydro); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_WATER_ADD_HURT)); + float ighydro = hydro * 100; + CommandHandler.sendMessage(sender, "Hydro DMG Bonus set to " + ighydro + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Hydro DMG Bonus value."); + return; + } + break; + case "eanemo": + try { + float eanemo = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float anemo = eanemo / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_WIND_ADD_HURT, anemo); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_WIND_ADD_HURT)); + float iganemo = anemo * 100; + CommandHandler.sendMessage(sender, "Anemo DMG Bonus set to " + iganemo + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Anemo DMG Bonus value."); + return; + } + break; + case "egeo": + try { + float egeo = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float geo = egeo / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_ROCK_ADD_HURT, geo); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_ROCK_ADD_HURT)); + float iggeo = geo * 100; + CommandHandler.sendMessage(sender, "Geo DMG Bonus set to " + iggeo + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Geo DMG Bonus value."); + return; + } + break; + case "ethunder": + case "eelec": + try { + float eelec = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float elec = eelec / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, elec); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CRITICAL_HURT)); + float igelec = elec * 100; + CommandHandler.sendMessage(sender, "Electro DMG Bonus set to " + igelec + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Electro DMG Bonus value."); + return; + } + break; + case "ephys": + try { + float ephys = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float phys = ephys / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, phys); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT)); + float igphys = phys * 100; + CommandHandler.sendMessage(sender, "Physical DMG Bonus set to " + igphys + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Physical DMG Bonus value."); + return; + } + break; + case "edend": + try { + float edend = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + float dend = edend / 10000; + entity.setFightProperty(FightProperty.FIGHT_PROP_GRASS_ADD_HURT, dend); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_GRASS_ADD_HURT)); + float igdend = dend * 100; + CommandHandler.sendMessage(sender, "Dendro DMG Bonus set to " + igdend + "%"); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Dendro DMG Bonus value."); + return; + } + break; + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java new file mode 100644 index 000000000..146837523 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -0,0 +1,39 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.props.PlayerProperty; + +import java.util.List; + +@Command(label = "setworldlevel", usage = "setworldlevel ", + description = "Sets your world level (Relog to see proper effects)", + aliases = {"setworldlvl"}, permission = "player.setworldlevel") +public final class SetWorldLevelCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; // TODO: set player's world level from console or other players + } + + if (args.size() < 1) { + CommandHandler.sendMessage(sender, "Usage: setworldlevel "); + return; + } + + try { + int level = Integer.parseInt(args.get(0)); + + // Set in both world and player props + sender.getWorld().setWorldLevel(level); + sender.setProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL, level); + + sender.dropMessage("World level set to " + level + "."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(null, "Invalid world level."); + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java new file mode 100644 index 000000000..969872119 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -0,0 +1,51 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.def.MonsterData; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.utils.Position; + +import java.util.List; + +@Command(label = "spawn", usage = "spawn [level] [amount]", + description = "Spawns an entity near you", permission = "server.spawn") +public final class SpawnCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + if (args.size() < 1) { + CommandHandler.sendMessage(sender, "Usage: spawn [amount]"); + return; + } + + try { + int entity = Integer.parseInt(args.get(0)); + int level = args.size() > 1 ? Integer.parseInt(args.get(1)) : 1; + int amount = args.size() > 2 ? Integer.parseInt(args.get(2)) : 1; + + MonsterData entityData = GenshinData.getMonsterDataMap().get(entity); + if (entityData == null) { + CommandHandler.sendMessage(sender, "Invalid entity id."); + return; + } + + float range = (5f + (.1f * amount)); + for (int i = 0; i < amount; i++) { + Position pos = sender.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); + EntityMonster monster = new EntityMonster(sender.getScene(), entityData, pos, level); + sender.getScene().addEntity(monster); + } + CommandHandler.sendMessage(sender, String.format("Spawned %s of %s.", amount, entity)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid item or player ID."); + } + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/StopCommand.java b/src/main/java/emu/grasscutter/command/commands/StopCommand.java new file mode 100644 index 000000000..4b63cd44e --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/StopCommand.java @@ -0,0 +1,23 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +@Command(label = "stop", usage = "stop", + description = "Stops the server", permission = "server.stop") +public final class StopCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + CommandHandler.sendMessage(null, "Server shutting down..."); + for (GenshinPlayer p : Grasscutter.getGameServer().getPlayers().values()) { + CommandHandler.sendMessage(p, "Server shutting down..."); + } + + System.exit(1); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java new file mode 100644 index 000000000..bb9ae4a42 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -0,0 +1,41 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.props.ClimateType; +import emu.grasscutter.server.packet.send.PacketSceneAreaWeatherNotify; + +import java.util.List; + +@Command(label = "weather", usage = "weather [climateId]", + description = "Changes the weather.", aliases = {"w"}, permission = "player.weather") +public final class WeatherCommand implements CommandHandler { + + @Override + public void execute(GenshinPlayer sender, List args) { + if (sender == null) { + CommandHandler.sendMessage(null, "Run this command in-game."); + return; + } + + if (args.size() < 1) { + CommandHandler.sendMessage(sender, "Usage: weather [climateId]"); + return; + } + + try { + int weatherId = Integer.parseInt(args.get(0)); + int climateId = args.size() > 1 ? Integer.parseInt(args.get(1)) : 1; + + ClimateType climate = ClimateType.getTypeByValue(climateId); + + sender.getScene().setWeather(weatherId); + sender.getScene().setClimate(climate); + sender.getScene().broadcastPacket(new PacketSceneAreaWeatherNotify(sender)); + CommandHandler.sendMessage(sender, "Changed weather to " + weatherId + " with climate " + climateId); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid ID."); + } + } +} diff --git a/src/main/java/emu/grasscutter/commands/Command.java b/src/main/java/emu/grasscutter/commands/Command.java deleted file mode 100644 index d1e181e20..000000000 --- a/src/main/java/emu/grasscutter/commands/Command.java +++ /dev/null @@ -1,23 +0,0 @@ -package emu.grasscutter.commands; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface Command { - String label() default ""; - - String usage() default ""; - - String[] aliases() default {""}; - - Execution execution() default Execution.ALL; - - String permission() default ""; - - enum Execution { - ALL, - CONSOLE, - PLAYER - } -} diff --git a/src/main/java/emu/grasscutter/commands/CommandHandler.java b/src/main/java/emu/grasscutter/commands/CommandHandler.java deleted file mode 100644 index 97bd8c81f..000000000 --- a/src/main/java/emu/grasscutter/commands/CommandHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package emu.grasscutter.commands; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.GenshinPlayer; - -import java.util.List; - -public interface CommandHandler { - /* Invoked on player execution. */ - default void execute(GenshinPlayer player, List args) { } - /* Invoked on server execution. */ - default void execute(List args) { } - - /* - * Utilities. - */ - - /** - * Send a message to the target. - * @param player The player to send the message to, or null for the server console. - * @param message The message to send. - */ - static void sendMessage(GenshinPlayer player, String message) { - if(player == null) { - Grasscutter.getLogger().info(message); - } else player.dropMessage(message); - } -} diff --git a/src/main/java/emu/grasscutter/commands/PlayerCommands.java b/src/main/java/emu/grasscutter/commands/PlayerCommands.java deleted file mode 100644 index b6401740d..000000000 --- a/src/main/java/emu/grasscutter/commands/PlayerCommands.java +++ /dev/null @@ -1,508 +0,0 @@ -package emu.grasscutter.commands; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GenshinData; -import emu.grasscutter.data.def.ItemData; -import emu.grasscutter.data.def.AvatarData; -import emu.grasscutter.data.def.AvatarSkillDepotData; -import emu.grasscutter.data.def.MonsterData; -import emu.grasscutter.game.GenshinPlayer; -import emu.grasscutter.game.GenshinScene; -import emu.grasscutter.game.World; -import emu.grasscutter.game.avatar.GenshinAvatar; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntityItem; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.inventory.GenshinItem; -import emu.grasscutter.game.inventory.Inventory; -import emu.grasscutter.game.inventory.ItemType; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; -import emu.grasscutter.utils.Position; - -import java.util.LinkedList; -import java.util.List; - -/** - * A container for player-related commands. - */ -public final class PlayerCommands { - @Command(label = "give", aliases = {"g", "item", "giveitem"}, - usage = "Usage: give [player] [amount]") - public static class GiveCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - int target, item, amount = 1; - - switch(args.size()) { - default: - CommandHandler.sendMessage(player, "Usage: give [amount]"); - return; - case 1: - try { - item = Integer.parseInt(args.get(0)); - target = player.getAccount().getPlayerId(); - } catch (NumberFormatException ignored) { - // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(player, "Invalid item id."); - return; - } - break; - case 2: - try { - target = Integer.parseInt(args.get(0)); - - if(Grasscutter.getGameServer().getPlayerByUid(target) == null) { - target = player.getUid(); amount = Integer.parseInt(args.get(1)); - item = Integer.parseInt(args.get(0)); - } else { - item = Integer.parseInt(args.get(1)); - } - } catch (NumberFormatException ignored) { - // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(player, "Invalid item or player ID."); - return; - } - break; - case 3: - try { - target = Integer.parseInt(args.get(0)); - - if(Grasscutter.getGameServer().getPlayerByUid(target) == null) { - CommandHandler.sendMessage(player, "Invalid player ID."); return; - } - - item = Integer.parseInt(args.get(1)); - amount = Integer.parseInt(args.get(2)); - } catch (NumberFormatException ignored) { - // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(player, "Invalid item or player ID."); - return; - } - break; - } - - GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); - - if(targetPlayer == null) { - CommandHandler.sendMessage(player, "Player not found."); return; - } - - ItemData itemData = GenshinData.getItemDataMap().get(item); - if(itemData == null) { - CommandHandler.sendMessage(player, "Invalid item id."); return; - } - - this.item(targetPlayer, itemData, amount); - } - - /** - * give [player] [itemId|itemName] [amount] - */ - @Override public void execute(List args) { - if(args.size() < 2) { - CommandHandler.sendMessage(null, "Usage: give [amount]"); - return; - } - - try { - int target = Integer.parseInt(args.get(0)); - int item = Integer.parseInt(args.get(1)); - int amount = 1; if(args.size() > 2) amount = Integer.parseInt(args.get(2)); - - GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); - - if(targetPlayer == null) { - CommandHandler.sendMessage(null, "Player not found."); return; - } - - ItemData itemData = GenshinData.getItemDataMap().get(item); - if(itemData == null) { - CommandHandler.sendMessage(null, "Invalid item id."); return; - } - - this.item(targetPlayer, itemData, amount); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid item or player ID."); - } - } - - private void item(GenshinPlayer player, ItemData itemData, int amount) { - GenshinItem genshinItem = new GenshinItem(itemData); - if(itemData.isEquip()) { - List items = new LinkedList<>(); - for(int i = 0; i < amount; i++) { - items.add(genshinItem); - } player.getInventory().addItems(items); - player.sendPacket(new PacketItemAddHintNotify(items, ActionReason.SubfieldDrop)); - } else { - genshinItem.setCount(amount); - player.getInventory().addItem(genshinItem); - player.sendPacket(new PacketItemAddHintNotify(genshinItem, ActionReason.SubfieldDrop)); - } - } - } - - @Command(label = "drop", aliases = {"d", "dropitem"}, - usage = "Usage: drop [amount]", - execution = Command.Execution.PLAYER) - public static class DropCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() < 1) { - CommandHandler.sendMessage(player, "Usage: drop [amount]"); - return; - } - - try { - int item = Integer.parseInt(args.get(0)); - int amount = 1; if(args.size() > 1) amount = Integer.parseInt(args.get(1)); - - ItemData itemData = GenshinData.getItemDataMap().get(item); - if(itemData == null) { - CommandHandler.sendMessage(player, "Invalid item id."); return; - } - - if (itemData.isEquip()) { - float range = (5f + (.1f * amount)); - for (int i = 0; i < amount; i++) { - Position pos = player.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); - EntityItem entity = new EntityItem(player.getScene(), player, itemData, pos, 1); - player.getScene().addEntity(entity); - } - } else { - EntityItem entity = new EntityItem(player.getScene(), player, itemData, player.getPos().clone().addY(3f), amount); - player.getScene().addEntity(entity); - } - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(player, "Invalid item or player ID."); - } - } - } - - @Command(label = "givechar", aliases = {"givec"}, - usage = "Usage: givechar [level|avatarId] [level]") - public static class GiveCharCommand implements CommandHandler { - @Override public void execute(GenshinPlayer player, List args) { - int target, avatarId, level = 1, ascension = 1; - - if(args.size() < 1) { - CommandHandler.sendMessage(player, "Usage: givechar [level]"); - return; - } - - switch(args.size()) { - default: - CommandHandler.sendMessage(player, "Usage: givechar [level]"); - return; - case 2: - try { - target = Integer.parseInt(args.get(0)); - if(Grasscutter.getGameServer().getPlayerByUid(target) == null) { - target = player.getUid(); - level = Integer.parseInt(args.get(1)); - avatarId = Integer.parseInt(args.get(0)); - } else { - avatarId = Integer.parseInt(args.get(1)); - } - } catch (NumberFormatException ignored) { - // TODO: Parse from avatar name using GM Handbook. - CommandHandler.sendMessage(player, "Invalid avatar or player ID."); - return; - } - break; - case 3: - try { - target = Integer.parseInt(args.get(0)); - if(Grasscutter.getGameServer().getPlayerByUid(target) == null) { - CommandHandler.sendMessage(player, "Invalid player ID."); return; - } - - avatarId = Integer.parseInt(args.get(1)); - level = Integer.parseInt(args.get(2)); - } catch (NumberFormatException ignored) { - // TODO: Parse from avatar name using GM Handbook. - CommandHandler.sendMessage(player, "Invalid avatar or player ID."); - return; - } - break; - } - - GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); - if(targetPlayer == null) { - CommandHandler.sendMessage(player, "Player not found."); return; - } - - AvatarData avatarData = GenshinData.getAvatarDataMap().get(avatarId); - if(avatarData == null) { - CommandHandler.sendMessage(player, "Invalid avatar id."); return; - } - - // Calculate ascension level. - if (level <= 40) { - ascension = (int) Math.ceil(level / 20f); - } else { - ascension = (int) Math.ceil(level / 10f) - 3; - } - - GenshinAvatar avatar = new GenshinAvatar(avatarId); - avatar.setLevel(level); - avatar.setPromoteLevel(ascension); - - // This will handle stats and talents - avatar.recalcStats(); - - targetPlayer.addAvatar(avatar); - } - - @Override - public void execute(List args) { - if(args.size() < 2) { - CommandHandler.sendMessage(null, "Usage: givechar [amount]"); - return; - } - - try { - int target = Integer.parseInt(args.get(0)); - int avatarID = Integer.parseInt(args.get(1)); - int level = 1; if(args.size() > 2) level = Integer.parseInt(args.get(2)); - int ascension; - - GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); - if(targetPlayer == null) { - CommandHandler.sendMessage(null, "Player not found."); return; - } - - AvatarData avatarData = GenshinData.getAvatarDataMap().get(avatarID); - if(avatarData == null) { - CommandHandler.sendMessage(null, "Invalid avatar id."); return; - } - - // Calculate ascension level. - if (level <= 40) { - ascension = (int) Math.ceil(level / 20f); - } else { - ascension = (int) Math.ceil(level / 10f) - 3; - } - - GenshinAvatar avatar = new GenshinAvatar(avatarID); - avatar.setLevel(level); - avatar.setPromoteLevel(ascension); - - // This will handle stats and talents - avatar.recalcStats(); - - targetPlayer.addAvatar(avatar); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid item or player ID."); - } - } - } - - @Command(label = "spawn", execution = Command.Execution.PLAYER, - usage = "Usage: spawn [level] [amount]") - public static class SpawnCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() < 1) { - CommandHandler.sendMessage(null, "Usage: spawn [amount]"); - return; - } - - try { - int entity = Integer.parseInt(args.get(0)); - int level = 1; if(args.size() > 1) level = Integer.parseInt(args.get(1)); - int amount = 1; if(args.size() > 2) amount = Integer.parseInt(args.get(2)); - - MonsterData entityData = GenshinData.getMonsterDataMap().get(entity); - if(entityData == null) { - CommandHandler.sendMessage(null, "Invalid entity id."); return; - } - - float range = (5f + (.1f * amount)); - for (int i = 0; i < amount; i++) { - Position pos = player.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); - EntityMonster monster = new EntityMonster(player.getScene(), entityData, pos, level); - player.getScene().addEntity(monster); - } - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid item or player ID."); - } - } - } - - @Command(label = "killall", - usage = "Usage: killall [playerUid] [sceneId]") - public static class KillAllCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - GenshinScene scene = player.getScene(); - scene.getEntities().values().stream() - .filter(entity -> entity instanceof EntityMonster) - .forEach(entity -> scene.killEntity(entity, 0)); - CommandHandler.sendMessage(null, "Killing all monsters in scene " + scene.getId()); - } - - @Override - public void execute(List args) { - if(args.size() < 2) { - CommandHandler.sendMessage(null, "Usage: killall [playerUid] [sceneId]"); return; - } - - try { - int playerUid = Integer.parseInt(args.get(0)); - int sceneId = Integer.parseInt(args.get(1)); - - GenshinPlayer player = Grasscutter.getGameServer().getPlayerByUid(playerUid); - if (player == null) { - CommandHandler.sendMessage(null, "Player not found or offline."); - return; - } - - GenshinScene scene = player.getWorld().getSceneById(sceneId); - if (scene == null) { - CommandHandler.sendMessage(null, "Scene not found in player world"); - return; - } - - scene.getEntities().values().stream() - .filter(entity -> entity instanceof EntityMonster) - .forEach(entity -> scene.killEntity(entity, 0)); - CommandHandler.sendMessage(null, "Killing all monsters in scene " + scene.getId()); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid arguments."); - } - } - } - - @Command(label = "resetconst", aliases = {"resetconstellation"}, - usage = "Usage: resetconst [all]", execution = Command.Execution.PLAYER) - public static class ResetConstellationCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() > 0 && args.get(0).equalsIgnoreCase("all")) { - player.getAvatars().forEach(this::resetConstellation); - player.dropMessage("Reset all avatars' constellations."); - } else { - EntityAvatar entity = player.getTeamManager().getCurrentAvatarEntity(); - if(entity == null) - return; - - GenshinAvatar avatar = entity.getAvatar(); - this.resetConstellation(avatar); - - player.dropMessage("Constellations for " + avatar.getAvatarData().getName() + " have been reset. Please relog to see changes."); - } - } - - private void resetConstellation(GenshinAvatar avatar) { - avatar.getTalentIdList().clear(); - avatar.setCoreProudSkillLevel(0); - avatar.recalcStats(); - avatar.save(); - } - } - - @Command(label = "godmode", - usage = "Usage: godmode", execution = Command.Execution.PLAYER) - public static class GodModeCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - player.setGodmode(!player.inGodmode()); - player.dropMessage("Godmode is now " + (player.inGodmode() ? "enabled" : "disabled") + "."); - } - } - - @Command(label = "sethealth", aliases = {"sethp"}, - usage = "Usage: sethealth ", execution = Command.Execution.PLAYER) - public static class SetHealthCommand implements CommandHandler { - - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() < 1) { - CommandHandler.sendMessage(null, "Usage: sethealth "); return; - } - - try { - int health = Integer.parseInt(args.get(0)); - EntityAvatar entity = player.getTeamManager().getCurrentAvatarEntity(); - if(entity == null) - return; - - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, health); - entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); - player.dropMessage("Health set to " + health + "."); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid health value."); - } - } - } - - @Command(label = "setworldlevel", aliases = {"setworldlvl"}, - usage = "Usage: setworldlevel ", execution = Command.Execution.PLAYER) - public static class SetWorldLevelCommand implements CommandHandler { - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() < 1) { - CommandHandler.sendMessage(player, "Usage: setworldlevel "); return; - } - - try { - int level = Integer.parseInt(args.get(0)); - - // Set in both world and player props - player.getWorld().setWorldLevel(level); - player.setProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL, level); - - player.dropMessage("World level set to " + level + "."); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid world level."); - } - } - } - - @Command(label = "clearartifacts", aliases = {"clearart"}, - usage = "Usage: clearartifacts", execution = Command.Execution.PLAYER) - public static class ClearArtifactsCommand implements CommandHandler { - @Override - public void execute(GenshinPlayer player, List args) { - Inventory playerInventory = player.getInventory(); - playerInventory.getItems().values().stream() - .filter(item -> item.getItemType() == ItemType.ITEM_RELIQUARY) - .filter(item -> item.getLevel() == 1 && item.getExp() == 0) - .filter(item -> !item.isLocked() && !item.isEquipped()) - .forEach(item -> playerInventory.removeItem(item, item.getCount())); - } - } - - @Command(label = "changescene", aliases = {"scene"}, - usage = "Usage: changescene ", execution = Command.Execution.PLAYER) - public static class ChangeSceneCommand implements CommandHandler { - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() < 1) { - CommandHandler.sendMessage(player, "Usage: changescene "); return; - } - - try { - int sceneId = Integer.parseInt(args.get(0)); - boolean result = player.getWorld().transferPlayerToScene(player, sceneId, player.getPos()); - - if (!result) { - CommandHandler.sendMessage(null, "Scene does not exist or you are already in it"); - } - } catch (Exception e) { - CommandHandler.sendMessage(player, "Usage: changescene "); return; - } - } - } -} diff --git a/src/main/java/emu/grasscutter/commands/ServerCommands.java b/src/main/java/emu/grasscutter/commands/ServerCommands.java deleted file mode 100644 index 0faf0e7f9..000000000 --- a/src/main/java/emu/grasscutter/commands/ServerCommands.java +++ /dev/null @@ -1,216 +0,0 @@ -package emu.grasscutter.commands; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.game.GenshinPlayer; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * A container for server-related commands. - */ -public final class ServerCommands { - @Command(label = "reload", usage = "Usage: reload") - public static class ReloadCommand implements CommandHandler { - - @Override - public void execute(List args) { - Grasscutter.getLogger().info("Reloading config."); - Grasscutter.loadConfig(); - Grasscutter.getDispatchServer().loadQueries(); - Grasscutter.getLogger().info("Reload complete."); - } - - @Override - public void execute(GenshinPlayer player, List args) { - this.execute(args); - } - } - - @Command(label = "sendmessage", aliases = {"sendmsg", "msg"}, - usage = "Usage: sendmessage ") - public static class SendMessageCommand implements CommandHandler { - - @Override - public void execute(List args) { - if(args.size() < 2) { - CommandHandler.sendMessage(null, "Usage: sendmessage "); return; - } - - try { - int target = Integer.parseInt(args.get(0)); - String message = String.join(" ", args.subList(1, args.size())); - - GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); - - if(targetPlayer == null) { - CommandHandler.sendMessage(null, "Player not found."); return; - } - - targetPlayer.dropMessage(message); - CommandHandler.sendMessage(null, "Message sent."); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid player ID."); - } - } - - @Override - public void execute(GenshinPlayer player, List args) { - if(args.size() < 2) { - CommandHandler.sendMessage(player, "Usage: sendmessage "); return; - } - - try { - int target = Integer.parseInt(args.get(0)); - String message = String.join(" ", args.subList(1, args.size())); - - GenshinPlayer targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); - - if(targetPlayer == null) { - CommandHandler.sendMessage(player, "Player not found."); return; - } - - targetPlayer.sendMessage(player, message); - CommandHandler.sendMessage(player, "Message sent."); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(player, "Invalid player ID."); - } - } - } - - @Command(label = "account", - usage = "Usage: account [uid]", - execution = Command.Execution.CONSOLE) - public static class AccountCommand implements CommandHandler { - - @Override - public void execute(List args) { - if(args.size() < 2) { - CommandHandler.sendMessage(null, "Usage: account [uid]"); return; - } - - String action = args.get(0); - String username = args.get(1); - - switch(action) { - default: - CommandHandler.sendMessage(null, "Usage: account [uid]"); - return; - case "create": - int uid = 0; - if(args.size() > 2) { - try { - uid = Integer.parseInt(args.get(2)); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, "Invalid UID."); return; - } - } - - Account account = DatabaseHelper.createAccountWithId(username, uid); - if(account == null) { - CommandHandler.sendMessage(null, "Account already exists."); return; - } else { - CommandHandler.sendMessage(null, "Account created with UID " + account.getPlayerId() + "."); - account.addPermission("*"); // Grant the player superuser permissions. - } - return; - case "delete": - if(DatabaseHelper.deleteAccount(username)) { - CommandHandler.sendMessage(null, "Account deleted."); return; - } else CommandHandler.sendMessage(null, "Account not found."); - return; - } - } - } - - @Command(label = "permission", - usage = "Usage: permission ", - execution = Command.Execution.CONSOLE) - public static class PermissionCommand implements CommandHandler { - - @Override - public void execute(List args) { - if(args.size() < 3) { - CommandHandler.sendMessage(null, "Usage: permission "); return; - } - - String action = args.get(0); - String username = args.get(1); - String permission = args.get(2); - - Account account = Grasscutter.getGameServer().getAccountByName(username); - if(account == null) { - CommandHandler.sendMessage(null, "Account not found."); return; - } - - switch(action) { - default: - CommandHandler.sendMessage(null, "Usage: permission "); - break; - case "add": - if(account.addPermission(permission)) { - CommandHandler.sendMessage(null, "Permission added."); - } else CommandHandler.sendMessage(null, "They already have this permission!"); - break; - case "remove": - if(account.removePermission(permission)) { - CommandHandler.sendMessage(null, "Permission removed."); - } else CommandHandler.sendMessage(null, "They don't have this permission!"); - break; - } - - account.save(); - } - } - - @Command(label = "help", - usage = "Usage: help [command]") - public static class HelpCommand implements CommandHandler { - - @Override - public void execute(List args) { - List handlers = CommandMap.getInstance().getHandlers(); - List annotations = handlers.stream() - .map(handler -> handler.getClass().getAnnotation(Command.class)) - .collect(Collectors.toList()); - - if(args.size() < 1) { - StringBuilder builder = new StringBuilder("Available commands:\n"); - annotations.forEach(annotation -> builder.append(annotation.usage()).append("\n")); - CommandHandler.sendMessage(null, builder.toString()); - } else { - String command = args.get(0); - CommandHandler handler = CommandMap.getInstance().getHandler(command); - if(handler == null) { - CommandHandler.sendMessage(null, "Command not found."); return; - } - - Command annotation = handler.getClass().getAnnotation(Command.class); - CommandHandler.sendMessage(null, annotation.usage()); - } - } - - @Override - public void execute(GenshinPlayer player, List args) { - List handlers = CommandMap.getInstance().getHandlers(); - List annotations = handlers.stream() - .map(handler -> handler.getClass().getAnnotation(Command.class)) - .collect(Collectors.toList()); - - if(args.size() < 1) { - annotations.forEach(annotation -> player.dropMessage(annotation.usage())); - } else { - String command = args.get(0); - CommandHandler handler = CommandMap.getInstance().getHandler(command); - if(handler == null) { - CommandHandler.sendMessage(player, "Command not found."); return; - } - - Command annotation = handler.getClass().getAnnotation(Command.class); - CommandHandler.sendMessage(player, annotation.usage()); - } - } - } -} diff --git a/src/main/java/emu/grasscutter/data/GenshinData.java b/src/main/java/emu/grasscutter/data/GenshinData.java index bb2709108..7e14b1b46 100644 --- a/src/main/java/emu/grasscutter/data/GenshinData.java +++ b/src/main/java/emu/grasscutter/data/GenshinData.java @@ -1,13 +1,16 @@ package emu.grasscutter.data; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import emu.grasscutter.Grasscutter; import emu.grasscutter.utils.Utils; import emu.grasscutter.data.custom.AbilityEmbryoEntry; import emu.grasscutter.data.custom.OpenConfigEntry; +import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.data.def.*; import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -18,6 +21,7 @@ public class GenshinData { private static final Int2ObjectMap abilityHashes = new Int2ObjectOpenHashMap<>(); private static final Map abilityEmbryos = new HashMap<>(); private static final Map openConfigEntries = new HashMap<>(); + private static final Map scenePointEntries = new HashMap<>(); // ExcelConfigs private static final Int2ObjectMap playerLevelDataMap = new Int2ObjectOpenHashMap<>(); @@ -52,6 +56,10 @@ public class GenshinData { private static final Int2ObjectMap avatarCostumeDataItemIdMap = new Int2ObjectLinkedOpenHashMap<>(); private static final Int2ObjectMap sceneDataMap = new Int2ObjectLinkedOpenHashMap<>(); + private static final Int2ObjectMap fetterDataMap = new Int2ObjectOpenHashMap<>(); + + // Cache + private static Map> fetters = new HashMap<>(); public static Int2ObjectMap getMapByResourceDef(Class resourceDefinition) { Int2ObjectMap map = null; @@ -82,6 +90,10 @@ public class GenshinData { return openConfigEntries; } + public static Map getScenePointEntries() { + return scenePointEntries; + } + public static Int2ObjectMap getAvatarDataMap() { return avatarDataMap; } @@ -215,4 +227,17 @@ public class GenshinData { public static Int2ObjectMap getSceneDataMap() { return sceneDataMap; } + + public static Map> getFetterDataEntries() { + if (fetters.isEmpty()) { + fetterDataMap.forEach((k, v) -> { + if (!fetters.containsKey(v.getAvatarId())) { + fetters.put(v.getAvatarId(), new ArrayList<>()); + } + fetters.get(v.getAvatarId()).add(k); + }); + } + + return fetters; + } } diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index f5b9f4f72..bb95d70e9 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -10,11 +10,15 @@ import java.util.regex.Pattern; import emu.grasscutter.utils.Utils; import org.reflections.Reflections; +import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.common.PointData; +import emu.grasscutter.data.common.ScenePointConfig; import emu.grasscutter.data.custom.AbilityEmbryoEntry; import emu.grasscutter.data.custom.OpenConfigEntry; +import emu.grasscutter.data.custom.ScenePointEntry; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; public class ResourceLoader { @@ -42,6 +46,7 @@ public class ResourceLoader { loadOpenConfig(); // Load resources loadResources(); + loadScenePoints(); // Process into depots GenshinDepot.load(); // Custom - TODO move this somewhere else @@ -121,6 +126,51 @@ public class ResourceLoader { } } + private static void loadScenePoints() { + Pattern pattern = Pattern.compile("(?<=scene)(.*?)(?=_point.json)"); + File folder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Scene/Point"); + + if (!folder.isDirectory() || !folder.exists() || folder.listFiles() == null) { + Grasscutter.getLogger().error("Scene point files cannot be found, you cannot use teleport waypoints!"); + return; + } + + List scenePointList = new ArrayList<>(); + for (File file : folder.listFiles()) { + ScenePointConfig config = null; + Integer sceneId = null; + + Matcher matcher = pattern.matcher(file.getName()); + if (matcher.find()) { + sceneId = Integer.parseInt(matcher.group(1)); + } else { + continue; + } + + try (FileReader fileReader = new FileReader(file)) { + config = Grasscutter.getGsonFactory().fromJson(fileReader, ScenePointConfig.class); + } catch (Exception e) { + e.printStackTrace(); + continue; + } + + if (config.points == null) { + continue; + } + + for (Map.Entry entry : config.points.entrySet()) { + PointData pointData = Grasscutter.getGsonFactory().fromJson(entry.getValue(), PointData.class); + + ScenePointEntry sl = new ScenePointEntry(sceneId + "_" + entry.getKey(), pointData); + scenePointList.add(sl); + } + + for (ScenePointEntry entry : scenePointList) { + GenshinData.getScenePointEntries().put(entry.getName(), entry); + } + } + } + private static void loadAbilityEmbryos() { // Read from cached file if exists File embryoCache = new File(Grasscutter.getConfig().DATA_FOLDER + "AbilityEmbryos.json"); @@ -197,7 +247,7 @@ public class ResourceLoader { } else { Map map = new TreeMap<>(); java.lang.reflect.Type type = new TypeToken>() {}.getType(); - String[] folderNames = {"BinOutput\\Talent\\EquipTalents\\", "BinOutput\\Talent\\AvatarTalents\\"}; + String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"}; for (String name : folderNames) { File folder = new File(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + name)); diff --git a/src/main/java/emu/grasscutter/data/common/PointData.java b/src/main/java/emu/grasscutter/data/common/PointData.java new file mode 100644 index 000000000..7c31d2f06 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/common/PointData.java @@ -0,0 +1,43 @@ +package emu.grasscutter.data.common; + +public class PointData { + private pos tranPos; + + public pos getTranPos() { + return tranPos; + } + + public void setTranPos(pos tranPos) { + this.tranPos = tranPos; + } + + public class pos { + private float x; + private float y; + private float z; + + public float getX() { + return x; + } + + public void setX(float x) { + this.x = x; + } + + public float getY() { + return y; + } + + public void setY(float y) { + this.y = y; + } + + public float getZ() { + return z; + } + + public void setZ(float z) { + this.z = z; + } + } +} diff --git a/src/main/java/emu/grasscutter/data/common/ScenePointConfig.java b/src/main/java/emu/grasscutter/data/common/ScenePointConfig.java new file mode 100644 index 000000000..340f7476f --- /dev/null +++ b/src/main/java/emu/grasscutter/data/common/ScenePointConfig.java @@ -0,0 +1,15 @@ +package emu.grasscutter.data.common; + +import com.google.gson.JsonObject; + +public class ScenePointConfig { + public JsonObject points; + + public JsonObject getPoints() { + return points; + } + + public void setPoints(JsonObject Points) { + points = Points; + } +} diff --git a/src/main/java/emu/grasscutter/data/custom/ScenePointEntry.java b/src/main/java/emu/grasscutter/data/custom/ScenePointEntry.java new file mode 100644 index 000000000..3e904f5c8 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/ScenePointEntry.java @@ -0,0 +1,21 @@ +package emu.grasscutter.data.custom; + +import emu.grasscutter.data.common.PointData; + +public class ScenePointEntry { + private String name; + private PointData pointData; + + public ScenePointEntry(String name, PointData pointData) { + this.name = name; + this.pointData = pointData; + } + + public String getName() { + return name; + } + + public PointData getPointData() { + return pointData; + } +} diff --git a/src/main/java/emu/grasscutter/data/def/AvatarData.java b/src/main/java/emu/grasscutter/data/def/AvatarData.java index dcc503994..8097e04a7 100644 --- a/src/main/java/emu/grasscutter/data/def/AvatarData.java +++ b/src/main/java/emu/grasscutter/data/def/AvatarData.java @@ -55,6 +55,8 @@ public class AvatarData extends GenshinResource { private float[] defenseGrowthCurve; private AvatarSkillDepotData skillDepot; private IntList abilities; + + private List fetters; @Override public int getId(){ @@ -193,9 +195,16 @@ public class AvatarData extends GenshinResource { return abilities; } + public List getFetters() { + return fetters; + } + @Override public void onLoad() { this.skillDepot = GenshinData.getAvatarSkillDepotDataMap().get(this.SkillDepotId); + + // Get fetters from GenshinData + this.fetters = GenshinData.getFetterDataEntries().get(this.Id); int size = GenshinData.getAvatarCurveDataMap().size(); this.hpGrowthCurve = new float[size]; diff --git a/src/main/java/emu/grasscutter/data/def/FetterData.java b/src/main/java/emu/grasscutter/data/def/FetterData.java new file mode 100644 index 000000000..d17c940d1 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/FetterData.java @@ -0,0 +1,24 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GenshinResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.ResourceType.LoadPriority; + +@ResourceType(name = {"FetterInfoExcelConfigData.json", "FettersExcelConfigData.json", "FetterStoryExcelConfigData.json"}, loadPriority = LoadPriority.HIGHEST) +public class FetterData extends GenshinResource { + private int AvatarId; + private int FetterId; + + @Override + public int getId() { + return FetterId; + } + + public int getAvatarId() { + return AvatarId; + } + + @Override + public void onLoad() { + } +} diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 0289c3e71..ed092e976 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -74,36 +74,36 @@ public class DatabaseHelper { } public static void saveAccount(Account account) { - DatabaseManager.getDatastore().save(account); + DatabaseManager.getAccountDatastore().save(account); } public static Account getAccountByName(String username) { - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static Account getAccountByToken(String token) { if (token == null) return null; - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("token").equal(token).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("token").equal(token).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static Account getAccountById(String uid) { - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("_id").equal(uid).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("_id").equal(uid).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static Account getAccountByPlayerId(int playerId) { - MorphiaCursor cursor = DatabaseManager.getDatastore().createQuery(Account.class).field("playerId").equal(playerId).find(FIND_ONE); + MorphiaCursor cursor = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("playerId").equal(playerId).find(FIND_ONE); if (!cursor.hasNext()) return null; return cursor.next(); } public static boolean deleteAccount(String username) { - Query q = DatabaseManager.getDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username); + Query q = DatabaseManager.getAccountDatastore().createQuery(Account.class).field("username").equalIgnoreCase(username); return DatabaseManager.getDatastore().findAndDelete(q) != null; } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 97e27a81a..46dad537d 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -17,7 +17,10 @@ import emu.grasscutter.game.inventory.GenshinItem; public final class DatabaseManager { private static MongoClient mongoClient; + private static MongoClient dispatchMongoClient; + private static Datastore datastore; + private static Datastore dispatchDatastore; private static final Class[] mappedClasses = new Class[] { DatabaseCounter.class, Account.class, GenshinPlayer.class, GenshinAvatar.class, GenshinItem.class, Friendship.class @@ -26,14 +29,24 @@ public final class DatabaseManager { public static MongoClient getMongoClient() { return mongoClient; } - - public static Datastore getDatastore() { - return datastore; - } - - public static MongoDatabase getDatabase() { + + public static Datastore getDatastore() { + return datastore; + } + + public static MongoDatabase getDatabase() { return getDatastore().getDatabase(); } + + // Yes. I very dislike this method. However, this will be good for now. + // TODO: Add dispatch routes for player account management + public static Datastore getAccountDatastore() { + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + return dispatchDatastore; + } else { + return datastore; + } + } public static void initialize() { // Initialize @@ -67,6 +80,28 @@ public final class DatabaseManager { datastore.ensureIndexes(); } } + + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + dispatchMongoClient = new MongoClient(new MongoClientURI(Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseUrl)); + dispatchDatastore = morphia.createDatastore(dispatchMongoClient, Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseCollection); + + // Ensure indexes for dispatch server + try { + dispatchDatastore.ensureIndexes(); + } catch (MongoCommandException e) { + Grasscutter.getLogger().info("Mongo index error: ", e); + // Duplicate index error + if (e.getCode() == 85) { + // Drop all indexes and re add them + MongoIterable collections = dispatchDatastore.getDatabase().listCollectionNames(); + for (String name : collections) { + dispatchDatastore.getDatabase().getCollection(name).dropIndexes(); + } + // Add back indexes + dispatchDatastore.ensureIndexes(); + } + } + } } public static synchronized int getNextId(Class c) { diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index dffbb0d86..dfb1e2829 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -1,5 +1,6 @@ package emu.grasscutter.game; +import dev.morphia.annotations.AlsoLoad; import dev.morphia.annotations.Collation; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Id; @@ -24,7 +25,7 @@ public class Account { private String username; private String password; // Unused for now - private int playerId; + @AlsoLoad("playerUid") private int playerId; private String email; private String token; @@ -68,7 +69,7 @@ public class Account { this.token = token; } - public int getPlayerId() { + public int getPlayerUid() { return this.playerId; } @@ -105,6 +106,10 @@ public class Account { if(this.permissions.contains(permission)) return false; this.permissions.add(permission); return true; } + + public boolean hasPermission(String permission) { + return this.permissions.contains(permission) || this.permissions.contains("*") ? true : false; + } public boolean removePermission(String permission) { return this.permissions.remove(permission); diff --git a/src/main/java/emu/grasscutter/game/GenshinPlayer.java b/src/main/java/emu/grasscutter/game/GenshinPlayer.java index 70fea04f0..44f960198 100644 --- a/src/main/java/emu/grasscutter/game/GenshinPlayer.java +++ b/src/main/java/emu/grasscutter/game/GenshinPlayer.java @@ -31,6 +31,7 @@ import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo; import emu.grasscutter.net.proto.PlayerApplyEnterMpReasonOuterClass.PlayerApplyEnterMpReason; import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; +import emu.grasscutter.net.proto.WorldPlayerLocationInfoOuterClass.WorldPlayerLocationInfo; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketAbilityInvocationsNotify; @@ -38,6 +39,7 @@ import emu.grasscutter.server.packet.send.PacketAvatarAddNotify; import emu.grasscutter.server.packet.send.PacketAvatarDataNotify; import emu.grasscutter.server.packet.send.PacketAvatarGainCostumeNotify; import emu.grasscutter.server.packet.send.PacketAvatarGainFlycloakNotify; +import emu.grasscutter.server.packet.send.PacketClientAbilityInitFinishNotify; import emu.grasscutter.server.packet.send.PacketCombatInvocationsNotify; import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; @@ -48,9 +50,11 @@ import emu.grasscutter.server.packet.send.PacketPlayerEnterSceneNotify; import emu.grasscutter.server.packet.send.PacketPlayerPropNotify; import emu.grasscutter.server.packet.send.PacketPlayerStoreNotify; import emu.grasscutter.server.packet.send.PacketPrivateChatNotify; +import emu.grasscutter.server.packet.send.PacketScenePlayerLocationNotify; import emu.grasscutter.server.packet.send.PacketSetNameCardRsp; import emu.grasscutter.server.packet.send.PacketStoreWeightLimitNotify; import emu.grasscutter.server.packet.send.PacketUnlockNameCardNotify; +import emu.grasscutter.server.packet.send.PacketWorldPlayerLocationNotify; import emu.grasscutter.server.packet.send.PacketWorldPlayerRTTNotify; import emu.grasscutter.utils.Position; @@ -100,10 +104,12 @@ public class GenshinPlayer { @Transient private int enterSceneToken; @Transient private SceneLoadState sceneState; @Transient private boolean hasSentAvatarDataNotify; + @Transient private long nextSendPlayerLocTime = 0; @Transient private final Int2ObjectMap coopRequests; @Transient private final InvokeHandler combatInvokeHandler; @Transient private final InvokeHandler abilityInvokeHandler; + @Transient private final InvokeHandler clientAbilityInitFinishHandler; @Deprecated @SuppressWarnings({ "rawtypes", "unchecked" }) // Morphia only! public GenshinPlayer() { @@ -119,6 +125,12 @@ public class GenshinPlayer { } this.properties.put(prop.getId(), 0); } + + this.gachaInfo = new PlayerGachaInfo(); + this.nameCardList = new HashSet<>(); + this.flyCloakList = new HashSet<>(); + this.costumeList = new HashSet<>(); + this.setSceneId(3); this.setRegionId(1); this.sceneState = SceneLoadState.NONE; @@ -126,6 +138,7 @@ public class GenshinPlayer { this.coopRequests = new Int2ObjectOpenHashMap<>(); this.combatInvokeHandler = new InvokeHandler(PacketCombatInvocationsNotify.class); this.abilityInvokeHandler = new InvokeHandler(PacketAbilityInvocationsNotify.class); + this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class); } // On player creation @@ -137,11 +150,6 @@ public class GenshinPlayer { this.nickname = "Traveler"; this.signature = ""; this.teamManager = new TeamManager(this); - this.gachaInfo = new PlayerGachaInfo(); - this.playerProfile = new PlayerProfile(this); - this.nameCardList = new HashSet<>(); - this.flyCloakList = new HashSet<>(); - this.costumeList = new HashSet<>(); this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1); this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); this.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 50); @@ -285,7 +293,7 @@ public class GenshinPlayer { } private float getExpModifier() { - return Grasscutter.getConfig().getGameRates().ADVENTURE_EXP_RATE; + return Grasscutter.getConfig().getGameServerOptions().getGameRates().ADVENTURE_EXP_RATE; } // Affected by exp rate @@ -344,7 +352,6 @@ public class GenshinPlayer { public PlayerProfile getProfile() { if (this.playerProfile == null) { this.playerProfile = new PlayerProfile(this); - this.save(); } return playerProfile; } @@ -389,6 +396,10 @@ public class GenshinPlayer { return this.abilityInvokeHandler; } + public InvokeHandler getClientAbilityInitFinishHandler() { + return clientAbilityInitFinishHandler; + } + public void setMpSetting(MpSettingType mpSetting) { this.mpSetting = mpSetting; } @@ -647,6 +658,13 @@ public class GenshinPlayer { return social; } + public WorldPlayerLocationInfo getWorldPlayerLocationInfo() { + return WorldPlayerLocationInfo.newBuilder() + .setSceneId(this.getSceneId()) + .setPlayerLoc(this.getPlayerLocationInfo()) + .build(); + } + public PlayerLocationInfo getPlayerLocationInfo() { return PlayerLocationInfo.newBuilder() .setUid(this.getUid()) @@ -672,9 +690,22 @@ public class GenshinPlayer { } // Ping if (this.getWorld() != null) { - this.sendPacket(new PacketWorldPlayerRTTNotify(this.getWorld())); // Player ping + // RTT notify - very important to send this often + this.sendPacket(new PacketWorldPlayerRTTNotify(this.getWorld())); + + // Update player locations if in multiplayer every 5 seconds + long time = System.currentTimeMillis(); + if (this.getWorld().isMultiplayer() && this.getScene() != null && time > nextSendPlayerLocTime) { + this.sendPacket(new PacketWorldPlayerLocationNotify(this.getWorld())); + this.sendPacket(new PacketScenePlayerLocationNotify(this.getScene())); + this.resetSendPlayerLocTime(); + } } } + + public void resetSendPlayerLocTime() { + this.nextSendPlayerLocTime = System.currentTimeMillis() + 5000; + } @PostLoad private void onLoad() { @@ -689,12 +720,8 @@ public class GenshinPlayer { // Make sure these exist if (this.getTeamManager() == null) { this.teamManager = new TeamManager(this); - } if (this.getGachaInfo() == null) { - this.gachaInfo = new PlayerGachaInfo(); - } if (this.nameCardList == null) { - this.nameCardList = new HashSet<>(); - } if (this.costumeList == null) { - this.costumeList = new HashSet<>(); + } if (this.getProfile().getUid() == 0) { + this.getProfile().syncWithCharacter(this); } // Check if player object exists in server diff --git a/src/main/java/emu/grasscutter/game/GenshinScene.java b/src/main/java/emu/grasscutter/game/GenshinScene.java index 668902d87..ea462fcdb 100644 --- a/src/main/java/emu/grasscutter/game/GenshinScene.java +++ b/src/main/java/emu/grasscutter/game/GenshinScene.java @@ -34,7 +34,8 @@ public class GenshinScene { private int time; private ClimateType climate; - + private int weather; + public GenshinScene(World world, SceneData sceneData) { this.world = world; this.sceneData = sceneData; @@ -89,10 +90,18 @@ public class GenshinScene { return climate; } + public int getWeather() { + return weather; + } + public void setClimate(ClimateType climate) { this.climate = climate; } + public void setWeather(int weather) { + this.weather = weather; + } + public boolean isInScene(GenshinEntity entity) { return this.entities.containsKey(entity.getId()); } diff --git a/src/main/java/emu/grasscutter/game/TeamInfo.java b/src/main/java/emu/grasscutter/game/TeamInfo.java index 0a12b1d2c..c60ce8cc0 100644 --- a/src/main/java/emu/grasscutter/game/TeamInfo.java +++ b/src/main/java/emu/grasscutter/game/TeamInfo.java @@ -13,7 +13,7 @@ public class TeamInfo { public TeamInfo() { this.name = ""; - this.avatars = new ArrayList<>(Grasscutter.getConfig().getServerOptions().MaxAvatarsInTeam); + this.avatars = new ArrayList<>(Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); } public String getName() { @@ -37,7 +37,7 @@ public class TeamInfo { } public boolean addAvatar(GenshinAvatar avatar) { - if (size() >= Grasscutter.getConfig().getServerOptions().MaxAvatarsInTeam || contains(avatar)) { + if (size() >= Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam || contains(avatar)) { return false; } @@ -57,7 +57,7 @@ public class TeamInfo { } public void copyFrom(TeamInfo team) { - copyFrom(team, Grasscutter.getConfig().getServerOptions().MaxAvatarsInTeam); + copyFrom(team, Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); } public void copyFrom(TeamInfo team, int maxTeamSize) { diff --git a/src/main/java/emu/grasscutter/game/TeamManager.java b/src/main/java/emu/grasscutter/game/TeamManager.java index 4cd7bfcdf..065c3bd28 100644 --- a/src/main/java/emu/grasscutter/game/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/TeamManager.java @@ -164,13 +164,13 @@ public class TeamManager { public int getMaxTeamSize() { if (getPlayer().isInMultiplayer()) { - int max = Grasscutter.getConfig().getServerOptions().MaxAvatarsInTeamMultiplayer; + int max = Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeamMultiplayer; if (getPlayer().getWorld().getHost() == this.getPlayer()) { return Math.max(1, (int) Math.ceil(max / (double) getWorld().getPlayerCount())); } return Math.max(1, (int) Math.floor(max / (double) getWorld().getPlayerCount())); } - return Grasscutter.getConfig().getServerOptions().MaxAvatarsInTeam; + return Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam; } // Methods diff --git a/src/main/java/emu/grasscutter/game/World.java b/src/main/java/emu/grasscutter/game/World.java index 02c0df63a..ffffb1e53 100644 --- a/src/main/java/emu/grasscutter/game/World.java +++ b/src/main/java/emu/grasscutter/game/World.java @@ -208,11 +208,14 @@ public class World implements Iterable { } public boolean transferPlayerToScene(GenshinPlayer player, int sceneId, Position pos) { - if (player.getScene().getId() == sceneId || GenshinData.getSceneDataMap().get(sceneId) == null) { + if (GenshinData.getSceneDataMap().get(sceneId) == null) { return false; } + Integer oldSceneId = null; + if (player.getScene() != null) { + oldSceneId = player.getScene().getId(); player.getScene().removePlayer(player); } @@ -221,7 +224,11 @@ public class World implements Iterable { player.getPos().set(pos); // Teleport packet - player.sendPacket(new PacketPlayerEnterSceneNotify(player, EnterType.EnterSelf, EnterReason.TransPoint, sceneId, pos)); + if (oldSceneId.equals(sceneId)) { + player.sendPacket(new PacketPlayerEnterSceneNotify(player, EnterType.EnterGoto, EnterReason.TransPoint, sceneId, pos)); + } else { + player.sendPacket(new PacketPlayerEnterSceneNotify(player, EnterType.EnterJump, EnterReason.TransPoint, sceneId, pos)); + } return true; } diff --git a/src/main/java/emu/grasscutter/game/avatar/GenshinAvatar.java b/src/main/java/emu/grasscutter/game/avatar/GenshinAvatar.java index a5dcb3ee8..6906d3896 100644 --- a/src/main/java/emu/grasscutter/game/avatar/GenshinAvatar.java +++ b/src/main/java/emu/grasscutter/game/avatar/GenshinAvatar.java @@ -1,7 +1,9 @@ package emu.grasscutter.game.avatar; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -13,6 +15,7 @@ import dev.morphia.annotations.Indexed; import dev.morphia.annotations.PostLoad; import dev.morphia.annotations.PrePersist; import dev.morphia.annotations.Transient; +import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GenshinData; import emu.grasscutter.data.common.FightPropData; import emu.grasscutter.data.custom.OpenConfigEntry; @@ -38,10 +41,13 @@ import emu.grasscutter.game.inventory.EquipType; import emu.grasscutter.game.inventory.GenshinItem; import emu.grasscutter.game.props.ElementType; import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.FetterState; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.net.proto.AvatarFetterInfoOuterClass.AvatarFetterInfo; +import emu.grasscutter.net.proto.FetterDataOuterClass.FetterData; import emu.grasscutter.net.proto.AvatarInfoOuterClass.AvatarInfo; +import emu.grasscutter.server.packet.send.PacketAbilityChangeNotify; import emu.grasscutter.server.packet.send.PacketAvatarEquipChangeNotify; import emu.grasscutter.server.packet.send.PacketAvatarFightPropNotify; import emu.grasscutter.utils.ProtoHelper; @@ -69,8 +75,10 @@ public class GenshinAvatar { @Transient private final Int2ObjectMap equips; @Transient private final Int2FloatOpenHashMap fightProp; - @Transient private final Set bonusAbilityList; + @Transient private Set extraAbilityEmbryos; + private List fetters; + private Map skillLevelMap; // Talent levels private Map proudSkillBonusMap; // Talent bonus levels (from const) private int skillDepotId; @@ -86,8 +94,9 @@ public class GenshinAvatar { // Morhpia only! this.equips = new Int2ObjectOpenHashMap<>(); this.fightProp = new Int2FloatOpenHashMap(); - this.bonusAbilityList = new HashSet<>(); - this.proudSkillBonusMap = new HashMap<>(); // TODO Move to genshin avatar + this.extraAbilityEmbryos = new HashSet<>(); + this.proudSkillBonusMap = new HashMap<>(); + this.fetters = new ArrayList<>(); // TODO Move to genshin avatar } // On creation @@ -260,8 +269,16 @@ public class GenshinAvatar { return proudSkillBonusMap; } - public Set getBonusAbilityList() { - return bonusAbilityList; + public Set getExtraAbilityEmbryos() { + return extraAbilityEmbryos; + } + + public void setFetterList(List fetterList) { + this.fetters = fetterList; + } + + public List getFetterList() { + return fetters; } public float getCurrentHp() { @@ -347,14 +364,14 @@ public class GenshinAvatar { item.setEquipCharacter(this.getAvatarId()); item.save(); + if (this.getPlayer().hasSentAvatarDataNotify()) { + this.getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(this, item)); + } + if (shouldRecalc) { this.recalcStats(); } - if (this.getPlayer().hasSentAvatarDataNotify()) { - this.getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(this, item)); - } - return true; } @@ -371,11 +388,21 @@ public class GenshinAvatar { } public void recalcStats() { + recalcStats(false); + } + + public void recalcStats(boolean forceSendAbilityChange) { // Setup AvatarData data = this.getAvatarData(); AvatarPromoteData promoteData = GenshinData.getAvatarPromoteData(data.getAvatarPromoteId(), this.getPromoteLevel()); Int2IntOpenHashMap setMap = new Int2IntOpenHashMap(); - this.getBonusAbilityList().clear(); + + // Extra ability embryos + Set prevExtraAbilityEmbryos = this.getExtraAbilityEmbryos(); + this.extraAbilityEmbryos = new HashSet<>(); + + // Fetters + this.setFetterList(data.getFetters()); // 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); @@ -458,7 +485,7 @@ public class GenshinAvatar { } // Add any skill strings from this affix - this.addToAbilityList(affix.getOpenConfig(), true); + this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true); } else { break; } @@ -505,7 +532,7 @@ public class GenshinAvatar { } // Add any skill strings from this affix - this.addToAbilityList(affix.getOpenConfig(), true); + this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true); } } } @@ -538,7 +565,7 @@ public class GenshinAvatar { } // Add any skill strings from this proud skill - this.addToAbilityList(proudSkillData.getOpenConfig(), true); + this.addToExtraAbilityEmbryos(proudSkillData.getOpenConfig(), true); } // Constellations @@ -550,7 +577,7 @@ public class GenshinAvatar { } // Add any skill strings from this constellation - this.addToAbilityList(avatarTalentData.getOpenConfig(), false); + this.addToExtraAbilityEmbryos(avatarTalentData.getOpenConfig(), false); } } @@ -573,11 +600,17 @@ public class GenshinAvatar { // Packet if (getPlayer() != null && getPlayer().hasSentAvatarDataNotify()) { + // Update stats for client getPlayer().sendPacket(new PacketAvatarFightPropNotify(this)); + // Update client abilities + EntityAvatar entity = this.getAsEntity(); + if (entity != null && (!this.getExtraAbilityEmbryos().equals(prevExtraAbilityEmbryos) || forceSendAbilityChange)) { + getPlayer().sendPacket(new PacketAbilityChangeNotify(entity)); + } } } - public void addToAbilityList(String openConfig, boolean forceAdd) { + public void addToExtraAbilityEmbryos(String openConfig, boolean forceAdd) { if (openConfig == null || openConfig.length() == 0) { return; } @@ -586,14 +619,14 @@ public class GenshinAvatar { if (entry == null) { if (forceAdd) { // Add config string to ability skill list anyways - this.getBonusAbilityList().add(openConfig); + this.getExtraAbilityEmbryos().add(openConfig); } return; } if (entry.getAddAbilities() != null) { for (String ability : entry.getAddAbilities()) { - this.getBonusAbilityList().add(ability); + this.getExtraAbilityEmbryos().add(ability); } } } @@ -668,6 +701,20 @@ public class GenshinAvatar { } public AvatarInfo toProto() { + AvatarFetterInfo.Builder avatarFetter = AvatarFetterInfo.newBuilder() + .setExpLevel(10) + .setExpNumber(6325); // Highest Level + + if (this.getFetterList() != null) { + for (int i = 0; i < this.getFetterList().size(); i++) { + avatarFetter.addFetterList( + FetterData.newBuilder() + .setFetterId(this.getFetterList().get(i)) + .setFetterState(FetterState.FINISH.getValue()) + ); + } + } + AvatarInfo.Builder avatarInfo = AvatarInfo.newBuilder() .setAvatarId(this.getAvatarId()) .setGuid(this.getGuid()) @@ -681,7 +728,7 @@ public class GenshinAvatar { .putAllProudSkillExtraLevel(getProudSkillBonusMap()) .setAvatarType(1) .setBornTime(this.getBornTime()) - .setFetterInfo(AvatarFetterInfo.newBuilder().setExpLevel(1)) + .setFetterInfo(avatarFetter) .setWearingFlycloakId(this.getFlyCloak()) .setCostumeId(this.getCostume()); diff --git a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java index 86e8ea458..233da5288 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java @@ -223,8 +223,8 @@ public class EntityAvatar extends GenshinEntity { } } // Add equip abilities - if (this.getAvatar().getBonusAbilityList().size() > 0) { - for (String skill : this.getAvatar().getBonusAbilityList()) { + if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) { + for (String skill : this.getAvatar().getExtraAbilityEmbryos()) { AbilityEmbryo emb = AbilityEmbryo.newBuilder() .setAbilityId(++embryoId) .setAbilityNameHash(Utils.abilityHash(skill)) diff --git a/src/main/java/emu/grasscutter/game/friends/FriendsList.java b/src/main/java/emu/grasscutter/game/friends/FriendsList.java index 582a10efd..45a11fb1c 100644 --- a/src/main/java/emu/grasscutter/game/friends/FriendsList.java +++ b/src/main/java/emu/grasscutter/game/friends/FriendsList.java @@ -220,7 +220,7 @@ public class FriendsList { friendship.setOwner(getPlayer()); // Check if friend is online - GenshinPlayer friend = getPlayer().getSession().getServer().getPlayerByUid(friendship.getFriendProfile().getId()); + GenshinPlayer friend = getPlayer().getSession().getServer().getPlayerByUid(friendship.getFriendProfile().getUid()); if (friend != null) { // Set friend to online mode friendship.setFriendProfile(friend); diff --git a/src/main/java/emu/grasscutter/game/friends/Friendship.java b/src/main/java/emu/grasscutter/game/friends/Friendship.java index 2be7d1fce..172d1bd43 100644 --- a/src/main/java/emu/grasscutter/game/friends/Friendship.java +++ b/src/main/java/emu/grasscutter/game/friends/Friendship.java @@ -88,7 +88,7 @@ public class Friendship { public FriendBrief toProto() { FriendBrief proto = FriendBrief.newBuilder() - .setUid(getFriendProfile().getId()) + .setUid(getFriendProfile().getUid()) .setNickname(getFriendProfile().getName()) .setLevel(getFriendProfile().getPlayerLevel()) .setAvatar(HeadImage.newBuilder().setAvatarId(getFriendProfile().getAvatarId())) diff --git a/src/main/java/emu/grasscutter/game/friends/PlayerProfile.java b/src/main/java/emu/grasscutter/game/friends/PlayerProfile.java index cc7408aa6..b9381c901 100644 --- a/src/main/java/emu/grasscutter/game/friends/PlayerProfile.java +++ b/src/main/java/emu/grasscutter/game/friends/PlayerProfile.java @@ -7,7 +7,7 @@ import emu.grasscutter.utils.Utils; public class PlayerProfile { @Transient private GenshinPlayer player; - private int id; + @AlsoLoad("id") private int uid; private int nameCard; private int avatarId; private String name; @@ -22,12 +22,12 @@ public class PlayerProfile { public PlayerProfile() { } public PlayerProfile(GenshinPlayer player) { - this.id = player.getUid(); + this.uid = player.getUid(); this.syncWithCharacter(player); } - public int getId() { - return id; + public int getUid() { + return uid; } public GenshinPlayer getPlayer() { @@ -87,6 +87,7 @@ public class PlayerProfile { return; } + this.uid = player.getUid(); this.name = player.getNickname(); this.avatarId = player.getHeadImage(); this.signature = player.getSignature(); diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 3e6bcdf7e..ed5492b62 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -92,7 +92,7 @@ public class GachaBanner { } public GachaInfo toProto() { - String record = "http://" + (Grasscutter.getConfig().DispatchServerPublicIp.isEmpty() ? Grasscutter.getConfig().DispatchServerIp : Grasscutter.getConfig().DispatchServerPublicIp) + "/gacha"; + String record = "http://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + "/gacha"; GachaInfo.Builder info = GachaInfo.newBuilder() .setGachaType(this.getGachaType()) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index 692e09264..079f0dda3 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -1,6 +1,8 @@ package emu.grasscutter.game.gacha; +import java.io.File; import java.io.FileReader; +import java.nio.file.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -8,6 +10,7 @@ import java.util.concurrent.ThreadLocalRandom; import com.google.gson.reflect.TypeToken; +import com.sun.nio.file.SensitivityWatchEventModifier; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GenshinData; import emu.grasscutter.data.def.ItemData; @@ -21,17 +24,20 @@ import emu.grasscutter.net.proto.GachaTransferItemOuterClass.GachaTransferItem; import emu.grasscutter.net.proto.GetGachaInfoRspOuterClass.GetGachaInfoRsp; import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.server.game.GameServerTickEvent; import emu.grasscutter.server.packet.send.PacketDoGachaRsp; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; +import org.greenrobot.eventbus.Subscribe; public class GachaManager { private final GameServer server; private final Int2ObjectMap gachaBanners; private GetGachaInfoRsp cachedProto; - + WatchService watchService; + private int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041}; private int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502}; private int[] purpleAvatars = new int[] {1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064}; @@ -40,11 +46,12 @@ public class GachaManager { private static int starglitterId = 221; private static int stardustId = 222; - + public GachaManager(GameServer server) { this.server = server; this.gachaBanners = new Int2ObjectOpenHashMap<>(); this.load(); + this.startWatcher(server); } public GameServer getServer() { @@ -65,9 +72,16 @@ public class GachaManager { public synchronized void load() { try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Banners.json")) { + getGachaBanners().clear(); List banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType()); - for (GachaBanner banner : banners) { - getGachaBanners().put(banner.getGachaType(), banner); + if(banners.size() > 0) { + for (GachaBanner banner : banners) { + getGachaBanners().put(banner.getGachaType(), banner); + } + Grasscutter.getLogger().info("Banners successfully loaded."); + this.cachedProto = createProto(); + } else { + Grasscutter.getLogger().error("Unable to load banners. Banners size is 0."); } } catch (Exception e) { // TODO Auto-generated catch block @@ -204,7 +218,6 @@ public class GachaManager { addStarglitter = 2; // Add 1 const gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null)); - gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(constItemId).setCount(1)); player.getInventory().addItem(constItemId, 1); } else { // Is max const @@ -266,6 +279,48 @@ public class GachaManager { // Packets player.sendPacket(new PacketDoGachaRsp(banner, list)); } + + private synchronized void startWatcher(GameServer server) { + if(this.watchService == null) { + try { + this.watchService = FileSystems.getDefault().newWatchService(); + Path path = new File(Grasscutter.getConfig().DATA_FOLDER).toPath(); + path.register(watchService, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY}, SensitivityWatchEventModifier.HIGH); + + server.OnGameServerTick.register(this); + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load the Gacha Manager Watch Service. If ServerOptions.watchGacha is true it will not auto-reload"); + e.printStackTrace(); + } + } else { + Grasscutter.getLogger().error("Cannot reinitialise watcher "); + } + } + + @Subscribe + public synchronized void watchBannerJson(GameServerTickEvent tickEvent) { + if(Grasscutter.getConfig().getGameServerOptions().WatchGacha) { + try { + WatchKey watchKey = watchService.take(); + + for (WatchEvent event : watchKey.pollEvents()) { + final Path changed = (Path) event.context(); + if (changed.endsWith("Banners.json")) { + Grasscutter.getLogger().info("Change detected with banners.json. Reloading gacha config"); + this.load(); + } + } + + boolean valid = watchKey.reset(); + if (!valid) { + Grasscutter.getLogger().error("Unable to reset Gacha Manager Watch Key. Auto-reload of banners.json will no longer work."); + return; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } private synchronized GetGachaInfoRsp createProto() { GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345); diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index 19bdcc525..8954b1031 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -37,10 +37,10 @@ public class Inventory implements Iterable { this.store = new Long2ObjectOpenHashMap<>(); this.inventoryTypes = new Int2ObjectOpenHashMap<>(); - this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitWeapon)); - this.createInventoryTab(ItemType.ITEM_RELIQUARY, new EquipInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitRelic)); - this.createInventoryTab(ItemType.ITEM_MATERIAL, new MaterialInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitMaterial)); - this.createInventoryTab(ItemType.ITEM_FURNITURE, new MaterialInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitFurniture)); + this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitWeapon)); + this.createInventoryTab(ItemType.ITEM_RELIQUARY, new EquipInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitRelic)); + this.createInventoryTab(ItemType.ITEM_MATERIAL, new MaterialInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitMaterial)); + this.createInventoryTab(ItemType.ITEM_FURNITURE, new MaterialInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitFurniture)); } public GenshinPlayer getPlayer() { diff --git a/src/main/java/emu/grasscutter/game/managers/ChatManager.java b/src/main/java/emu/grasscutter/game/managers/ChatManager.java index 40b1a55b5..d9867e875 100644 --- a/src/main/java/emu/grasscutter/game/managers/ChatManager.java +++ b/src/main/java/emu/grasscutter/game/managers/ChatManager.java @@ -1,6 +1,6 @@ package emu.grasscutter.game.managers; -import emu.grasscutter.commands.CommandMap; +import emu.grasscutter.command.CommandMap; import emu.grasscutter.game.GenshinPlayer; import emu.grasscutter.net.packet.GenshinPacket; import emu.grasscutter.server.game.GameServer; diff --git a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java index 3d56dfad2..532155db1 100644 --- a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java +++ b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java @@ -471,7 +471,7 @@ public class InventoryManager { } // Consume weapon - player.getInventory().removeItem(feed); + player.getInventory().removeItem(feed, 1); // Get weapon.setRefinement(targetRefineLevel); @@ -589,7 +589,6 @@ public class InventoryManager { // Update proud skills AvatarSkillDepotData skillDepot = GenshinData.getAvatarSkillDepotDataMap().get(avatar.getSkillDepotId()); - boolean hasAddedProudSkill = false; if (skillDepot != null && skillDepot.getInherentProudSkillOpens() != null) { for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) { @@ -599,7 +598,6 @@ public class InventoryManager { if (openData.getNeedAvatarPromoteLevel() == avatar.getPromoteLevel()) { int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1; if (GenshinData.getProudSkillDataMap().containsKey(proudSkillId)) { - hasAddedProudSkill = true; avatar.getProudSkillList().add(proudSkillId); player.sendPacket(new PacketProudSkillChangeNotify(avatar)); } @@ -607,20 +605,13 @@ public class InventoryManager { } } - // Racalc stats and save avatar - avatar.recalcStats(); - avatar.save(); - - // Resend ability embryos if proud skill has been added - if (hasAddedProudSkill && avatar.getAsEntity() != null) { - player.sendPacket(new PacketAbilityChangeNotify(avatar.getAsEntity())); - } - - // TODO Send entity prop update packet to world - // Packets player.sendPacket(new PacketAvatarPropNotify(avatar)); player.sendPacket(new PacketAvatarPromoteRsp(avatar)); + + // TODO Send entity prop update packet to world + avatar.recalcStats(true); + avatar.save(); } public void upgradeAvatar(GenshinPlayer player, long guid, int itemId, int count) { @@ -804,6 +795,12 @@ public class InventoryManager { // Get talent int currentTalentLevel = avatar.getCoreProudSkillLevel(); int nextTalentId = ((avatar.getAvatarId() % 10000000) * 10) + currentTalentLevel + 1; + + if (avatar.getAvatarId() == 10000006) { + // Lisa is special in that her talentId starts with 4 instead of 6. + nextTalentId = 40 + currentTalentLevel + 1; + } + AvatarTalentData talentData = GenshinData.getAvatarTalentDataMap().get(nextTalentId); if (talentData == null) { @@ -821,25 +818,20 @@ public class InventoryManager { // Apply + recalc avatar.getTalentIdList().add(talentData.getId()); avatar.setCoreProudSkillLevel(currentTalentLevel + 1); - avatar.recalcStats(); // Packet player.sendPacket(new PacketAvatarUnlockTalentNotify(avatar, nextTalentId)); player.sendPacket(new PacketUnlockAvatarTalentRsp(avatar, nextTalentId)); - // Proud skill bonus map + // Proud skill bonus map (Extra skills) OpenConfigEntry entry = GenshinData.getOpenConfigEntries().get(talentData.getOpenConfig()); if (entry != null && entry.getExtraTalentIndex() > 0) { avatar.recalcProudSkillBonusMap(); player.sendPacket(new PacketProudSkillExtraLevelNotify(avatar, entry.getExtraTalentIndex())); } - // Resend ability embryos - if (avatar.getAsEntity() != null) { - player.sendPacket(new PacketAbilityChangeNotify(avatar.getAsEntity())); - } - - // Save avatar + // Recalc + save avatar + avatar.recalcStats(true); avatar.save(); } diff --git a/src/main/java/emu/grasscutter/game/props/FetterState.java b/src/main/java/emu/grasscutter/game/props/FetterState.java new file mode 100644 index 000000000..346060e19 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/props/FetterState.java @@ -0,0 +1,42 @@ +package emu.grasscutter.game.props; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +public enum FetterState { + NONE(0), + NOT_OPEN(1), + OPEN(1), + FINISH(3); + + private final int value; + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + private static final Map stringMap = new HashMap<>(); + + static { + Stream.of(values()).forEach(e -> { + map.put(e.getValue(), e); + stringMap.put(e.name(), e); + }); + } + + private FetterState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static FetterState getTypeByValue(int value) { + return map.getOrDefault(value, NONE); + } + + public static FetterState getTypeByName(String name) { + return stringMap.getOrDefault(name, NONE); + } +} diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index da90caa42..ad8354867 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -1,30 +1,14 @@ package emu.grasscutter.server.dispatch; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLDecoder; -import java.security.KeyStore; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.protobuf.ByteString; import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsServer; +import emu.grasscutter.Config; import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; @@ -32,30 +16,34 @@ import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegio import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp; import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; -import emu.grasscutter.server.dispatch.json.ComboTokenReqJson; -import emu.grasscutter.server.dispatch.json.ComboTokenResJson; -import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; -import emu.grasscutter.server.dispatch.json.LoginResultJson; -import emu.grasscutter.server.dispatch.json.LoginTokenRequestJson; +import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.Utils; -import com.sun.net.httpserver.HttpServer; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import java.io.*; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.security.KeyStore; +import java.util.*; public final class DispatchServer { - private final InetSocketAddress address; - private final Gson gson; - private QueryCurrRegionHttpRsp currRegion; - - public String regionListBase64; - public String regionCurrentBase64; - public static String query_region_list = ""; public static String query_cur_region = ""; + + private final InetSocketAddress address; + private final Gson gson; + private final String defaultServerName = "os_usa"; + public String regionListBase64; + public HashMap regions; + public DispatchServer() { - this.address = new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, Grasscutter.getConfig().DispatchServerPort); + this.regions = new HashMap(); + this.address = new InetSocketAddress(Grasscutter.getConfig().getDispatchOptions().Ip, Grasscutter.getConfig().getDispatchOptions().Port); this.gson = new GsonBuilder().create(); this.loadQueries(); @@ -71,7 +59,13 @@ public final class DispatchServer { } public QueryCurrRegionHttpRsp getCurrRegion() { - return currRegion; + // Needs to be fixed by having the game servers connect to the dispatch server. + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) { + return regions.get(defaultServerName).parsedRegionQuery; + } + + Grasscutter.getLogger().warn("[Dispatch] Unsupported run mode for getCurrRegion()"); + return null; } public void loadQueries() { @@ -81,14 +75,14 @@ public final class DispatchServer { if (file.exists()) { query_region_list = new String(FileUtils.read(file)); } else { - Grasscutter.getLogger().warn("query_region_list not found! Using default region list."); + Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list."); } file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt"); if (file.exists()) { query_cur_region = new String(FileUtils.read(file)); } else { - Grasscutter.getLogger().warn("query_cur_region not found! Using default current region."); + Grasscutter.getLogger().warn("[Dispatch] query_cur_region not found! Using default current region."); } } @@ -99,53 +93,80 @@ public final class DispatchServer { byte[] decoded2 = Base64.getDecoder().decode(query_cur_region); QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2); - - RegionSimpleInfo server = RegionSimpleInfo.newBuilder() - .setName("os_usa") - .setTitle(Grasscutter.getConfig().GameServerName) - .setType("DEV_PUBLIC") - .setDispatchUrl("https://" + (Grasscutter.getConfig().DispatchServerPublicIp.isEmpty() ? Grasscutter.getConfig().DispatchServerIp : Grasscutter.getConfig().DispatchServerPublicIp) + ":" + getAddress().getPort() + "/query_cur_region") - .build(); - - RegionSimpleInfo serverTest2 = RegionSimpleInfo.newBuilder() - .setName("os_euro") - .setTitle("Grasscutter") - .setType("DEV_PUBLIC") - .setDispatchUrl("https://" + (Grasscutter.getConfig().DispatchServerPublicIp.isEmpty() ? Grasscutter.getConfig().DispatchServerIp : Grasscutter.getConfig().DispatchServerPublicIp) + ":" + getAddress().getPort() + "/query_cur_region") - .build(); - + + List servers = new ArrayList(); + List usedNames = new ArrayList(); // List to check for potential naming conflicts + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) { // Automatically add the game server if in hybrid mode + RegionSimpleInfo server = RegionSimpleInfo.newBuilder() + .setName("os_usa") + .setTitle(Grasscutter.getConfig().getGameServerOptions().Name) + .setType("DEV_PUBLIC") + .setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region_" + defaultServerName) + .build(); + usedNames.add(defaultServerName); + servers.add(server); + + RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() + .setIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getGameServerOptions().Ip : Grasscutter.getConfig().getGameServerOptions().PublicIp)) + .setPort(Grasscutter.getConfig().getGameServerOptions().Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .build(); + + QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); + regions.put(defaultServerName, new RegionData(parsedRegionQuery, Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()))); + + } else { + if(Grasscutter.getConfig().getDispatchOptions().getGameServers().length == 0) { + Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state."); + System.exit(1); + } + } + + for (Config.DispatchServerOptions.RegionInfo regionInfo : Grasscutter.getConfig().getDispatchOptions().getGameServers()) { + if(usedNames.contains(regionInfo.Name)) { + Grasscutter.getLogger().error("Region name already in use."); + continue; + } + RegionSimpleInfo server = RegionSimpleInfo.newBuilder() + .setName(regionInfo.Name) + .setTitle(regionInfo.Title) + .setType("DEV_PUBLIC") + .setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region_" + regionInfo.Name) + .build(); + usedNames.add(regionInfo.Name); + servers.add(server); + + RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() + .setIp(regionInfo.Ip) + .setPort(regionInfo.Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .build(); + + QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); + regions.put(regionInfo.Name, new RegionData(parsedRegionQuery, Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()))); + } + QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder() - .addServers(server) - .addServers(serverTest2) + .addAllServers(servers) .setClientSecretKey(rl.getClientSecretKey()) .setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted()) .setEnableLoginPc(true) .build(); - - RegionInfo currentRegion = regionQuery.getRegionInfo().toBuilder() - .setIp((Grasscutter.getConfig().GameServerPublicIp.isEmpty() ? Grasscutter.getConfig().GameServerIp : Grasscutter.getConfig().GameServerPublicIp)) - .setPort(Grasscutter.getConfig().GameServerPort) - .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) - .build(); - - QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(currentRegion).build(); this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray()); - this.regionCurrentBase64 = Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()); - this.currRegion = parsedRegionQuery; } catch (Exception e) { - Grasscutter.getLogger().error("Error while initializing region info!", e); + Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", e); } } public void start() throws Exception { HttpServer server; - if(Grasscutter.getConfig().UseSSL) { + if (Grasscutter.getConfig().getDispatchOptions().UseSSL) { HttpsServer httpsServer; httpsServer = HttpsServer.create(getAddress(), 0); SSLContext sslContext = SSLContext.getInstance("TLS"); - try (FileInputStream fis = new FileInputStream(Grasscutter.getConfig().DispatchServerKeystorePath)) { - char[] keystorePassword = Grasscutter.getConfig().DispatchServerKeystorePassword.toCharArray(); + try (FileInputStream fis = new FileInputStream(Grasscutter.getConfig().getDispatchOptions().KeystorePath)) { + char[] keystorePassword = Grasscutter.getConfig().getDispatchOptions().KeystorePassword.toCharArray(); KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(fis, keystorePassword); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); @@ -156,56 +177,39 @@ public final class DispatchServer { httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext)); server = httpsServer; } catch (Exception e) { - Grasscutter.getLogger().error("No SSL cert found!"); - return; + Grasscutter.getLogger().warn("[Dispatch] No SSL cert found! Falling back to HTTP server."); + Grasscutter.getConfig().getDispatchOptions().UseSSL = false; + server = HttpServer.create(getAddress(), 0); } } else { server = HttpServer.create(getAddress(), 0); } - - server.createContext("/", t -> { - //Create a response form the request query parameters - String response = "Hello"; - //Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); - t.sendResponseHeaders(200, response.getBytes().length); - //Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - }); + + server.createContext("/", t -> responseHTML(t, "Hello")); // Dispatch server.createContext("/query_region_list", t -> { // Log - Grasscutter.getLogger().info("Client request: query_region_list"); - // Create a response form the request query parameters - String response = regionListBase64; - // Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); - t.sendResponseHeaders(200, response.getBytes().length); - // Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - }); - server.createContext("/query_cur_region", t -> { - // Log - Grasscutter.getLogger().info("Client request: query_cur_region"); - // Create a response form the request query parameters - URI uri = t.getRequestURI(); - String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (uri.getQuery() != null && uri.getQuery().length() > 0) { - response = regionCurrentBase64; - } - // Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); - t.sendResponseHeaders(200, response.getBytes().length); - // Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", t.getRemoteAddress())); + + responseHTML(t, regionListBase64); }); + + for (String regionName : regions.keySet()) { + server.createContext("/query_cur_region_" + regionName, t -> { + String regionCurrentBase64 = regions.get(regionName).Base64; + // Log + Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region_%s", t.getRemoteAddress(), regionName)); + // Create a response form the request query parameters + URI uri = t.getRequestURI(); + String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; + if (uri.getQuery() != null && uri.getQuery().length() > 0) { + response = regionCurrentBase64; + } + responseHTML(t, response); + }); + } + // Login via account server.createContext("/hk4e_global/mdk/shield/api/login", t -> { // Get post data @@ -213,38 +217,56 @@ public final class DispatchServer { try { String body = Utils.toString(t.getRequestBody()); requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class); - } catch (Exception e) { - - } + } catch (Exception ignored) { } + // Create response json if (requestData == null) { return; } LoginResultJson responseData = new LoginResultJson(); + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in", t.getRemoteAddress())); // Login Account account = DatabaseHelper.getAccountByName(requestData.account); - // Test + // Check if account exists, else create a new one. if (account == null) { - responseData.retcode = -201; - responseData.message = "Username not found."; + // Account doesnt exist, so we can either auto create it if the config value is set + if (Grasscutter.getConfig().getDispatchOptions().AutomaticallyCreateAccounts) { + // This account has been created AUTOMATICALLY. There will be no permissions added. + account = DatabaseHelper.createAccountWithId(requestData.account, 0); + + if (account != null) { + responseData.message = "OK"; + responseData.data.account.uid = account.getId(); + responseData.data.account.token = account.generateSessionKey(); + responseData.data.account.email = account.getEmail(); + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in: Account %s created", t.getRemoteAddress(), responseData.data.account.uid)); + } else { + responseData.retcode = -201; + responseData.message = "Username not found, create failed."; + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in: Account create failed", t.getRemoteAddress())); + } + } else { + responseData.retcode = -201; + responseData.message = "Username not found."; + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in: Account no found", t.getRemoteAddress())); + } } else { + // Account was found, log the player in responseData.message = "OK"; responseData.data.account.uid = account.getId(); responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in as %s", t.getRemoteAddress(), responseData.data.account.uid)); } - - // Create a response - String response = getGsonFactory().toJson(responseData); - // Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json")); - t.sendResponseHeaders(200, response.getBytes().length); - // Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); + + responseJSON(t, responseData); }); // Login via token server.createContext("/hk4e_global/mdk/shield/api/verify", t -> { @@ -253,14 +275,14 @@ public final class DispatchServer { try { String body = Utils.toString(t.getRequestBody()); requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class); - } catch (Exception e) { - - } + } catch (Exception ignored) { } + // Create response json if (requestData == null) { return; } LoginResultJson responseData = new LoginResultJson(); + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in via token", t.getRemoteAddress())); // Login Account account = DatabaseHelper.getAccountById(requestData.uid); @@ -269,22 +291,18 @@ public final class DispatchServer { if (account == null || !account.getSessionKey().equals(requestData.token)) { responseData.retcode = -111; responseData.message = "Game account cache information error"; + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in via token", t.getRemoteAddress())); } else { responseData.message = "OK"; responseData.data.account.uid = requestData.uid; responseData.data.account.token = requestData.token; responseData.data.account.email = account.getEmail(); + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in via token as %s", t.getRemoteAddress(), responseData.data.account.uid)); } - - // Create a response - String response = getGsonFactory().toJson(responseData); - // Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json")); - t.sendResponseHeaders(200, response.getBytes().length); - // Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); + + responseJSON(t, responseData); }); // Exchange for combo token server.createContext("/hk4e_global/combo/granter/login/v2/login", t -> { @@ -293,9 +311,8 @@ public final class DispatchServer { try { String body = Utils.toString(t.getRequestBody()); requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class); - } catch (Exception e) { - - } + } catch (Exception ignored) { } + // Create response json if (requestData == null || requestData.data == null) { return; @@ -310,22 +327,18 @@ public final class DispatchServer { if (account == null || !account.getSessionKey().equals(loginData.token)) { responseData.retcode = -201; responseData.message = "Wrong session key."; + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to exchange combo token", t.getRemoteAddress())); } else { responseData.message = "OK"; responseData.data.open_id = loginData.uid; responseData.data.combo_id = "157795300"; responseData.data.combo_token = account.generateLoginToken(); + + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s succeed to exchange combo token", t.getRemoteAddress())); } - - // Create a response - String response = getGsonFactory().toJson(responseData); - // Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json")); - t.sendResponseHeaders(200, response.getBytes().length); - // Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); + + responseJSON(t, responseData); }); // Agreement and Protocol server.createContext( // hk4e-sdk-os.hoyoverse.com @@ -389,63 +402,88 @@ public final class DispatchServer { "/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}") ); - // Start server - server.start(); - Grasscutter.getLogger().info("Dispatch server started on port " + getAddress().getPort()); + server.createContext( // /perf/config/verify?device_id=xxx&platform=x&name=xxx + "/perf/config/verify", + new DispatchHttpJsonHandler("{\"code\":0}") + ); // Logging servers - HttpServer overseaLogServer = HttpServer.create(new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, 8888), 0); - overseaLogServer.createContext( // overseauspider.yuanshen.com - "/log", + server.createContext( // overseauspider.yuanshen.com + "/log", new DispatchHttpJsonHandler("{\"code\":0}") ); - overseaLogServer.start(); - Grasscutter.getLogger().info("Log server (overseauspider) started on port " + 8888); - - HttpServer uploadLogServer = HttpServer.create(new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, Grasscutter.getConfig().UploadLogPort), 0); - uploadLogServer.createContext( // log-upload-os.mihoyo.com - "/crash/dataUpload", + + server.createContext( // log-upload-os.mihoyo.com + "/crash/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}") ); - uploadLogServer.createContext("/gacha", t -> { - //Create a response form the request query parameters - String response = "Gacha"; - //Set the response header status and length - t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); - t.sendResponseHeaders(200, response.getBytes().length); - //Write the response string - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - }); - uploadLogServer.start(); - Grasscutter.getLogger().info("Log server (log-upload-os) started on port " + Grasscutter.getConfig().UploadLogPort); + server.createContext("/gacha", t -> responseHTML(t, "Gacha")); + + // Start server + server.start(); + Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + getAddress().getPort()); + } + + private void responseJSON(HttpExchange t, Object data) throws IOException { + // Create a response + String response = getGsonFactory().toJson(data); + // Set the response header status and length + t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json")); + t.sendResponseHeaders(200, response.getBytes().length); + // Write the response string + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + + private void responseHTML(HttpExchange t, String response) throws IOException { + // Set the response header status and length + t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8")); + t.sendResponseHeaders(200, response.getBytes().length); + //Write the response string + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); } private Map parseQueryString(String qs) { - Map result = new HashMap<>(); - if (qs == null) - return result; + Map result = new HashMap<>(); + if (qs == null) { + return result; + } - int last = 0, next, l = qs.length(); - while (last < l) { - next = qs.indexOf('&', last); - if (next == -1) - next = l; + int last = 0, next, l = qs.length(); + while (last < l) { + next = qs.indexOf('&', last); + if (next == -1) { + next = l; + } - if (next > last) { - int eqPos = qs.indexOf('=', last); - try { - if (eqPos < 0 || eqPos > next) - result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), ""); - else - result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"), URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java - } - } - last = next + 1; - } - return result; + if (next > last) { + int eqPos = qs.indexOf('=', last); + try { + if (eqPos < 0 || eqPos > next) { + result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), ""); + } else { + result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"), URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8")); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java + } + } + last = next + 1; + } + return result; + } + + public static class RegionData { + + QueryCurrRegionHttpRsp parsedRegionQuery; + String Base64; + + public RegionData(QueryCurrRegionHttpRsp prq, String b64) { + this.parsedRegionQuery = prq; + this.Base64 = b64; + } } } diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java index dac26cfa4..b3497f8d4 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java +++ b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java @@ -7,7 +7,7 @@ public class ComboTokenReqJson { public String device; public String sign; - public class LoginTokenData { + public static class LoginTokenData { public String uid; public String token; public boolean guest; diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java index 731d50853..7c49d1278 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java +++ b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java @@ -5,7 +5,7 @@ public class ComboTokenResJson { public int retcode; public LoginData data = new LoginData(); - public class LoginData { + public static class LoginData { public int account_type = 1; public boolean heartbeat; public String combo_id; diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java index 5988752d8..88e142d4f 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java +++ b/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java @@ -5,7 +5,7 @@ public class LoginResultJson { public int retcode; public VerifyData data = new VerifyData(); - public class VerifyData { + public static class VerifyData { public VerifyAccountData account = new VerifyAccountData(); public boolean device_grant_required = false; public String realname_operation = "NONE"; @@ -13,7 +13,7 @@ public class LoginResultJson { public boolean safe_mobile_required = false; } - public class VerifyAccountData { + public static class VerifyAccountData { public String uid; public String name = ""; public String email; diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index b42ced55c..065344ee2 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentHashMap; import emu.grasscutter.GenshinConstants; import emu.grasscutter.Grasscutter; -import emu.grasscutter.commands.CommandMap; +import emu.grasscutter.command.CommandMap; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.game.GenshinPlayer; @@ -19,6 +19,7 @@ import emu.grasscutter.game.shop.ShopManager; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.netty.MihoyoKcpServer; +import org.greenrobot.eventbus.EventBus; public final class GameServer extends MihoyoKcpServer { private final InetSocketAddress address; @@ -33,10 +34,18 @@ public final class GameServer extends MihoyoKcpServer { private final MultiplayerManager multiplayerManager; private final DungeonManager dungeonManager; private final CommandMap commandMap; + + public EventBus OnGameServerStartFinish; + public EventBus OnGameServerTick; + public EventBus OnGameServerStop; public GameServer(InetSocketAddress address) { super(address); - + + OnGameServerStartFinish = EventBus.builder().throwSubscriberException(true).logNoSubscriberMessages(false).build(); + OnGameServerTick = EventBus.builder().throwSubscriberException(true).logNoSubscriberMessages(false).build(); + OnGameServerStop = EventBus.builder().throwSubscriberException(true).logNoSubscriberMessages(false).build(); + this.setServerInitializer(new GameServerInitializer(this)); this.address = address; this.packetHandler = new GameServerPacketHandler(PacketHandler.class); @@ -145,7 +154,7 @@ public final class GameServer extends MihoyoKcpServer { public Account getAccountByName(String username) { Optional playerOpt = getPlayers().values().stream().filter(player -> player.getAccount().getUsername().equals(username)).findFirst(); - if (playerOpt.get() != null) { + if (playerOpt.isPresent()) { return playerOpt.get().getAccount(); } return DatabaseHelper.getAccountByName(username); @@ -155,14 +164,20 @@ public final class GameServer extends MihoyoKcpServer { for (GenshinPlayer player : this.getPlayers().values()) { player.onTick(); } + + OnGameServerTick.post(new GameServerTickEvent()); } @Override public void onStartFinish() { Grasscutter.getLogger().info("Game Server started on port " + address.getPort()); + + OnGameServerStartFinish.post(new GameServerStartFinishEvent()); } public void onServerShutdown() { + OnGameServerStop.post(new GameServerStopEvent()); + // Kick and save all players List list = new ArrayList<>(this.getPlayers().size()); list.addAll(this.getPlayers().values()); diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index 118637668..62a57df91 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -87,7 +87,7 @@ public class GameServerPacketHandler { } // Log unhandled packets - if (Grasscutter.getConfig().LOG_PACKETS) { + if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) { //Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + PacketOpcodesUtil.getOpcodeName(opcode)); } } diff --git a/src/main/java/emu/grasscutter/server/game/GameServerStartFinishEvent.java b/src/main/java/emu/grasscutter/server/game/GameServerStartFinishEvent.java new file mode 100644 index 000000000..5e960ad1d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/game/GameServerStartFinishEvent.java @@ -0,0 +1,5 @@ +package emu.grasscutter.server.game; + +public class GameServerStartFinishEvent { + // Placeholder class for now, probably will get used later +} diff --git a/src/main/java/emu/grasscutter/server/game/GameServerStopEvent.java b/src/main/java/emu/grasscutter/server/game/GameServerStopEvent.java new file mode 100644 index 000000000..7a7ef40d7 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/game/GameServerStopEvent.java @@ -0,0 +1,5 @@ +package emu.grasscutter.server.game; + +public class GameServerStopEvent { + // Placeholder class for now, probably will get used later +} diff --git a/src/main/java/emu/grasscutter/server/game/GameServerTickEvent.java b/src/main/java/emu/grasscutter/server/game/GameServerTickEvent.java new file mode 100644 index 000000000..f9069d2d9 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/game/GameServerTickEvent.java @@ -0,0 +1,5 @@ +package emu.grasscutter.server.game; + +public class GameServerTickEvent { + // Placeholder class for now, probably will get used later +} diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index 1a54f31ba..ebd66dc20 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -165,7 +165,7 @@ public class GameSession extends MihoyoKcpChannel { byte[] data = genshinPacket.build(); // Log - if (Grasscutter.getConfig().LOG_PACKETS) { + if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) { logPacket(genshinPacket); } @@ -225,7 +225,7 @@ public class GameSession extends MihoyoKcpChannel { } // Log packet - if (Grasscutter.getConfig().LOG_PACKETS) { + if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) { Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")"); System.out.println(Utils.bytesToHex(payload)); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java new file mode 100644 index 000000000..cfe697b91 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; +import emu.grasscutter.net.proto.ClientAbilityInitFinishNotifyOuterClass.ClientAbilityInitFinishNotify; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.ClientAbilityInitFinishNotify) +public class HandlerClientAbilityInitFinishNotify extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + ClientAbilityInitFinishNotify notif = ClientAbilityInitFinishNotify.parseFrom(payload); + + for (AbilityInvokeEntry entry : notif.getInvokesList()) { + session.getPlayer().getClientAbilityInitFinishHandler().addEntry(entry.getForwardType(), entry); + } + + if (notif.getInvokesList().size() > 0) { + session.getPlayer().getClientAbilityInitFinishHandler().update(session.getPlayer()); + } + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterSceneDoneReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterSceneDoneReq.java index 5b56a39f4..2f7da6884 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterSceneDoneReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterSceneDoneReq.java @@ -31,8 +31,11 @@ public class HandlerEnterSceneDoneReq extends PacketHandler { // Locations session.send(new PacketWorldPlayerLocationNotify(session.getPlayer().getWorld())); - session.send(new PacketScenePlayerLocationNotify(session.getPlayer())); + session.send(new PacketScenePlayerLocationNotify(session.getPlayer().getScene())); session.send(new PacketWorldPlayerRTTNotify(session.getPlayer().getWorld())); + + // Reset timer for sending player locations + session.getPlayer().resetSendPlayerLocTime(); } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java index dc686f5c1..5034fb01a 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java @@ -35,15 +35,15 @@ public class HandlerGetPlayerTokenReq extends PacketHandler { // Has character boolean doesPlayerExist = false; - if (account.getPlayerId() > 0) { + if (account.getPlayerUid() > 0) { // Set flag for player existing - doesPlayerExist = DatabaseHelper.checkPlayerExists(account.getPlayerId()); + doesPlayerExist = DatabaseHelper.checkPlayerExists(account.getPlayerUid()); } // Set reserve player id if account doesnt exist if (!doesPlayerExist) { - int id = DatabaseHelper.getNextPlayerId(session.getAccount().getPlayerId()); - if (id != session.getAccount().getPlayerId()) { + int id = DatabaseHelper.getNextPlayerId(session.getAccount().getPlayerUid()); + if (id != session.getAccount().getPlayerUid()) { session.getAccount().setPlayerId(id); session.getAccount().save(); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java new file mode 100644 index 000000000..588e1c4e1 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.inventory.GenshinItem; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.NpcTalkReqOuterClass.NpcTalkReq; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketNpcTalkRsp; + +@Opcodes(PacketOpcodes.NpcTalkReq) +public class HandlerNpcTalkReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + NpcTalkReq req = NpcTalkReq.parseFrom(payload); + + session.send(new PacketNpcTalkRsp(req.getNpcEntityId(), req.getTalkId(), req.getEntityId())); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerPlayerLoginReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerPlayerLoginReq.java index 183ef7461..05753aec0 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerPlayerLoginReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerPlayerLoginReq.java @@ -30,7 +30,7 @@ public class HandlerPlayerLoginReq extends PacketHandler { } // Load character from db - GenshinPlayer player = DatabaseHelper.getPlayerById(session.getAccount().getPlayerId()); + GenshinPlayer player = DatabaseHelper.getPlayerById(session.getAccount().getPlayerUid()); if (player == null) { // Send packets diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSceneTransToPointReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSceneTransToPointReq.java new file mode 100644 index 000000000..f01f5980d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSceneTransToPointReq.java @@ -0,0 +1,36 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.custom.ScenePointEntry; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.SceneTransToPointReqOuterClass.SceneTransToPointReq; +import emu.grasscutter.net.proto.SceneTransToPointRspOuterClass.SceneTransToPointRsp; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketSceneTransToPointRsp; +import emu.grasscutter.utils.Position; + +@Opcodes(PacketOpcodes.SceneTransToPointReq) +public class HandlerSceneTransToPointReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + SceneTransToPointReq req = SceneTransToPointReq.parseFrom(payload); + + String code = req.getSceneId() + "_" + req.getPointId(); + ScenePointEntry scenePointEntry = GenshinData.getScenePointEntries().get(code); + + if (scenePointEntry != null) { + float x = scenePointEntry.getPointData().getTranPos().getX(); + float y = scenePointEntry.getPointData().getTranPos().getY(); + float z = scenePointEntry.getPointData().getTranPos().getZ(); + + session.getPlayer().getWorld().transferPlayerToScene(session.getPlayer(), req.getSceneId(), new Position(x, y, z)); + session.send(new PacketSceneTransToPointRsp(session.getPlayer(), req.getPointId(), req.getSceneId())); + } else { + session.send(new PacketSceneTransToPointRsp()); + } + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetEntityClientDataNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetEntityClientDataNotify.java index accbd253d..d194e1465 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetEntityClientDataNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetEntityClientDataNotify.java @@ -1,7 +1,9 @@ package emu.grasscutter.server.packet.recv; +import emu.grasscutter.net.packet.GenshinPacket; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.SetEntityClientDataNotifyOuterClass.SetEntityClientDataNotify; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.server.game.GameSession; @@ -10,7 +12,18 @@ public class HandlerSetEntityClientDataNotify extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - // Auto template + // Skip if there is no one to broadcast it too + if (session.getPlayer().getScene().getPlayerCount() <= 1) { + return; + } + + // Make sure packet is a valid proto before replaying it to the other players + SetEntityClientDataNotify notif = SetEntityClientDataNotify.parseFrom(payload); + + GenshinPacket packet = new GenshinPacket(PacketOpcodes.SetEntityClientDataNotify, true); + packet.setData(notif); + + session.getPlayer().getScene().broadcastPacketToOthers(session.getPlayer(), packet); } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java index 5cbbd8d04..72fc709d9 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java @@ -43,7 +43,7 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler { try { // Save to db - DatabaseHelper.createPlayer(player, session.getAccount().getPlayerId()); + DatabaseHelper.createPlayer(player, session.getAccount().getPlayerUid()); // Create avatar if (player.getAvatars().getAvatarCount() == 0) { diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAbilityChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAbilityChangeNotify.java index a6e1fb452..d8cdfdef8 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketAbilityChangeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAbilityChangeNotify.java @@ -8,8 +8,8 @@ import emu.grasscutter.net.proto.AbilityChangeNotifyOuterClass.AbilityChangeNoti public class PacketAbilityChangeNotify extends GenshinPacket { public PacketAbilityChangeNotify(EntityAvatar entity) { - super(PacketOpcodes.AbilityChangeNotify); - + super(PacketOpcodes.AbilityChangeNotify, true); + AbilityChangeNotify proto = AbilityChangeNotify.newBuilder() .setEntityId(entity.getId()) .setAbilityControlBlock(entity.getAbilityControlBlock()) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketClientAbilityInitFinishNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketClientAbilityInitFinishNotify.java new file mode 100644 index 000000000..dc676c4d1 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketClientAbilityInitFinishNotify.java @@ -0,0 +1,29 @@ +package emu.grasscutter.server.packet.send; + +import java.util.List; + +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; +import emu.grasscutter.net.proto.ClientAbilityInitFinishNotifyOuterClass.ClientAbilityInitFinishNotify; + +public class PacketClientAbilityInitFinishNotify extends GenshinPacket { + + public PacketClientAbilityInitFinishNotify(List entries) { + super(PacketOpcodes.ClientAbilityInitFinishNotify, true); + + int entityId = 0; + + if (entries.size() > 0) { + AbilityInvokeEntry entry = entries.get(0); + entityId = entry.getEntityId(); + } + + ClientAbilityInitFinishNotify proto = ClientAbilityInitFinishNotify.newBuilder() + .setEntityId(entityId) + .addAllInvokes(entries) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java index 9df89af5f..4d22f9603 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java @@ -23,7 +23,6 @@ public class PacketGetPlayerFriendListRsp extends GenshinPacket { .setWorldLevel(0) .setSignature("") .setLastActiveTime((int) (System.currentTimeMillis() / 1000f)) - .setIsMpModeAvailable(true) .setNameCardId(210001) .setOnlineState(FriendOnlineState.FRIEND_ONLINE) .setParam(1) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java index 2306aa0ae..00f512ea7 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java @@ -16,7 +16,7 @@ public class PacketGetPlayerTokenRsp extends GenshinPacket { this.setUseDispatchKey(true); GetPlayerTokenRsp p = GetPlayerTokenRsp.newBuilder() - .setPlayerUid(session.getAccount().getPlayerId()) + .setPlayerUid(session.getAccount().getPlayerUid()) .setAccountToken(session.getAccount().getToken()) .setAccountType(1) .setIsProficientPlayer(doesPlayerExist) // Not sure where this goes diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketNpcTalkRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketNpcTalkRsp.java new file mode 100644 index 000000000..b447a19eb --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketNpcTalkRsp.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.NpcTalkRspOuterClass.NpcTalkRsp; + +public class PacketNpcTalkRsp extends GenshinPacket { + public PacketNpcTalkRsp(int npcEntityId, int curTalkId, int entityId) { + super(PacketOpcodes.NpcTalkRsp); + + NpcTalkRsp p = NpcTalkRsp.newBuilder() + .setNpcEntityId(npcEntityId) + .setCurTalkId(curTalkId) + .setEntityId(entityId) + .build(); + + this.setData(p); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterSceneNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterSceneNotify.java index 69ca9cb5f..d66575273 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterSceneNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterSceneNotify.java @@ -52,13 +52,14 @@ public class PacketPlayerEnterSceneNotify extends GenshinPacket { .setSceneId(newScene) .setPos(newPos.toProto()) .setSceneBeginTime(System.currentTimeMillis()) - .setType(EnterType.EnterSelf) + .setType(type) .setTargetUid(target.getUid()) .setEnterSceneToken(player.getEnterSceneToken()) .setWorldLevel(target.getWorld().getWorldLevel()) .setEnterReason(reason.getValue()) .addSceneTagIdList(102) .addSceneTagIdList(107) + .addSceneTagIdList(109) .addSceneTagIdList(113) .addSceneTagIdList(117) .setUnk1(1) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java index 0d0871ca9..a5e167100 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -1,21 +1,61 @@ package emu.grasscutter.server.packet.send; +import com.google.protobuf.ByteString; import emu.grasscutter.Grasscutter; import emu.grasscutter.net.packet.GenshinPacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.PlayerLoginRspOuterClass.PlayerLoginRsp; +import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass; import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.utils.FileUtils; + +import java.io.File; +import java.net.URL; +import java.util.Base64; public class PacketPlayerLoginRsp extends GenshinPacket { + private static QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp regionCache; + public PacketPlayerLoginRsp(GameSession session) { super(PacketOpcodes.PlayerLoginRsp, 1); this.setUseDispatchKey(true); - - RegionInfo info = Grasscutter.getDispatchServer().getCurrRegion().getRegionInfo(); - + + RegionInfo info; + + if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + if (regionCache == null) { + try { + File file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt"); + String query_cur_region = ""; + if (file.exists()) { + query_cur_region = new String(FileUtils.read(file)); + } else { + Grasscutter.getLogger().warn("query_cur_region not found! Using default current region."); + } + + byte[] decodedCurRegion = Base64.getDecoder().decode(query_cur_region); + QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp.parseFrom(decodedCurRegion); + + RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() + .setIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getGameServerOptions().Ip : Grasscutter.getConfig().getGameServerOptions().PublicIp)) + .setPort(Grasscutter.getConfig().getGameServerOptions().Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .build(); + + regionCache = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); + } catch (Exception e) { + Grasscutter.getLogger().error("Error while initializing region cache!", e); + } + } + + info = regionCache.getRegionInfo(); + } else { + info = Grasscutter.getDispatchServer().getCurrRegion().getRegionInfo(); + } + PlayerLoginRsp p = PlayerLoginRsp.newBuilder() .setIsUseAbilityHash(true) // true .setAbilityHashCode(1844674) // 1844674 diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerStoreNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerStoreNotify.java index c21a15351..a3309a5c1 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerStoreNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerStoreNotify.java @@ -19,7 +19,7 @@ public class PacketPlayerStoreNotify extends GenshinPacket { PlayerStoreNotify.Builder p = PlayerStoreNotify.newBuilder() .setStoreType(StoreType.StorePack) - .setWeightLimit(Grasscutter.getConfig().getServerOptions().InventoryLimitAll); + .setWeightLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitAll); for (GenshinItem item : player.getInventory()) { Item itemProto = item.toProto(); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPullRecentChatRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPullRecentChatRsp.java index 8048e4b0d..5af6999d5 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPullRecentChatRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPullRecentChatRsp.java @@ -1,6 +1,6 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.Config.ServerOptions; +import emu.grasscutter.Config.GameServerOptions; import emu.grasscutter.GenshinConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.GenshinPlayer; @@ -14,7 +14,7 @@ public class PacketPullRecentChatRsp extends GenshinPacket { public PacketPullRecentChatRsp(GenshinPlayer player) { super(PacketOpcodes.PullRecentChatRsp); - ServerOptions serverOptions = Grasscutter.getConfig().getServerOptions(); + GameServerOptions serverOptions = Grasscutter.getConfig().getGameServerOptions(); PullRecentChatRsp.Builder proto = PullRecentChatRsp.newBuilder(); if (serverOptions.WelcomeEmotes != null && serverOptions.WelcomeEmotes.length > 0) { @@ -33,7 +33,7 @@ public class PacketPullRecentChatRsp extends GenshinPacket { .setTime((int) (System.currentTimeMillis() / 1000)) .setUid(GenshinConstants.SERVER_CONSOLE_UID) .setToUid(player.getUid()) - .setText(Grasscutter.getConfig().getServerOptions().WelcomeMotd) + .setText(Grasscutter.getConfig().getGameServerOptions().WelcomeMotd) .build(); proto.addChatInfo(welcomeMotd); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneAreaWeatherNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneAreaWeatherNotify.java index de26dd064..69c40dba0 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneAreaWeatherNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneAreaWeatherNotify.java @@ -12,7 +12,7 @@ public class PacketSceneAreaWeatherNotify extends GenshinPacket { super(PacketOpcodes.SceneAreaWeatherNotify); SceneAreaWeatherNotify proto = SceneAreaWeatherNotify.newBuilder() - .setWeatherAreaId(1) + .setWeatherAreaId(player.getScene().getWeather()) .setClimateType(player.getScene().getClimate().getValue()) .build(); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerLocationNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerLocationNotify.java index f6fa9b8f0..2e9fb479d 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerLocationNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerLocationNotify.java @@ -1,19 +1,20 @@ package emu.grasscutter.server.packet.send; import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.GenshinScene; import emu.grasscutter.net.packet.GenshinPacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.ScenePlayerLocationNotifyOuterClass.ScenePlayerLocationNotify; public class PacketScenePlayerLocationNotify extends GenshinPacket { - public PacketScenePlayerLocationNotify(GenshinPlayer player) { + public PacketScenePlayerLocationNotify(GenshinScene scene) { super(PacketOpcodes.ScenePlayerLocationNotify); ScenePlayerLocationNotify.Builder proto = ScenePlayerLocationNotify.newBuilder() - .setSceneId(player.getSceneId()); + .setSceneId(scene.getId()); - for (GenshinPlayer p : player.getWorld().getPlayers()) { + for (GenshinPlayer p : scene.getPlayers()) { proto.addPlayerLocList(p.getPlayerLocationInfo()); } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTransToPointRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTransToPointRsp.java new file mode 100644 index 000000000..4795c5e9f --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTransToPointRsp.java @@ -0,0 +1,34 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.custom.ScenePointEntry; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.SceneTransToPointRspOuterClass.SceneTransToPointRsp; +import emu.grasscutter.utils.Position; + +public class PacketSceneTransToPointRsp extends GenshinPacket { + + public PacketSceneTransToPointRsp(GenshinPlayer player, int pointId, int sceneId) { + super(PacketOpcodes.SceneTransToPointRsp); + + SceneTransToPointRsp proto = SceneTransToPointRsp.newBuilder() + .setRetcode(0) + .setPointId(pointId) + .setSceneId(sceneId) + .build(); + + this.setData(proto); + } + + public PacketSceneTransToPointRsp() { + super(PacketOpcodes.SceneTransToPointRsp); + + SceneTransToPointRsp proto = SceneTransToPointRsp.newBuilder() + .setRetcode(1) // Internal server error + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneUnlockInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneUnlockInfoNotify.java index 857f9879c..bd1b30685 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneUnlockInfoNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneUnlockInfoNotify.java @@ -12,8 +12,8 @@ public class PacketSceneUnlockInfoNotify extends GenshinPacket { SceneUnlockInfoNotify proto = SceneUnlockInfoNotify.newBuilder() .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(1)) - .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(3)) - .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(4)) + .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(3).addSceneTagIdList(102).addSceneTagIdList(113).addSceneTagIdList(117)) + .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(4).addSceneTagIdList(106).addSceneTagIdList(109)) .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(5)) .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(6)) .addUnlockInfos(SceneUnlockInfo.newBuilder().setSceneId(7)) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketStoreWeightLimitNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketStoreWeightLimitNotify.java index b93130388..04668b00e 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketStoreWeightLimitNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketStoreWeightLimitNotify.java @@ -13,11 +13,11 @@ public class PacketStoreWeightLimitNotify extends GenshinPacket { StoreWeightLimitNotify p = StoreWeightLimitNotify.newBuilder() .setStoreType(StoreType.StorePack) - .setWeightLimit(Grasscutter.getConfig().getServerOptions().InventoryLimitAll) - .setWeaponCountLimit(Grasscutter.getConfig().getServerOptions().InventoryLimitWeapon) - .setReliquaryCountLimit(Grasscutter.getConfig().getServerOptions().InventoryLimitRelic) - .setMaterialCountLimit(Grasscutter.getConfig().getServerOptions().InventoryLimitMaterial) - .setFurnitureCountLimit(Grasscutter.getConfig().getServerOptions().InventoryLimitFurniture) + .setWeightLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitAll) + .setWeaponCountLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitWeapon) + .setReliquaryCountLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitRelic) + .setMaterialCountLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitMaterial) + .setFurnitureCountLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitFurniture) .build(); this.setData(p); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerLocationNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerLocationNotify.java index cb546f0e7..1bb09f8db 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerLocationNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerLocationNotify.java @@ -14,7 +14,7 @@ public class PacketWorldPlayerLocationNotify extends GenshinPacket { WorldPlayerLocationNotify.Builder proto = WorldPlayerLocationNotify.newBuilder(); for (GenshinPlayer p : world.getPlayers()) { - proto.addPlayerLocList(p.getPlayerLocationInfo()); + proto.addPlayerLocList(p.getWorldPlayerLocationInfo()); } this.setData(proto); diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 2e4a7e153..6f6466773 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -21,6 +21,7 @@ import emu.grasscutter.data.def.AvatarData; import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.SceneData; +import emu.grasscutter.utils.Utils; public final class Tools { @@ -29,7 +30,7 @@ public final class Tools { ResourceLoader.loadResources(); Map map; - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMapEN.json")) { + try (FileReader fileReader = new FileReader(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMapEN.json"))) { map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken>() {}.getType()); } diff --git a/start.cmd b/start.cmd new file mode 100644 index 000000000..8c89f359e --- /dev/null +++ b/start.cmd @@ -0,0 +1,160 @@ +@rem +@rem Copyright (C) 2002-2022 MlgmXyysd All Rights Reserved. +@rem + +@if "%DEBUG%" == "" echo off +pushd %~dp0 +set CUR_PATH=%~dp0 +title Grasscutter +call :LOG [INFO] Welcome to Grasscutter +call :LOG [INFO] To proper exit this console, use [Ctrl + C] and enter N not Y. +call :LOG [INFO] +call :LOG [INFO] Initializing... + +set CONFIG=start_config + +set JAVA_PATH=DO_NOT_CHECK_PATH +set MITMDUMP_PATH=DO_NOT_CHECK_PATH +set MONGODB_PATH=DO_NOT_CHECK_PATH + +set SERVER_JAR_PATH=%CUR_PATH% +set DATABASE_STORAGE_PATH=%CUR_PATH%resources\Database + +set SERVER_JAR_NAME=grasscutter.jar +set PROXY_SCRIPT_NAME=proxy + +if exist "%CUR_PATH%%CONFIG%.cmd" ( + call "%CUR_PATH%%CONFIG%.cmd" >nul 2>nul +) + +if not "%JAVA_PATH%" == "DO_NOT_CHECK_PATH" ( + if not exist "%JAVA_PATH%java.exe" ( + call :LOG [ERROR] Java not found. + goto :EXIT + ) +) else set JAVA_PATH= +if not exist "%SERVER_PATH%grasscutter.jar" ( + call :LOG [ERROR] Server jar not found. + goto :EXIT +) + +@rem mitmproxy not found, server only +if not "%MITMDUMP_PATH%" == "DO_NOT_CHECK_PATH" ( + if not exist "%MITMDUMP_PATH%mitmdump.exe" ( + call :LOG [WARN] mitmdump not found, server only mode. + goto :SERVER + ) +) else set MITMDUMP_PATH= +@rem proxy script not found, server only +if not exist "%PROXY_SCRIPT_NAME%.py" ( + if not exist "%PROXY_SCRIPT_NAME%.pyc" ( + call :LOG [WARN] Missing proxy script or compiled proxy script, server only mode. + goto :SERVER + ) else set PROXY_SCRIPT_NAME=%PROXY_SCRIPT_NAME%.pyc +) else set PROXY_SCRIPT_NAME=%PROXY_SCRIPT_NAME%.py + +:PROXY +@rem UAC Administrator privileges +>nul 2>&1 reg query "HKU\S-1-5-19" || ( + call :LOG [WARN] Currently running with non Administrator privileges, raising... + echo set UAC = CreateObject^("Shell.Application"^) > "%temp%\UAC.vbs" + echo UAC.ShellExecute "%~f0","%1","","runas",1 >> "%temp%\UAC.vbs" + "%temp%\UAC.vbs" + del /f /q "%temp%\UAC.vbs" >nul 2>nul + exit /b +) + +call :LOG [INFO] Starting proxy daemon... + +set PROXY=true + +@rem Store original proxy settings +for /f "tokens=2*" %%a in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable 2^>nul') do set "ORIG_PROXY_ENABLE=%%b" +for /f "tokens=2*" %%a in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer 2^>nul') do set "ORIG_PROXY_SERVER=%%b" + +@rem TODO: External proxy when ORIG_PROXY_ENABLE == 0x1 +echo set ws = createobject("wscript.shell") > "%temp%\proxy.vbs" +echo ws.currentdirectory = "%MITMDUMP_PATH%" >> "%temp%\proxy.vbs" +echo ws.run "cmd /c mitmdump.exe -s "^&chr(34)^&"%PROXY_SCRIPT_NAME%"^&chr(34)^&" -k",0 >> "%temp%\proxy.vbs" +"%temp%\proxy.vbs" +del /f /q "%temp%\proxy.vbs" >nul 2>nul + +@rem CA certificate for HTTPS scheme +call :LOG [INFO] Waiting for CA certificate generation... +set CA_CERT_FILE="%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer" + +set /a TIMEOUT_COUNT=0 + +:CERT_CA_CHECK +if not exist %CA_CERT_FILE% ( + timeout /t 1 >nul 2>nul + set /a TIMEOUT_COUNT+=1 + goto CERT_CA_CHECK +) +:EXTRA_TIMEOUT +if %TIMEOUT_COUNT% LEQ 2 ( + timeout /t 1 >nul 2>nul + set /a TIMEOUT_COUNT+=1 + goto EXTRA_TIMEOUT +) +call :LOG [INFO] Adding CA certificate to store... +certutil -addstore root %CA_CERT_FILE% >nul 2>nul + +call :LOG [INFO] Setting up network proxy... +reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f >nul 2>nul +reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "127.0.0.1:8080" /f >nul 2>nul + +:SERVER +if not "%MONGODB_PATH%" == "DO_NOT_CHECK_PATH" ( + if not exist "%MONGODB_PATH%mongod.exe" ( + call :LOG [WARN] MongoDB daemon not found, server only mode. + goto :GAME + ) +) else set MONGODB_PATH= +call :LOG [INFO] Starting MongoDB daemon... +set DATABASE=true + +mkdir "%DATABASE_STORAGE_PATH%" >nul 2>nul + +echo set ws = createobject("wscript.shell") > "%temp%\db.vbs" +echo ws.currentdirectory = "%MONGODB_PATH%" >> "%temp%\db.vbs" +echo ws.run "cmd /c mongod.exe --dbpath "^&chr(34)^&"%DATABASE_STORAGE_PATH%"^&chr(34)^&"",0 >> "%temp%\db.vbs" +"%temp%\db.vbs" +del /f /q "%temp%\db.vbs" >nul 2>nul + +:GAME +call :LOG [INFO] Starting server... +"%JAVA_PATH%java.exe" -jar "%SERVER_PATH%grasscutter.jar" +call :LOG [INFO] Server stopped + +:EXIT +if "%DATABASE%" == "" ( + call :LOG [INFO] MongoDB daemon not started, no need to clean up. +) else ( + call :LOG [INFO] Shutting down MongoDB daemon... + taskkill /t /f /im mongod.exe >nul 2>nul +) +if "%PROXY%" == "" ( + call :LOG [INFO] Proxy daemon not started, no need to clean up. +) else ( + call :LOG [INFO] Restoring network settings... + + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d "%ORIG_PROXY_ENABLE%" /f >nul 2>nul + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "%ORIG_PROXY_SERVER%" /f >nul 2>nul + + call :LOG [INFO] Shutting down proxy daemon... + taskkill /t /f /im mitmdump.exe >nul 2>nul + + call :LOG [INFO] Removing CA certificate... + for /F "tokens=2" %%s in ('certutil -dump %CA_CERT_FILE% ^| findstr ^"^sha1^"') do ( + set SERIAL=%%s + ) + + certutil -delstore root %SERIAL% >nul 2>nul +) + +call :LOG [INFO] See you again :) +goto :EOF + +:LOG +echo [%time:~0,8%] %* \ No newline at end of file diff --git a/start_config.cmd b/start_config.cmd new file mode 100644 index 000000000..575e4d8df --- /dev/null +++ b/start_config.cmd @@ -0,0 +1,25 @@ +@rem +@rem Copyright (C) 2002-2022 MlgmXyysd All Rights Reserved. +@rem + +@echo off +pushd %~dp0 +set CUR_PATH=%~dp0 + +@rem This will not work if your java or mitmproxy is in a different location, plugin as necessary +@rem this just saves you from changing your PATH + +@rem Executable Path +@rem Note: Fill DO_NOT_CHECK_PATH if you need to run it from PATH +@rem without detecting whether the executable file exists +set JAVA_PATH=C:\Program Files\Java\jdk1.8.0_202\bin\ +set MITMDUMP_PATH=%CUR_PATH% +set MONGODB_PATH=%CUR_PATH% + +@rem Utility Path +set SERVER_JAR_PATH=%CUR_PATH% +set DATABASE_STORAGE_PATH=%CUR_PATH%resources\Database + +@rem Utility Name +set SERVER_JAR_NAME=grasscutter.jar +set PROXY_SCRIPT_NAME=proxy \ No newline at end of file